15-122 Principles of Imperative Computation
Recitation 11

Reviewing HW5 - bst insert
Reviewing HW5 - bst delete
Reviewing HW6 - Rotations, RBTs
An example: ROBDD

Reviewing HW5 - Binary Search Trees - insert

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 HW5 - Binary Search Trees - delete

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?)

Reviewing HW6

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"!

An example: ROBDD

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.


written by Tom Cortina, 11/03/10