Fundametal Algorithms, Fall 2002

Solutions for Homework 3, Questions 4-7

written by Ofer H. Gill

(4) Given the following tree:

(i) List the keys in an in-order traversal 
(ii) List the keys in a post-order traversal 
(iii) List the keys in a pre-order traversal 
(iv) Prove or disprove: Given two list of keys, one being the
pre-order traversal of a tree T, the other a post-order traversal of a
tree T, you can reconstruct the tree T. Assume all keys of T are
distinct.

Answers: 
(i) 2, 3, 4, 6, 7, 9, 13, 15, 17, 18, 20 

(ii) 2, 4, 3, 9, 13, 7, 6, 17, 20, 18, 15 

(iii) 15, 6, 3, 2, 4, 7, 13, 9, 18, 17, 20 

(iv) The pre and post-order traversals alone don't give a unique
tree. Here's a simple counter-example. Suppose we're given the
following pre and post-order traversals:

Pre-order: 13, 9

Post-order: 9, 13

For this, we can construct either of the following trees:

Here's a more detailed answer to this question...

There are actually two valid assumptions you can make for this part. 

Assumption 1: If, on a given pre- or post-order traversal, you can implicitly infer
the in-order traversal, then you can reconstruct the tree T.  

Assumption 2: If you can't implicitly infer the in-order traversal, you won't always
be able to reconstruct the tree T.  

I'll now give the answers based on each assumption.  

Using Assumption 1: When you have the in-order and pre-order
traversals, you have enough info to construct the tree T. (Post-order
is actually not necessary...) I'll prove this inductively.  

Basis: If the in and pre-order traversals have only one node s. We
know the tree T is unique.  

Inductive Hypothesis: Assume for all m < n that any pre and in-order
traverals consisting of m nodes gives a unique tree.  

Inductive Step: For an in and pre-order traversal consisting of n
nodes... 
Let's assume s is the first node mentioned in the pre-order traversal, 
and s is the kth node mentioned in the in-order traversal. 
We then know s must the the root of the tree. Let L denote the set of
nodes in the left subtree of s, and R denote the set of nodes in the
right subtree of s. (Note that |L| < n and |R| < n.)  

We know that the 1st to (k-1)th elements of the in-order sequence are
all the elements in L, and the 2nd to kth elements of the pre-order
sequence are also all the elements in L.   

We know that the (k+1) to nth elements of the in-order sequence are
all the elements in R, and the (k+1) to nth elements of the pre-order
sequence are also all the elements in R.   

Thus, using the subsequence of the in/pre-order sequences from L,
since |L| < n, we know the left subtree must be unique. And, using
the subsequence of the in/pre-order sequences from R, since |R| < n,
we know the right subtree must be unique. Therefore, the entire tree
must be unique, and we're done!  

Using Assumption 2: The post and pre-order traversals mentioned in
parts (ii) and (iii) do not give a unqiue tree. In addition to the
tree picture above, the tree shown below is also constructable from
the sequences of parts (ii) and (iii).

(5)

Given the rotate(u) operation, where u is a node in a binary tree: 
(i) Describe how many pointers get updated in the worst case. 
(ii) Describe an implementation of rotate.

Answer: 
(i) Using the figure mentioned on page 6 in the Lecture III notes (and
assuming w is the parent of v before rotation), and assuming without
loss of generality that u is initially to the left of v (the argument for when
u is initially to the right of v is similar), then in the worst case, 
when w and B are non-null, rotating u involves changing:

- u's parent pointer
- u's right pointer
- v's left pointer
- v's parent pointer
- B's parent pointer
- either w's left pointer, or w's right pointer

Thus, six pointers are updated.


