Notes on Trees

(related reading: Main & Savitch, Chapters Ten and Eleven)

Rooted trees

A tree is a collection of nodes. An empty tree has no nodes. Non-empty trees have these properties:

A Family Tree

                 Eleanor
                /   |   \
	       /    |    \
          David   Daniel   Judy
	 /  |      |    \
	/   |      |     \
   Ellen  Maggie  Andrew  Michael
     |
     |
    Ben         

Tree terminology

There is a great deal of terminology associated with trees, most of it relates to family trees.

Height and depth

For example, the depth of Andrew in the family tree (above) is 2. The depth of Eleanor is 0. The depth of Ben is 3 and, since Ben is the deepest node in the tree, the height of the tree is 3.

Order among nodes

There are several ways to impose order among the nodes of a tree, all centered around these two notions:
  1. Order by depth
  2. Ordered children --- if a node has three children, an order is imposed indicating the first child, the second child and so on.

Binary trees

A binary tree is a rooted tree such that every node has at most two children. The children (when present) are known as the left child and right child. Binary trees are extremely useful for ordering data --- and for allowing efficient access to that data.

Implementing binary trees in C++

There are numerous ways to implement binary trees:

A first attempt

template <class Item>
class BinaryTree {
public:
  ...
private:
  Item val;
  BinaryTree *left;
  BinaryTree *right;
};
template <class Item>
class BinaryTree {
public:
  ...
  // Constant member functions
  size_t height() const;
  size_t size() const;
  bool isLeaf() const;
  BinaryTree *leftSubtree() const;
  BinaryTree *rightSubtree() const;
  // Friend functions
  friend BinaryTree *merge(Item v, BinaryTree *L, BinaryTree *R);
private:
  ...
};
template <class Item>
class BinaryTree {
public:
  // Constructors
  BinaryTree(Item v); // create a leaf consisting of v
  BinaryTree(const BinaryTree& source); // copy constructor

  // Destructor
  ~BinaryTree();

  // Modification member functions
  void operator=(const BinaryTree& source); // assignment op
  ...
private:
  ...
}

A Simple Example

BinaryTree<int> *B = new BinaryTree<int>(8);
//  B --> (8)

BinaryTree<int> *C = new BinaryTree<int>(5);
//  C --> (5)

BinaryTree<int> *D = new BinaryTree<int>(2);
//  D --> (2)

BinaryTree<int> *E = merge(4, D, C);
//  E --> (4)
//        / \
//      (2) (5)

BinaryTree<int> *F = merge(6, E, B);
//  F --> (6)
//        / \
//      (4) (8)
//      / \
//    (2) (5)

Leaf Constructor

template <class Item>
BinaryTree<Item>::BinaryTree(Item v)
// create a leaf consisting of v
: val(v), left(NULL), right(NULL)
{ }

Some Simple Constant Member Functions

template <class Item> 
bool BinaryTree<Item>::isLeaf() const 
{ 
  return (left==NULL && right==NULL);
}

template <class Item> 
BinaryTree<Item> *BinaryTree<Item>::leftSubtree() const 
{ 
  return left;
}

template <class Item> 
BinaryTree<Item> *BinaryTree<Item>::rightSubtree() const 
{ 
  return right;
}

Size

template <class Item>
size_t BinaryTree<Item>::size() const
{
  if (isLeaf())
    return 1;
  else
    if (left==NULL) 
      return (1 + right->size());
    else if (right==NULL)
      return (1 + left->size());
    else
      return (1 + left->size() + right->size());
}

Height

template <class Item> 
size_t BinaryTree<Item>::height() const 
{ 
  if (isLeaf())
    return 0;
  else
    if (left==NULL) 
      return (1 + right->height());
    else if (right==NULL)
      return (1 + left->height());
    else
      return (1 + max(left->height(), right->height()));
}

Merge

template <class Item>
BinaryTree<Item> *merge(Item v, BinaryTree<Item> *L, BinaryTree<Item> *R)
// a friend of class BinaryTree<Item<
{
  BinaryTree<Item> *T = new BinaryTree<Item>(v); // start with a leaf

  T->left = L;
  T->right = R;
  return T;
}
We need to be careful. Because we are swinging pointers and not actually using a deep copy, not all uses of merge (as defined above) result in a proper tree:
BinaryTree<int> *C = new BinaryTree<int>(5);
BinaryTree<int> *D = new BinaryTree<int>(2);
BinaryTree<int> *E = merge(4, D, C);
BinaryTree<int> *G = merge(6, E, C);
//  G --> (6)
//        / \
//      (4) (8)
//      / \ /
//    (2) (5)
C (5) no longer has a unique parent so this is not a real tree. (It is an example of something we will see later known as a directed acyclic graph (DAG).)

