Data Structures and Algorithms

Trees and Tree Algorithms

Definition

Trees came out of graph theory. This concept allows us to make concrete data structures that allow us to implement the tree structure.

One way we could do this:

struct Node {
    T datum;
    Node *left, *right;
};

This is efficient for moving down a tree from parent to child. No easy way to go backwards.

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

  • Root: top-most vertex
  • Parent/child: direct links in a tree
  • Siblings: children of the same parent
  • Ancestor: predecessor in tree
  • Descendent: successor in tree
  • Internal node: a node with children
  • External (leaf) node: a node without children
  • Ordered tree: linear ordering for the children of each node
  • Binary tree: every node has at most two children

  • Depth of root is 0, depth of node is depth of parent + 1.

  • Height of leaf is 0, height of node is max(height(children)) + 1.

Proper Binary Tree

A proper binary tree is a tree where all external nodes have zero children, and each internal node have two children.

A complete binary tree is a tree where every level is full except the last level, which is filled from left to right.

Binary Tree Array Implementation

Array-based binary tree implementation:

  • Root at index 1
  • Left child of node at index 2i
  • Right child of node at index 2i + 1
  • Some indices may be skipped
  • Can be space prohibitive for sparse trees

Complexity of array implementation:

  • Insert key (best case): \(O(1)\)
  • Insert key (worst case): \(O(n)\)
  • Delete key (worst case): \(O(n)\)
  • Parent: \(O(1)\)
  • Child: \(O(1)\)
  • Space (best case): \(O(n)\)
  • Space (worst case): \(O(2^n)\)

Pointer-based binary tree implementation

Complexity of node-based implementation:

  • Insert key (best case): \(O(1)\)
  • Insert key (worst case): \(O(n)\)
  • Delete key (worst case): \(O(n)\)
  • Parent: \(O(n)\)
  • Child: \(O(1)\)
  • Space (best case): \(O(n)\)
  • Space (worst case): \(O(n)\)

You can improve the parent operation by introducing a parent pointer. However, this is not that common because while it might make things easier to use parent pointers, there is nothing you can do with parent pointers that you couldn't do without them.

Translating General Trees into Binary Trees

Start at the root node. Of its multitude of siblings, the first sibling becomes the left child of a, and c and d become children of b. C has children e and f, etc. Basically, you go through and each two elements become children of the new nodes.

Traversal

  • Preorder traversal: Visit node, visit left, visit right. (Depth-first search)
Algorithm preorder(T, v)
    visit node v
    for (node c : v.children())
        preorder(T, c)
  • Inorder traversal: Visit left, visit node, visit right. (Depth-first search)
Algorithm inorder(T, v)
    inorder(T, leftchild(v))
    visit node v
    inorder(T, rightchild(v))
  • Postorder: Visit left, visit right, visit node. (Depth-first search)
Algorithm postorder(T, v)
    for (node c : v.children())
        postorder(T, c)
    visit node v
  • Level order: Visit all nodes in depth. (Breadth-first search)