(ii) Note: I'm assuming u is always non-null, and the parent of u is always
non-null. (If I want, It's very simple for me to check if u is null,
and if u's parent is null and do nothing for these cases.)

Using pseudo-C/C++/Java code, we have:

rotate(u) {
   Node A, B, C, v, w;
   boolean isLeft;

   v = u.parent;
   if (v.left == u) {
         /* u is to the left of v here... */
         A = u.left; B = u.right; C = v.right;
	 w = v.parent;
	 if ((w != null) && (w.left == v)) {
	       isLeft = true;
	 } else {
	       isLeft = false;
	 }
	 u.right = v;
	 u.parent = w;
	 v.left = B;
	 v.parent = u;
	 if (B != null) {
	       B.parent = v;
	 }
	 if (w != null) {
	       if (isLeft) {
	             w.left = u;
	       } else {
	             w.right = u;
	       }
	 }
   } else {
         /* u is to the right of v here... */
         A = v.left; B = u.left; C = u.right;
	 w = v.parent;
	 if ((w != null) && (w.left == v)) {
	       isLeft = true;
	 } else {
	       isLeft = false;
	 }
	 v.right = B;
	 v.parent = u;
	 u.left = v;
	 u.parent = w;
	 if (B != null) {
	       B.parent = v;
	 }
	 if (w != null) {
	       if (isLeft) {
	             w.left = u;
	       } else {
	             w.right = u;
	       }
	 }   
   }
   return u;
}

 (Notice how I was very careful not to ever touch any pointers of
a node if that node is null. This is an important thing to remember in
programming, because this can cause at runtime things like the
infamous NullPointerException or Segmentation Fault.)

(6)

In a variant of the LookUp operation, our goal is to, on a call to
LookUp(u, k), search the tree T_u rooted at u for a node v with key k. If
such a node v exists, we make v the root of the subtree T_u, otherwise
we don't care what happens to T_u and return null. We want to
implement LookUp using repeated rotation. Here's one attempt to
implement LookUp:

0:  LookUp(u, k) {
1:     if (u == null) return null;
2:     if (u.key == k) return k;
3:     if (u.key > k) return LookUp(rotate(u.left), k);
4:     return LookUp(rotate(u.right), k);
5:  }

(i) What's wrong with this implementation?
(ii) Give a corrected version of this procedure where pointer changes
are reduced to mere rotations.

Answer:
(i) Actually few things are wrong here. (Some I didn't mention in
class because I was hoping students could find them out on their own.)

First off, left x denote u.left or u.right, whichever one might be
passed into the rotate method in the code shown above. If x is null, 
the rotate implementation used in question 5 won't work. However, a
simple modification in the rotate code that checks when x is null will
fix this.

Second, and this problem is much more serious, note the following:

Let's suppose we're given the following tree shown below, where u is
the root of this tree, and we call LookUp(u, 11).

After rotating the tree in line 4, we make a second call to LookUp, this second call seeks 11 on the following tree, where the u in the second call is as shown here:

And, after rotating the tree in line 3, we make a third call to LookUp, this third call seeks 11 on the following tree, where the u in the third call is as shown here:

What's wrong with this picture? The u used in the first and third calls are identical, and trees used in the first and third call are identical, and the key k we seek is identical. Everything is identical. What does this mean? It means our program will loop forever! (And looping forever can be just as bad as accessing pointers of null nodes, if not worse!)

(ii) There are several ways to fix this. One simple fix is a
"2-pass algorithm" that passes thru the tree twice. Once to look for a
node with key k, the second time to rotate this node to the root.

We do this by making an auxiliary method called BinLookUp where
calling BinLookUp(u, k) returns v such that v.key is k if such a v
exists, and null otherwise, and we don't modify the tree in BinLookUp!
We use BinLookUp to help implement LookUp, where LookUp is where the
tree changes are done. The code is as follows:

BinLookUp(u, k) {
    if (u == null) return null;
    if (u.key == k) return u;
    if (u.key > k) return BinLookUp(u.left, k);
    return BinLookUp(u.right, k);
}

LookUp(u, k) (
    Node r, x;
    r = u.parent;
    x = BinLookUp(u, k);
    if (x != null) {
        while (x.parent != r) {
	   rotate(x);
	}
    }
    return x;
}

An alternative fix to LookUp is a "1-pass algorithm". This algorithm
is more sophisticated, but runs faster on certain trees. It goes as 
follows:

LookUp(u, k) {
    if (u == null) return null;
    if (u.key == k) return k;
    if (u.key > k) {
        if (u.left.key < k) return LookUp(rotate(rotate(u.left.right)), k);
	else return LookUp(rotate(u.left), k);
    } else {
        if (u.right.key > k) return LookUp(rotate(rotate(u.right.left)), k);
	else return LookUp(rotate(u.right), k);
    }
}

(7) Given the following AVL tree (note that I added node 49 to make this an AVL tree, as a few students cleverly pointed out that the tree would not be AVL otherwise.):

Show what happens upon the following operations (include intermediary
steps):

(a) Insert key 1 into the tree.
(b) Delete key 15 from the original tree (not the tree after part (a)).
(c) Given an AVL with 20 nodes, what's the range for its height?

Answer:
Using schematic representation for the parts of the tree that don't
change, we get...

(a) Adding 1 to the tree gives us initially:

We then rotate 3 to get:

And this tree is AVL balanced.

(b) Deleting 15 from the tree gives us initially:

We then rotate 10 to get:

And this tree is AVL balanced.

(c) Based on the Lecture III notes, page 13, (assume logf means log
of base fi) any AVL tree with n nodes has height at most 
logf(n) = ln(n) / ln(fi).

Plugging in n = 20 here gives us roughly 6.225..., which means the
height is at most 6. (Since heights can only be of integer values.)

And, based on page 12 in the Lecture III notes, we can actually do better
than this. Computing mu(h) for h=5 and h=6 using the recursive formula 
tells us that mu(5)=20, mu(6)=33. That means an AVL tree with height 5 has 
at least 20 nodes, and it is impossible to get height 6.  Hence, 5 is
an upper bound. And, we can construct a 20-node AVL tree with height
5. Using the drawings for T0, T1, T2, and T3 mentioned in page
12 of the Lecture III notes, we can then draw T4 and T5. T5 is a
20-node tree with height 5. Hence, 5 is an absolute upper bound to the
height of a 20-node tree.

To find the lower bound, the mu function listed on page 12 (marked as
(1)) can be modified to make a lower bound, but a simpler way to find
the lower bound is a follows... Minimum height for an AVL tree is
achieved by packing as many nodes onto each level as possible. With
this in mind, we're sure that the minimum heights of the AVL trees
with 1, 3, 7, and 15 nodes are 0, 1, 2, and 3 respectively. (See
drawing below...)

And clearly, based on the drawing above of the tree with 15 nodes, if we were to have any more than 15 nodes, the height would have to be at least 4. Thus, a 20-node AVL tree must have height at least 4.