Tree traversals

A tree traversal is the process of iterating over the value stored at each node in a tree exactly once. In the process you may actually pass through a node more than once, but the action is performed on the value of the node only one time. When the action is performed largely determines the order of traversal. Sometimes performing the action at a node is referred to as visiting.

           (8)
	  /   \
       (2)     (21)
      /   \     /         
    (1)   (5) (13)
          /
        (3)

pre-order:  8, 2, 1, 5, 3, 21, 13
post-order: 1, 3, 5, 2, 13, 21, 8
in-order:   1, 2, 3, 5, 8, 13, 21

Traversals need be performed recursively - we use the recursion to allow us to backtrack up the tree without repeating ourselves.

Algorithm for a pre-order traversal:
  1. visit this node
  2. recursively perform pre-order traversal on left subtree
  3. recursively perform pre-order traversal on right subtree
Algorithm for a post-order traversal:
  1. recursively perform post-order traversal on left subtree
  2. recursively perform post-order traversal on right subtree
  3. visit this node
Algorithm for an in-order traversal:
  1. recursively perform in-order traversal on left subtree
  2. visit this node
  3. recursively perform in-order traversal on right subtree

A better BinaryTree class template

Before looking at a C++ implementation of the traversals, let's rethink our BinaryTree class. Examine the interface of the binary tree class template here.

Changes from our previous attempt:
  • Instead of using a merge friend function, we make a constructor to perform the merge:
    BinaryTree(Item v, BinaryTree<Item> *L, BinaryTree<Item> *R);
    
  • We have allowed uniform access to the private members with valOf, rightOf, and leftOf. We also allow these members to be updated in a uniform way with setVal, setLeft, and setRight.


  • We add a depth functon to compute the depth of any node in a tree. (Notice how we can think of a node as a tree and vice versa. Think about why this is so.)

    How do we implement depth? We can use recursion: if a node has no parent (it's the root) then it has depth 0 otherwise the depth is 1 + depth of the node's parent.


  • How do we determine the parent of a node in a tree? The easiest way is to add a private member to our class, parent, which is a pointer to the parent of that node. (parent is NULL when the node is the root.) We also add a parentOf member function so users of the class can move up the tree as well as down:
    BinaryTree<Item> *parentOf() const;
    

    We do not add any function to update the parent of a node --- such a function is unnecessary as the parent of a node is changed by using the merge constructor, setLeft, or setRight.

    Now, depth can be defined recursively:
    template <class Item>
    size_t BinaryTree<Item>::depth() const 
    { 
      if (parent==NULL)
        return 0;
      else
        return (1 + parent->depth());
    }
    

Implementing tree traversals

Because we have public access to the value of a node and its children (through the get-access functions valOf, leftOf, rightOf) we can implement traversals outside of the class. For example, might write an in-order print as:
template <class Item>
void inOrderPrint(BinaryTree<Item> *bt)
{
  if (bt != NULL) {
    inOrderPrint(bt->leftOf());
    cout << bt->valOf() << "\n";
    inOrderPrint(bt->rightOf());
  }
}

Of course, we might want to have related functions that print the values in-order, but slightly differently (perhaps no carriage return). Or we might want to do an in-order traversal that does an entirely different action --- like put the elements into a global array. We know how to generalize the in-order traversal. We can use function pointers as in:

template <class Item>
void inOrderWalk(BinaryTree<Item> *bt, void (*f)(Item))
{
  if (bt != NULL) {
    inOrderWalk(bt->leftOf(), f);
    f(bt->valOf());
    inOrderWalk(bt->rightOf(),f);
  }
}
For example, for a BinaryTree<int> we could achieve the same functionality as inOrderPrint by defining a function:
void print(int x)
{
  cout << x << "\n";
}
and then for a binary tree *t, calling inOrderWalk(t, print).

However, there is a limitation to the generality of this method --- we cannot now decide to use a function argument that takes an argument that is a reference to an Item. We'd like to be able to do that as well. We can achieve that by adding a template parameter:
template <class Item, class Func>
void inOrderWalk(BinaryTree<Item> *bt, Func f)
{
  if (bt != NULL) {
    inOrderWalk(bt->leftOf(), f);
    f(bt->valOf());
    inOrderWalk(bt->rightOf(),f);
  }
}
Now we can use any function as an argument to this function that can be called that fits the form f(bt->valOf()). This code as well as code performing general pre-order and post-order traversals can be found here.