Data Structures – Trees, Graphs

- Implementing data structures requires the consideration of the speed of operations on the objects
- Objects must be inserted, deleted, and searched as fast as possible for acceptable performance
- Object traversal on data structures such as linked lists proceed in a sequential, linear fashion
- Processing objects sequentially is good for only small collections of objects
- The number of comparisons required in a sequential search is, on average, half the size of the list
- For large lists, a more efficient organizational structure is required for faster operations
 

 

Binary Trees
- Data structure that organizes objects in a non-linear, two-dimensional fashion
- Each node in a binary tree contains two links (pointers) to other nodes (binary nodes)
- Contains a link to the first node in the binary tree referred to as the root node
- Branching out from the root node on both sides are links to the left subtree and right subtree

- The STL makes extensive use of binary search trees (e.g. Associative container: set)

 

 

Binary Tree Operations
- Insertion, deletion, and lookup (or search) operations require that tree nodes be visited
- Visiting tree nodes in order to perform an operation is also referred to as traversing the tree
- Hence, one of the most common tasks performed on a binary tree is traversal
- Ideally, we wish to visit the least amount of nodes possible to perform our operation (i.e. search)
- The fewer the number of visits, the faster the operation
- By positioning tree nodes according to their data value, we can minimize the number of visits
- The data value of a tree node is also referred to as a key
- By organizing nodes in this manner, we construct a binary search tree
 
 

Binary Search Tree
- In a binary search tree, the data value in each node is

- Larger (or greater than) the data value in its left child

- Smaller (or less than) the data value in its right child

 

- Node data values decrease down left branches and increase down right branches
- Every node has a unique data value - no nodes are duplicated in a binary search tree
- With this structure, searching traversal algorithms can be significantly enhanced
- In fact, every time we move down to a child during a search, we eliminate an entire subtree!

- To traverse binary search trees, we can call a method from within the same method

- Known as recursion, this process lends itself well as a method of searching in trees

 

 

Recursion
- An algorithm is recursive if it refers to itself to complete the operation
- Recursive methods are methods that call itself
- Recursive methods must have some test for termination
 
 

Recursive traversals
- Let's consider the design and implementation of binary search tree traversals
- As we traverse the tree, we process (or visit) a node performing some operation
- We continue traversing nodes until we get to a node with no further links (an empty node)
- In our examples, "visiting a node" will refer to simply outputting the data value
- Consider the binary tree example below

 

- To traverse such a tree, we have two choices:

- Visit the node first

- Visit the subtrees first

 

- In addition, we also have an option to traverse either the left or the right subtree
- These various options lead to three different methods of recursive traversal

- Let’s look at each one using this tree as an example

 

Inorder traversal
- Traverse left subtree in an inorder traversal manner
- After left subtree is traversed, visit (process) the node
- After node is visited, traverse right subtree in an inorder traversal manner
- Node values are not processed until values in left subtree are processed

- The tree nodes in our example are processed in the following order under this method

6, 13, 17, 27, 33, 42, 48

 

Preorder traversal
- Visit (process) the node
- After node is visited, traverse left subtree in a preorder traversal manner
- After left subtree is traversed, traverse right subtree in a preorder traversal manner
- Node values are processed before traversal

- The tree nodes in our example are processed in the following order under this method

27, 13, 6, 17, 42, 33, 48

 

Postorder traversal
- Traverse left subtree in a postorder traversal manner
- After left subtree is traversed, traverse right subtree in a postorder traversal manner
- Visit (process) the node
- Node values are not processed until all its children

- The tree nodes in our example are processed in the following order under this method

6, 17, 13, 33, 48, 42, 27

 

 

Binary Tree Design

- Using recursion, let's see how we can design and implement one of the traversal methods above

- We design our class(es) to encapsulate the behavior of a binary tree using templates
- As with stacks, queues, and linked lists, a binary tree class should be able to contain any data type
- We can create a class to encapsulate both a tree node as well as the tree itself
- In our design below, we'll consider the inorder traversal method for a binary search tree

