Binary Tree Traversals
Running time of operations on Trees
How to ensure O(lg N) running time
AVL Trees
Heaps (the tree kind)
Here is (another) class to represent a binary tree. It will be used to illustrate binary tree traversals. (link to the source code)
#ifndef BINTREENODE_H #define BINTREENODE_H templateclass binTreeNode; #include "btnodevisitor.h" template <class T> class binTreeNode { public: T data; binTreeNode *left; binTreeNode *right; binTreeNode(const T& d) : data(d), left(0), right(0) {} binTreeNode(const T& d, binTreeNode *lf, binTreeNode *rt) : data(d), left(lf), right(rt) {} template <class S, class R> R accept(btNodeVisitor<T,S,R>& v, const S& x) { return v.visit(this, x); } }; #endif
1. Create a binary tree. 2. The tree is not a "linear" structure. It's hard to know what order data was inserted after the tree has been built. What is a "reasonable" order (or orders)? 3. Print all the data in the tree in a "reasonable" order. 4. A binary search tree has the properties a. The data in the tree has a natural ordering (e.g. intgers, strings, etc. have natural orderings. A type representing an Shape might not have a natural ordering. b. For every node in a binary search tree, the data is >= the data in its left child (if present) and < the data in its right child (if present). What tree results from inserting the values 10, 1, 20, 15, 30, 3,4, 2 into an initially empty binary search tree? 5. What is the height of the tree in 4? 6. How many comparisons does it take to find out that 17 is not in the tree? 7. If the same data were inserted in order into a list, how many comparisions would be needed to find out that 17 is not in the list? 8. Why is the height of a binary search tree important for insertion and find operations? 9. A balanced binary tree would have smaller height. What would be a definition of "balanced" that we could use in writing a balanced binary search tree class. That is, we would want to be able to test this condition in the insert method, so it needs to be fairly precise. 10. Preorder traversal of a binary tree with root p: a. Visit the p (e.g. print the data in the node) b. Visit the tree rooted at p->left in preorder. c. Visit the tree rooted at p->right in preorder. What is the preorder traversal of the tree in 4? 11. What is the definition of postorder traversal? of inorder traversal?
... typedef binTreeNodetreenode; void preorderPrint(treenode *p) { if ( p == 0 ) return; cout << p->data << " "; preorderPrint(p->left); preorderPrint(p->right); } void inorderPrint(treenode *p) { if ( p == 0 ) return; inorderPrint(p->left); cout << p->data << " "; inorderPrint(p->right); } void postorderPrint(treenode *p) { if ( p == 0 ) return; postorderPrint(p->left); postorderPrint(p->right); cout << p->data << " "; } void doDrawTree(treenode *p, string lpad, string rpad) { string pad = lpad.substr(0, lpad.size() - 1); if (p == 0) return; doDrawTree(p->right, rpad + " |", rpad + " "); cout << pad << "+--" << setw(3) << p->data << endl; doDrawTree(p->left, lpad + " ", lpad + " |"); } void drawTree(treenode *p) { doDrawTree(p, " ", " "); } int main() { treenode *root; root = new treenode(5); root->left = new treenode(3); root->right = new treenode(10); treenode *p = root->right; p->left = new treenode(8); p->right = new treenode(15); p->left->left = new treenode(7); p->left->right = new treenode(9); root->left->right = new treenode(4); root->left->left = new treenode(1); drawTree(root); cout << endl; cout << "Preorder Traversal:" << endl; preorderPrint(root); cout << "\nInorder Traversal:" << endl; inorderPrint(root); cout << "\nPostorder Traversal:" << endl; postorderPrint(root); return 0; }
+-- 15 +-- 10 | | +-- 9 | +-- 8 | +-- 7 +-- 5 | +-- 4 +-- 3 +-- 1 Preorder Traversal: 5 3 1 4 10 8 7 9 15 Inorder Traversal: 1 3 4 5 7 8 9 10 15 Postorder Traversal: 1 4 3 7 9 8 15 10 5
A / \ B C / \ / D E F Preorder: A B D E C F Inorder: D B E A F C Postorder: D E B F C A
1. The preorder traversal of a binary tree is X Y A B C D 2. The inorder traversal of the same tree is Y A X C B D 3. What is the postorder traversal?
The idea: (a) Binary Tree represents structural arrangement of data. (b) Allow flexibility in order of visiting and processing data elements in a Binary Tree.
#ifndef BINTREENODE_H #define BINTREENODE_H templateclass binTreeNode; #include "btnodevisitor.h" template class binTreeNode { public: T data; binTreeNode *left; binTreeNode *right; binTreeNode(const T& d) : data(d), left(0), right(0) {} binTreeNode(const T& d, binTreeNode *lf, binTreeNode *rt) : data(d), left(lf), right(rt) {} template R accept(btNodeVisitor & v, const S& x) { return v.visit(this, x); } }; #endif
templateclass btNodeVisitor; #include "binTreeNode.h" template class btNodeVisitor { public: virtual R visit(binTreeNode *p, const S& x) = 0; };
#include "btnodevisitor.h" class postfix: public btNodeVisitor{ public: int visit(binTreeNode *p, const int& x) { if (p == 0) { return 0; } p->left->accept(*this, 0); p->right->accept(*this, 0); cout << p->data << " "; return 0; } };
#include "btnodevisitor.h" class sumvisitor: public btNodeVisitor{ public: int visit(binTreeNode *p, const int& x) { int y = x; if (p == 0) { return y; } y += p->left->accept(*this, 0); y += p->data; y += p->right->accept(*this, 0); return y; } };
1. What order were the nodes visited to produce this drawing? +-- 15 +-- 10 | | +-- 9 | +-- 8 | +-- 7 +-- 5 | +-- 4 +-- 3 +-- 1 Preorder Traversal: 5 3 1 4 10 8 7 9 15 Inorder Traversal: 1 3 4 5 7 8 9 10 15 Postorder Traversal: 1 4 3 7 9 8 15 10 5
Let f(N) = number of microseconds to solve a problem of size N for an algorithm. The what is the biggest size problem that can be solved in 1 minute. f(N) Largest N solvable in 1 minute log2(N) 1018000000 N 60000000 Nlog2(N) 2811900 N2 7746 N3 391 2N 25
Insertion/find/deletion: Worst case: Tree is just a list; e.g., every node has a null left link. Worst case running time: O(n) where n is the number of nodes.
Let d be the depth of the node to 'find'. The running time for the operation on such a node is O(d). So in general, Worst case is O(h) where h is the height of the tree.
What do we mean by the "average" height of binary search trees with n nodes?
One possibility is to compute the heights of the binary search trees resulting from inserting each permutation of 1,2,...,n into an initially empty tree. Then compute the average of these n! heights.
It turns out that this average is O(lg n).
This is somewhat hard to prove. However, some intuition can be gained by considering the following:
For n = 4, of the 24 permutations of 1-4 for insertion, the resulting tree heights and their frequencies are: height frequency 3 8 2 16 So the average height is 2.333... log(n+1) = log(5) = 2.3219...
One could also write a program to determine the heights and their frequencies for all permutations of n for other values of n.
The average height could then be computed as the average height of the n! trees generated for each permutaion of the values 1,2,...,n.
We could try to ensure that the heights of both subtrees of the root have equal height or if that is not possible, guarantee that the heights differ by at most 1.
That is not quite enough. For example, the tree we get from inserting the following would have that property, but the height would be approximately n/2 = O(n), not O(log n):
Insert the values 0,1,2,...,100 (n=101) in the order 50, 49,48,47,46, ...,2,1,0,51,52,53,54,55,56,...,100 Both right and left subtrees would have height 49.
We have to ensure that each subtree is also balanced.
So one property would be that at every node in the tree: The difference between the heights of the left and right subtrees should be at most 1. To handle cases like 5 / 4 where the height of the left subtree of 5 is 0, the height of an empty subtree is defined to be -1.
AVL trees are Binary Search Trees with the additional property that the methods maintain a "balanced" property:
For every node in an AVL tree, the heights of its children differ by at most 1.
For the purpose of this definition (and also the implementation), an empty child will be considered to be at height -1.
The AVL tree below shows the height of each node.
The children of 600 have heights 0 (left child 550) and -1 (right child empty). and these differ by only 1.
Maintaining the AVL Tree property, requires that the insert includes some code to rebalance portions of the tree if the property would otherwise be violated.
For example if we try to insert the value 525 into this tree in the usual way as a child of 550, the height of 550 would become 1, while the right child (empty) is still at height -1, a difference of 2.
This problem is solved by:
1. First insert in the usual way for a binary search tree either in the left subtree or right subtree. (Duplicates are not allowed in this version.) 2. Check if the heights of the two children subtrees differs by 2. If so, then rotate nodes to restore the AVL properties.
There are two kinds of rotations: single or double.
The double rotation can be implemented by calling 2 single rotation methods.
There are also two directions to rotate: with the left child or the right child.
The 4 methods are:
1. private static Node rotateWithLeftChild( Node k2 ) 2. private static Node doubleWithLeftChild( Node k3 ) 3. private static Node rotateWithRightChild( Node k2 ) 4. private static Node doubleWithRightChild( Node k3 )
Returning to the example AVL tree, we try to insert 525:
The rotateWithLeftChild method below is called to rotate the left child, 550 to become the parent of 525 and 600:
If we try to insert 425 into this tree, node 500 would then violate the AVL condition. However, we can't use the rotateWithLeftChild method:
A "double" rotation will first rotate 400 with its new right child (450), and then rotate 500 with its new left child (450 again in a new position) to get:
A slightly more typical example of a double rotation is shown below. The value 450 is being inserted:
The same 2 single rotations as before will make 425 the new root with left child 400 and right child 500.
What happens to 425's previous left and right children?
Here is the implementation for this method:
private static Node rotateWithLeftChild( Node k2 ) { // Rotate with k2's left child Node k1 = k2.left; k2.left = k1.right; k1.right = k2; // Adjust the heights since the nodes have been rotated. k2.height = Math.max( height(k2.left), height(k2.right) ) + 1; k1.height = Math.max( height(k1.left), k2.height ) + 1; return k1; }The height method is a static method just returns the height of the node passed to it, but also handles the case if null is passed. In the later case, height returns -1.
Here is Weiss's implementation of this method:
private static Node doubleWithLeftChild( Node k3 ) { k3.left = rotateWithRightChild( k3.left ); return rotateWithLeftChild( k3 ); }
As promised, it is implemented by performing 2 single rotations.
This is a java program that has an AvlTree member and which displays the data from this AVL tree as it allows the user to interactively insert data (no deletions, although it wouldn't be hard to add those too).
We previously mentioned minPriority queues and maxPriority queues. A minPriority queue would have the following methods as an abstract data type:
template <class T> PriorityQueue { public: T deleteMin(); public void insert(const T& x); public boolean empty(); }
A class could implement this interface through composition using any of the various container classes such as C++'s vector or list, or deque, as I asked you to do.
The suggested implementations are not be especially efficient, however. We can do better. In fact we can achieve this efficiency:
Running Time operation Proportional To: Big-Oh Notation deleteMin() log(N) O(log(N)) insert log(N) O(log(N)) Recall for a list implementation, the best we can do is: deleteMin constant O(1) insert N O(N) or the otherway around deleteMin N O(N) insert 1 O(1) Which is better?
The problem with using the list (or vector or deque) to implement the PriorityQueue is that we want both operations, insert and deleteMin to be efficient.
A balanced binary search tree would be one possibility. E.g., an AVL tree. What would the running times be for the two operations?
Another balanced tree which does not require the AVL type rotations, is the MinHeap. (There is also a MaxHeap).
1. All leaf nodes are at depth d or d-1 for some d. 2. All nodes at depth < d - 1 have two children. 3. All nodes at depth d are as far to the left as possible. 4. The value at each node must be <= the values of its children.
This is a MinHeap: This is not a MinHeap 1 1 +---|---+ +---|---+ | | | | 8 3 8 3 +-|-+ +-|-+ +-|-+ +-|-+ | | | | | | | | 9 10 12 4 9 10 12 2 +-| +-| | | 11 11
Insertion works by 1. Add the new item as a leaf in the next available position. (This may destroy the heap condition!) 2. "Bubble" the new item up until the heap condition is satisfied. Swap the item with its parent if it is smaller than the parent.
Insert 1 into the heap 2 2 +---|---+ +---|---+ | | | | 8 3 => 8 3 +-|-+ +-|-+ +-|-+ +-|-+ | | | | | | | 9 10 12 9 10 12 1 2 1 +---|---+ +---|---+ | | | | => 8 1 => 8 2 +-|-+ +-|-+ +-|-+ +-|-+ | | | | | | | | 9 10 12 3 9 10 12 3
Deletion from a heap is only slightly more complicated.
Deletion works by 1. Remove the last item in the heap and use it to replace the item to be deleted. (This may destroy the heap condition!) 2. "Bubble" the new item down until the heap condition is satisfied. Swap the item with its smaller of its children if either child is smaller than the item.