Trees

Introduction

    Original version by Dr. Dee Parks. Modifications were made by Rahman Tashakkori in July 2004.

  1. A tree is a nonlinear data structure made of nodes and links. In a binary tree each node has up to two children, a left child and/or a right child. If a node has no children (both its links are NULL), we call it a leaf. The beginning of the tree, the topmost node, is called the root. Each node of a tree has exactly one parent, with the exception of the root. We also use the terms ancestor and descendant when discussing trees. All of the nodes on the path from node x to the root, including x and the root, are ancestors of x. All of the nodes on the path from node x to a leaf are descendants of x. Two nodes are siblings if they have the same parent. Any node in a tree can be viewed as the root of a smaller tree. This smaller tree contains the node and all the node's descendants, and is called a subtree of the original tree. For a node in a binary tree, the nodes beginning with its left child and below are its left subtree. The nodes beginning with its right child and below are its . The number of links from the root to node x is called the depth of x. The depth of a tree is the maximum depth of any leaf of the tree. Sometimes this is referred to as the height of the tree.

  2. In a full binary tree, all the leaves are at the same depth. In a complete binary tree, leaves are all at depth d or at depth d+1. Those at depth d+1 are all at the left of the tree. Every full tree is a complete tree. Draw a picture of a full binary tree and a complete binary tree that is not a full tree.







  3. Not all trees are binary trees. In general, the nodes of a tree may have any number of children. Still, each node has only one parent. There are no cycles in a tree.

Tree Representations

  1. Interestingly, a complete binary tree can be stored in an array. We don't use real links, only imaginary ones. We put the elements of the tree in the array level by level. The root goes at position 0. The left child of the root goes at position 1 followed by the right child at position 2. The four nodes on the next level of the tree go in positions 3, 4, 5, and 6, and we continue from there. Draw a complete binary tree here and show how it is placed in an array. Example: 1 3 5 6 8 12 13 7 10
  2. We need to be able to traverse a tree using the links from parent to child. How can we find the left child of the node stored in cell x of a complete binary tree? The right child? The parent?
    1. The left child of the node in cell x is located in cell 2x+1.
    2. The right child is in cell 2x+2.
    3. The parent of the node in cell x is located in cell (x-1)/2 (using integer division.)
      Use the tree in part (4) and the above formulas to find out the children and parents of node that holds 6.

  3. Another way to store the nodes of a binary tree is to use actual links. We declare a struct BinaryTreeNode as follows:
    template <class Item>
    struct BinaryTreeNode
    {
    Item data;
    BinaryTreeNode *left;
    BinaryTreeNode *right;
    };

  4. A NULL child pointer indicates that such a child does not exist. The above node structure gives us the ability to traverse a tree from top to bottom. If we need to go from a node to its parent we could add another link to the structure.

  5. A separate pointer is used to store the address of the root of a tree. This is similar to the way a head pointer is used to point to the beginning of a linked list. An empty tree is indicated by a NULL root pointer.

A Toolkit for Binary Tree Nodes

  1. In this section we will look at a binary tree toolkit similar to our linked list toolkit. The above definition of a BinaryTreeNode is the basis of our structure. Our toolkit contains only four functions for now.

  2. A function to create a new node:
    template <class Item>
    BinaryTreeNode<Item>* create_node(
    const Item& entry,
    BinaryTreeNode<Item>* l_ptr = NULL,
    BinaryTreeNode<Item>* r_ptr = NULL
    )
    {
    BinaryTreeNode<Item> *result_ptr;
    result_ptr = new BinaryTreeNode<Item>;
    result_ptr->data = entry;
    result_ptr->left = l_ptr;
    result_ptr->right = r_ptr;
    return result_ptr;
    }

  3. A function to test whether a node is a leaf:
    template <class Item>
    bool is_leaf(const BinaryTreeNode<Item>& node)
    {
    return (node.left == NULL) && (node.right == NULL);
    }
  4. A function to return nodes to the heap:
    template <class Item>
    void tree_clear(BinaryTreeNode<Item>*& root_ptr)
    {
    if (root_ptr != NULL)
    {
    tree_clear(root_ptr->left);
    tree_clear(root_ptr->right);
    delete root_ptr;
    root_ptr = NULL;
    }
    }
    This function is simple to write because it is recursive. The base case is the empty tree. In that case there is nothing to do. Otherwise, we clear the left subtree, then clear the right subtree, then delete the root and set the root pointer to NULL. Clearing a subtree is a smaller version of the original problem and thus leads to a recursive solution.

  5. A function to copy a tree:
    template <class Item>
    BinaryTreeNode<Item>* tree_copy(BinaryTreeNode<Item>* root_ptr)
    {
    BinaryTreeNode<Item> *l_ptr;
    BinaryTreeNode<Item> *r_ptr;
    If (root_ptr == NULL)
    return NULL;
    else
    {
    l_ptr = tree_copy(root_ptr->left);
    r_ptr = tree_copy(root_ptr->right);
    return create_node(root_ptr->data, l_ptr, r_ptr);
    }
    }
    This function is also recursive. The base case is again the empty tree. If the tree is not empty then we copy its left subtree, then copy its right subtree, then create a root node and attach the two subtrees to its left child and right child. Copying a subtree is a smaller version of the original problem and thus leads to the recursive solution.