Chapter 4 Trees
4.1 Preliminaries
A path from node n1 to nk is defined as a sequence of nodes n1, n2, ..., nk such that ni is the parent of ni+1 for 1 <= i < k.
The length of this path is the number of edges on the path, namely, k-1. There is a path of length zero from every node to itself. Notice that in a tree there is exactly one path from the root to each node.
For any node ni, the depth of ni is the length of the unique path from the root to ni. The root is at depth 0.
The height of ni is the length of the longest path from ni to a leaf. Thus all leaves are at height 0. The height of a tree is equal to the height of the root. For the tree in Figure 4.2, E is at depth 1 and height 2(from E to longest leaf node P or Q); F is at depth 1(from root to F) and height 1(from F to leaf K or L or M); the height of the tree is 3(from root node to the longest leaf such as P or Q). The depth of the tree(notice this concept is not equal to the root node) is equal to the depth of the deepest leaf; this is always equal to the height of the tree.
If there is a path from n1 to n2, then n1 is an ancestor of n2 and n2 is a descendant of n1. if n1 != n2, then n1 is aproper ancestor of n2 and n2 is aproper descendant of n1.
4.1.1 Implementation of Trees
template <typename Object>
struct TreeNode
{
Object element;
TreeNode *firstChild;
TreeNode *nextSibling;
};
Keep the children of each node in a linked list of three nodes. and keep the sibling of each node as another linklist.
4.1.2 Tree Traversals with an Application
One of the popular uses for tree is the directory structure in many common operating systems, indluding UNIX and DOS. Refer Fig 4.5 a typical directory in the UNIX file system.
This traversal strategy is known as a preorder traversal (先序遍历). In a preorder traversal, work at a node is performed before (pre) its children are processed.
Time complexity analysis: line 1 and 2 is executed exactly once per node, line 4 can be executed at most once for each child of each node. But the number of children is exactly one less than the number of nodes. Finally the for loop iterates once per execution of line 4 plus once each time the loop ends. Thus, the total amount of work by this traversal strategy is constant per node. If there are N file names to be output, then the running time is O(N).
Another common method of traversing a tree is the postorder traversal (后续遍历). The work at a node is performed after (post) its children are evaluated. Figure 4.8 represents the same directory structure as before, with the numbers in parentheses representing the number of disk blocks taken up by each file.
Since the directories are themselves files, they have size too. Suppose we would like to calculate the total number of blocks used by all the files in the tree. The most natural way to do this would be to find the number of blocks contained the subdirectories /usr/mark(30), /usr/alex(9), and /usr/bill(32). The total number of blocks is then the total in subdirectories(71) plus the one block used by /usr, for a total of 2. Refer the pseudocode method size infigure 4.9.
if the current object is not a directory, then size merely returns the number of blocks it uses in the current object. Otherwise, the number of blocks used by the directory s added to the number of blocks(recursively)found in all the children. figure 4.10 trace the size function (postorder)
4.2 Binary Trees
4.2.1 Implementation
An implementation for Binary Tree
template <typename Object>
struct BinaryNode
{
Object element; // the data in the node
BinaryNode *left; // left child
BinaryNode *right; // right child
};
We also do not explicity draw
nullptr links when refering to trees, because every binary tree with N nodes would requir N + 1
nullptr links(this could be shown by mathematical induction)
Binary Tree is not only used for searching but also be used to evaluate as compiler design, refer the example following:
4.2.2 An Example: Expression Trees
Constructing an Expression Tree
4.3 The Search Tree ADT--Binary Search Trees
template <typename Comparable>
class BinarySearchTree
{
public:
BinarySearchTree();
BinarySearchTree(const BinarySearchTree &rhs);
BinarySearchTree(BinarySearchTree &&rhs);
~BinarySearchTree();
const Comparable &findMin() const;
const Comparable &findMax() const;
bool contains(const Comparable &x) const;
bool isEmpty() const;
void printTree(std::ostream &out = std::cout) const;
void makeEmpty();
void insert(const Comparable &x);
void insert(Comparable &&x);
void remove(const Comparable &x);
BinarySearchTree &operator=(const BinarySearchTree &rhs);
BinarySearchTree &operator=(BinarySearchTree &&rhs);
private:
struct BinaryNode
{
Comparable element;
BinaryNode *left;
BinaryNode *right;
BinaryNode(const Comparable &theElement, BinaryNode *lt, BinaryNode *rt)
: element{theElement}, left{lt}, right{rt}
{
}
BinaryNode(Comparable &&theElement, BinaryNode *lt, BinaryNode *rt)
: element{std::move(theElement)}, left{lt}, right{rt}
{
}
};
BinaryNode *root; // pointer to the root node of the BinarySearchTree
// private member for recursive call