Trees

A tree is a set of nodes in a parent-child relationship.

Tree Terminology

General Tree Representation

A general linked tree structure defines each node with a value, a reference to a parent, and a sequence of references to children.

    node parent(node v)
    sequence& children(node v)

    bool isRoot(node v)
    bool isExternal(node v)

Trees are naturally recursive.

We can also recursively traverse a tree, visiting each node in either pre-order, in-order, or post-order. We can also traverse in depth-first order using a stack, or breadth-first order using a queue.

Binary Trees

A binary tree is simplified because it does away with the child sequence.

    class node
    {
        T value
        node left
        node right
    }

It’s trivially easy to define

    bool isRoot(node v)
    bool isExternal(node v)

A note about the textbook’s definition of tree structures: it is very specific in its use of iterators. For example, Code Fragment 7.24 in the C++ book (similar to C.F. 8.22 in the Java). This obsession with iterators is not necessary. A more pure implementation of a pre-order traversal would look more like this:

  function binaryPreorder(v)
   visit(v)
   if v.left exists
    binaryPreorder(v.left)
   if v.right exists
    binaryPreorder(v.right)

An in-order:

  function binaryInorder(v)
   if v.left exists
    binaryInorder(v.left)
   visit(v)
   if v.right exists
    binaryInorder(v.right)

A post-order:

  function binaryPostorder(v)
   if v.left exists
    binaryPostorder(v.left)
   if v.right exists
    binaryPostorder(v.right)
   visit(v)

Here’s an alternative formulation of a pre-order traversal, showing the stack usage explicitly.

\(S\).push(\(v\))
  while \(S\) is not empty
   \(v\) = \(S\).top()
   visit(\(v\))
   \(S\).pop()
   if \(v\).right exists
     \(S\).push(\(v\).right)
   if \(v\).left exists
     \(S\).push(\(v\).left)

Here’s the same pre-order traversal, but with a queue instead of a stack. Note that breadth-first traversal emerges naturally.

\(Q\).enqueue(\(v\))
  while \(Q\) is not empty
   \(v\) = \(Q\).front()
   visit(\(v\))
   \(Q\).dequeue()
   if \(v\).right exists
     \(Q\).enqueue(\(v\).right)
   if \(v\).left exists
     \(Q\).enqueue(\(v\).left)

We can easily define recursive algorithms to give the depth and height of a node. The height of a proper binary tree:

  function computeHeight(node \(v\))
   if \(v\).isExternal()
    return 0
   else
    return 1 + max(computeHeight(\(v\).left), computeHeight(\(v\).right)

The height of an improper binary tree:

  function computeHeight(node \(v\))
   if \(v\).left and \(v\).right
    return 1 + max(computeHeight(\(v\).left), computeHeight(\(v\).right)
   else if \(v\).left
    return 1 + computeHeight(\(v\).left)
   else if \(v\).right
    return 1 + computeHeight(\(v\).right)
   else
    return 0

The following properties must hold for node count \(n\), height \(h\), external count \(n_E\), and internal count \(n_I\).

If a tree is proper then:

The maximum number of nodes at level \(l\) is \(2^l\).

Note, it’s straightforward to define a general tree in terms of a binary tree.

Binary Search Trees

Invariant: All of the values in the left subtree of a node \(v\) are less than or equal to the value at \(v\), while all of the values in the right subtree of a node \(v\) are greater than the value at \(v\).