CSC148H lab -- week 6


Here are the instructions for the week 6 CSC148H lab. To earn your lab mark, you must actively participate in the lab. As always, you will work as a pair, taking turns being the driver and the navigator. The driver types, and the navigator watches for mistakes, thinking ahead. At the end of each exercise, show your TA what you've done, and then switch roles.

This week's exercises are on the topic of binary search trees and iterators.

Exercise 1: Understanding bst.py

Download the file bst.py. This is the implementation of the binary search tree that we looked at the past week in class. The delete method is split into two different cases. For the case in which N, the node being deleted, has 0 or 1 children, the delete method simply eliminates N by promoting its child up to N's current position.

However, for the case in which N has two children, the delete method has to do something more sophisticated in order to remove the node. Promoting one of N's child nodes is not an option, since the BST property may no longer hold. Instead, the delete method has to find another node in the tree that it can put in N's place. One such node that can be safely put in N's place is N's successor node S, which is the next larger node in the tree. S is the leftmost child in N's right subtree, and is guaranteed to have at most 1 child (in particular, it can only have a right child). This last property of the successor allows us to remove it from its current position quite easily. To place S in N's current position, S's left and right child attributes are updated to be the same as N's left and right child attributes, and then the left child (or right child, whichever is appropriate) of N's parent is set to point to S. This is essentially how the delete method of the TreeNode class in bst.py works. As discussed in lecture, however, the implementation of TreeNode in bst.py doesn't explicitly keep track of a node's parent. This means that whenever the delete method has to modify the parent of the node being deleted, it cannot do it by simply accessing the parent (as is done in the code in your textbook). To get around this problem, the delete method always returns the root node of the current subtree. Whenever a recursive call to delete is made on a node's child, the child attribute (i.e., left or right) is set to point to the value returned by the call. In this way, when the delete method is called on N (the node being deleted), the call will return S (the successor), and the parent node will assign its left child (or right child, whichever is appropriate) to point to S, as required. You can see the details of this in bst.py.

The only goal of this exercise is to ensure that you understand how the delete method in bst.py currently works. To this end, there are two questions in bst.py labelled as "# Ex1 Question: ... ". Answer these questions with your partner. Once you have done this and are convinced  that you understand the code, you're ready to move to exercise 2. If you are having trouble understanding the code, ask the TA for assistance.

Exercise 2: Modifying the delete method to use the predecessor node

As some people noticed in lecture, whenever the node being deleted has two children, we don't necessarily have to replace it with its successor node. The predecessor node works just as well. The predecessor of a node N is the largest node smaller than N.

Recall that the successor of a node N that has two children is the leftmost child of N's right subtree. Where is the predecessor of a node N that has two children?

In the TreeNode class, implement the following method
Using this method, modify the delete method so that when the node being deleted has two children, it is replaced by its predecessor node instead of its successor. You can do this in a number of steps:
Run bst.py and use the interactive command prompt to add a number of nodes to the tree. You can use the following commands:
Try deleting some nodes that have two children, and observe how the predecessor node takes the place of the node being deleted. Show your TA.

Exercise 3: Merging Binary Search Trees


Write an iterative function merge(tree1, tree2) that takes two instances of the class BinarySearchTree as arguments, and creates a new binary search tree containing all the nodes of both trees. Do this as follows
(The __iter__ method is defined on the BinarySearchTree class, so iterating through the elements should be easy. In particular, you can iterate through the elements of a BST in the same way you can iterate through elements of a Python list or dictionary. )

Test your code. You can print the resulting tree using the print_tree method supplied in bst.py. (Note: print_tree takes as an argument the root node of the tree, not the tree itself).

What do you notice about the general shape of the merged BST? Why does it take this shape? (Hint: consider the order in which the iterator returns nodes.)

Show your TA.

Exercise 4: Set Difference with Binary Search Trees

Write an iterative function diff(tree1, tree2) that takes two instances of the class BinarySearchTree as arguments, and creates a new binary search tree containing the (key,value) pairs from tree1 that are not also in tree2. Test your code and show your TA.

Exercise 5: Determining the depth of a node

In the BinarySearchTree class and the TreeNode classes, create a recursive method get_depth(self, key)  that returns the depth of a node with the given key. You can assume the method will only be called when the key exists in the tree. Test your code and show your TA. (Recall that the depth of a node is the number of edges traversed on a path from the root to the node.)