Recitation 11

Reviewing HW5 - bst insert

Reviewing HW5 - bst delete

Reviewing HW6 - Rotations, RBTs

An example: ROBDD

Refer to HW5 (pdf) for the following discussion items.

Consider the `bst_insert` function that has three bugs.

One error is that if T is NULL, setting T to newT does not change the binary search tree. Why? Because T is a COPY of B->root, it is NOT B->root itself. So after we set T = newT, B->root is still NULL. To correct this, set B->root = newT instead.

Another error relates to the condition given in the while loop. The condition

`T->left != NULL && T->right != NULL`

implies that we want to continue searching while the node that T points to
has exactly two children. The reasoning here is wrong since we can insert
on to nodes with one child or nodes with 0 children (leaves). In fact, if
you look at the body of the while loop, the insert code is correctly given
and must eventually be executed since we will see a NULL pointer to the
left or right when we find the correct insert point. Thus, one solution to
this error is to change the condition of the while loop to `true`.

The third error has to do with the fact that if you find a node with the same key, you shouldn't set T = newT here since nodes below the matching node would be lost. Instead, reset the data in T to the new element:

`T->data = x;`

Note that there is a difference between the key k and the element x (that contains key k). The tree is ordered based on keys, but the information in each data field is an element. It is possible that we want to insert an element with the same key as one already in the tree. For example, in a structure holding student records, the key might be student ID. If you change your address, we would reinsert your student record with the new information. The insert operation would find the key already in the structure and replace the old record with the new record.

**Recursion vs. Iteration**: You'll note that we used iteration here for
bst_insert which is reasonable since we are only traversing one path down the
tree. If we need to traverse the entire tree, however, iteration becomes more
difficult since we'd have to move down and up the tree, requiring parent
pointers or some auxiliary data structure like a stack to keep track of where
we've been. So for traversing the whole tree, recursion is generally preferred.

Reviewing the `tree_delete` operation, the main thing to think about
here is to understand that this function takes a pointer to some node in
the binary search tree, T, which we consider to be the local root of a
binary search tree. It could be the actual root (B->root) or it could be
the root of a subtree which is also a BST. This function should return a
pointer to the local root of the same tree once the element with the given
key is deleted.

For most cases, the pointer returned by this function will be the same pointer it gets as its parameter since the local root of the BST won't be deleted. But there will be a case where we will need to delete the local root of a BST. In this situation, the code deals with the various cases needed to fix the tree and return a pointer to the new local root for the BST once the appropriate node has been deleted.

If T is NULL, then the tree is empty, so there's nothing to delete, so we return NULL also. That is, if you give me an empty tree and tell me to delete something, I return back an empty tree.

If the key is less than the key in the root, then we need to try to delete the key from the left subtree. Remember that this recursive call will return back a pointer to the updated tree once the delete happens (if it does). So we need to reset our left pointer to point to this new tree. Thus, the code for this is:

`T->left = tree_delete(T->left, k);`

A similar argument applies when we try to delete the key from the right subtree.

If the key is in the local root (T->data), then we need to figure out how to delete it. If the left subtree of T is empty, then using the BST ordering property, the root of the right subtree can be the root of the updated tree, so we can return T->right in this case. Likewise, if the right subtree is empty, then the root of the left subtree can be the root of the updated tree, so we can return T->left. Of course, if both are NULL, we return NULL since we're removing a node that is a leaf so the updated tree will be empty.

What if the node we want to delete has two children? We have a choice to look toward the left or right for a replacement value. In this implementation, we always look left. If the left child has no right child, then a simple fix is to copy the data from the left child to the local root and then removing the left child instead:

T->data = T->left->data; T->left = T->left->left;

But if the left child has two children, then we can't do this since we'll
lose the right subtree of the left child. So using the fact that an inorder
traversal gives the data values in order, we can find the inorder predecessor
of the key to be deleted and copy it to the local root (and then remove
it from its original position). The inorder predecessor of the local root
is simply the largest value in the left subtree of the local root.
So we need the helper function `findLargestChild` to find this value,
remove it from the tree and return it so we can copy it to node T.

Here is a simple recursive implementation for `findLargestChild`. Make
sure you understand why it works and why the preconditions are always
satisfied.

elem findLargestChild(tree T) //@requires T != NULL && T->right != NULL; { if (T->right->right == NULL) { elem x = T->right->data; T->right = T->right->left; // The largest value can have a left child! return x; } else return findLargestChild(T->right); }

In the solution above, we need to "look ahead" to see when the maximum value is to our right so we can change the parent's pointer to point to the maximum node's left child instead. (Think about it: is this valid in a BST?)

Refer to HW6 (pdf) for the following discussion items.

For any arbitrary n-node BST, why are there n-1 possible rotations? Quite simply, every node can be rotated with its parent except for the root, since the root has no parent.

In a red-black tree, the length of the longest path from the root to a leaf is at most twice the length of the shortest path. We can show this using the structure invariants for RBTs. Since the black height is the same, the number of black nodes must be the same in both paths. Also, since a red node can't have a red parent, we can have at most red node between each pair of black nodes along a path from root to leaf. Thus, the longest path can be at most twice as long as the shortest path. That's why the red-black tree remains "balanced"!

Consider the following binary decision tree with three arguments:

x1 0/ \1 x2 x2 0/ \1 0/ \1 x3 x3 x3 x3 0/ \1 0/ \1 0/ \1 0/ \1 [1] [0][1] [1][1] [0][1] [1]

To convert this to an ROBDD, we first see that in two cases, a decision on x3 always leads to [1]:

x1 0/ \1 x2 x2 0/ \1 0/ \1 x3 x3 x3 x3 0/ \1 0||1 0/ \1 0||1 [1] [0] [1] [1] [0] [1]

If a variable xi has both decisions leading to xj (or a terminal node), then the decision on xi is not necessary, so we can factor it out:

x1 0/ \1 x2 x2 0/ \1 0/ \1 x3 [1] x3 [1] 0/ \1 0/ \1 [1] [0] [1] [0]

Now we see that the left subtree off of x1 and the right subtree off of x1 are both the same, so we can merge these:

x1 0||1 x2 0/ \1 x3 [1] 0/ \1 [1] [0]

We can use the first rule again to factor out x1:

x2 0/ \1 x3 [1] 0/ \1 [1] [0]

Finally, we can now reduce the tree by combining the [1] terminal nodes:

x2 0/ |1 x3 | 1/ \0 | [0] [1]

So this is our ROBDD for the original binary decision tree. Functionally, they are equivalent, but the ROBDD requires much less computation. Write down the boolean formula represented by the first binary decision tree and the formula for the ROBDD above. These formulas are equivalent logically.

Review the lecture notes from Prof. Pfenning for more information about ROBDDs.