Data Structures and Algorithms — Homework 10

Due by 11:59pm Wednesday, April 10

Reading

Instructions

IMPORTANT: Download the folder assign10.zip and use the file BinarySearchTree.java in the folder as your starting point for this assignment.

This folder includes the interactive test program TestBinarySearchTree, which may be of help in testing and debugging your methods. There is also a precompiled tester program called BSTChecker that will automatically check whether your completed BinarySearchTree methods are working correctly. To test an individual method, simply type java BSTChecker method_name. To test all methods at once, type java BSTChecker. However, to test the buildBalancedTree and remove methods, which are extra credit, you'll need to use the first form of the command. If there is a problem with your code, BSTChecker will very likely catch it, but unfortunately it doesn't give any further details about where exactly the problem occurred or what caused it. Maybe someday I'll have time to improve its level of feedback, but for now this will have to do.

Programming Exercises

  1. A pre-order traversal of a tree visits the root element first, then recursively visits all elements in the left subtree, followed by all elements in the right subtree. An in-order traversal is similar, but we visit all elements in the left subtree first, followed by the root element, followed by all elements in the right subtree. In class we implemented the methods printPreOrder and printInOrder to print out the elements using these two types of traversals. A post-order traversal is similar, but all left subtree elements are visited first, followed by all right subtree elements, and the root element is visited last. Finish the implementation of the printPostOrder method. For example, calling this method on the tree below would print the elements in the order: 1 0 3 2 5 6 4 9 8 7.

  2. A level-order traversal goes through the tree level by level, printing or visiting all elements on one level before going on to the next level below that. We can implement a level-order traversal using a queue to hold the elements remaining to be printed. Here is a sketch of the algorithm:

    create a new empty queue
    add the root node to the queue
    while the queue is not empty:
        remove the next node from the queue
        print the element stored in the node
        add the node's children (if any) to the queue
    

    For example, the elements in the above tree would be printed as: 7 4 8 2 6 9 0 3 5 1.

    In our code, each node in the tree is represented by a BinarySearchTree object, so we need to use a queue of type Queue<BinarySearchTree> to hold the nodes. In Java, a Queue is actually an interface, not a class, but we can use a LinkedList<BinarySearchTree> object to represent the queue, which implements the Queue interface methods add and remove. Here is the beginning of the printLevelOrder method. Complete its definition and test it on a few different random trees to make sure it works:

    public void printLevelOrder() {
        Queue<BinarySearchTree> queue = new LinkedList<BinarySearchTree>();
        queue.add(this);
        while (!queue.isEmpty()) {
           . . .
        }
    }
    
  3. Implement and test the following methods of the BinarySearchTree class:

  4. Implement the method findElement, which takes a String consisting of some combination of the uppercase letters R and L (for Right and Left) that specifies a pathway through the tree starting at the root, and retrieves the corresponding element. If no such element exists, the method returns null. For example, findElement("LLR") called on the above tree would return 3, findElement("") would return 7, and findElement("RLRR") would return null. Hint: think recursively!

EXTRA CREDIT PROBLEMS

You should finish the other problems first before working on these.

  1. A binary search tree can become severely unbalanced if new elements are added one by one in sorted (or nearly sorted) order. However, if we start with all of the elements in a sorted array, we can construct a balanced tree recursively by first building a balanced tree from the first half of the elements, then building another balanced tree from the second half of the elements, and then combining the two subtrees into a bigger tree with the middle element at the root. For example, if we start with the following sorted array (with the middle element highlighted):

    this process will build the balanced tree shown below:

    Using this idea, complete the implementation of the method buildBalancedTree, which takes an ArrayList elements of sorted elements, and two index positions first and last specifying a sub-region of the array, and builds a balanced tree from the elements in the sub-region. When the rebalance method of a tree is called, it uses getSortedElements and buildBalancedTree to build a new balanced tree from a sorted list of the tree elements.

  2. Complete the implementation of the remove method, which removes an element from a tree and returns a reference to the updated tree. Normally, after removing an element from the tree, remove should just return a reference to the tree itself. The only exception is if the tree becomes empty as a result of removing the element, in which case null should be returned, representing an empty tree. If the element to be removed is smaller than the root, we just recursively remove the element from the left subtree (if it exists). If the element to be removed is greater than the root, we just recursively remove the element from the right subtree (if it exists). However, in case the recursive call on the subtree returns null, we need to remember to update this.left or this.right to be null, since that subtree is now empty.

    On the other hand, if the element to be removed happens to be the root element, things are slightly more complicated. In that case, we replace the root element by the largest element from the left subtree. That is, we find the largest element in the left subtree, remove it from the left subtree, and then update the root to be that element. But what if there is no left subtree? In that case, we replace the root element by the smallest element from the right subtree instead. But what if there is no right subtree? In that case, the tree is a leaf, so we just return null, indicating that the tree is now empty. Here is an outline of the algorithm:

     if element to be removed is at the root:
         if tree has a left subtree:
             replace root with largest element from left subtree
             remove largest element from left subtree
         else if tree has a right subtree:
             replace root with smallest element from right subtree
             remove smallest element from right subtree
         else tree is a leaf:
             return empty tree (null)
     else if element to be removed < root and there is a left subtree:
         remove element from left subtree
     else if element to be removed > root and there is a right subtree:
         remove element from right subtree
    

Turning in Your Homework

Files to submit:

Submit the above file using the Homework Upload Site. Please include your name and the assignment number in a comment at the top of the file. You DO NOT need to submit any other files for this assignment.

If you have questions about anything, just ask!