// Binary Tree class

#include <iostream>
#include "BinaryTreeNode.h"

template <class T>
class BinaryTree
{
 private:
  BinaryTreeNode<T> *root;      // root of tree

 public:

  // constructor
  BinaryTree();

  // add node (pass data value of node)
  void insert( const T & );

  // inorder traversal
  void inorder()
    {
      inorderHelper( root );
      cout << endl;
    }

 private:

  // inorder method - recursive
  void inorderHelper( BinaryTreeNode<T> * );
 

};

// Constructor
template <class T>
BinaryTree<T>::BinaryTree()
{
  root = NULL;
}

// Add (insert) node, binary search tree
template <class T>
void BinaryTree<T>::insert( const T &value )
{
  BinaryTreeNode<T> *current;
  BinaryTreeNode<T> *trailer;
  BinaryTreeNode<T> *newNode;

  // Create node to store value
  newNode = new BinaryTreeNode<T>;
  newNode->data = value;

  // Insert according to binary search tree criteria
  if( root == NULL )
    root = newNode;
  else
  {
    current = root;
    while( current != NULL )
      {
        trailer = current;
        if( current->data == value )
          {
            cout << "Duplicates not allowed" << endl;
            return;
          }
        else
          if( current->data > value )
            current = current->lPtr;
          else
            current = current->rPtr;
      }
    if( trailer->data > value )
      trailer->lPtr = newNode;
    else
      trailer->rPtr = newNode;
  }

}

// Inorder traversal - recursive
template <class T>
void BinaryTree<T>::inorderHelper( BinaryTreeNode<T> *p )
{
  if( p != NULL )
    {
      inorderHelper( p->lPtr );
      cout << p->data << " ";
      inorderHelper( p->rPtr );
    }
}  

// Binary Tree Node class
#include <iostream>

// Forward declaration of a templated class
template<class T> class BinaryTree;

template <class T>
class BinaryTreeNode
{
  friend class BinaryTree<T>;   // so tree can access node info

 private:
  T data;                       // data value in node
  BinaryTreeNode<T> *lPtr;      // pointer to left node
  BinaryTreeNode<T> *rPtr;      // pointer to right node

 public:

  // constructor
  BinaryTreeNode();

};

// Constructor
template <class T>
BinaryTreeNode<T>::BinaryTreeNode()
{
  lPtr = rPtr = NULL;
}
   

- Note the following line in the BinaryTreeNode header file

// Forward declaration of a templated class
template<class T> class BinaryTree;

- This takes the place of an #include declaration for a specific purpose

- Note that our BinaryTree header file includes the BinaryTreeNode header file

- We have a situation where two classes refer to each other in their class definitions

- If we include the BinaryTree header in an application, it includes the BinaryTreeNode

- If an include were used above, the BinaryTreeNode would include the BinaryTree

- This process would continue before either class is fully declared

- To avoid such a situation, we simply inform the compiler that a class will be defined later

- This is known as a forward declaration

- Using our classes, let's create a binary tree and traverse using inorder traversal

#include "BinaryTree.h"

main()
{
  BinaryTree<int> iTree;
  iTree.insert( 12 );
  iTree.insert( 10 );
  iTree.insert( 14 );
  iTree.inorder();
}

Output
10 12 14

 

Graphs
- Many simulations and applications involve finding a shortest route or path
- Applications include electrical circuits, highway maps, project planning, etc...
- The study and analysis of such problems is referred to as graph theory
- These problems can be simulated programmatically to find solutions
- Graphs can be represented as an array of linked lists termed adjacency lists
- Similar to other data structures, operations on graphs can include

- Create the graph (i.e. using adjacency lists)

- Clear the graph

- Print the graph

- Determine whether graph is empty

- Traverse the graph