Rice University - Comp 212 - Intermediate Programming

Spring 2002

Lecture #25 - Binary Search Tree


1. Example

Consider the following binary tree of Integer objects.

-7
|_ -55
| |_ []
| |_ -16
|    |_ -20
|    | |_ []
|    | |_ []
|    |_ -9
|      |_ []
|      |_ []
|_ 0
  |_ -4
  | |_ []
  | |_ []
  |_ 23
    |_ []
    |_ []

Notice the following property:

Moreover, this property holds recursively for all subtrees.  It is called the binary search tree (BST) property.  We can take advantage of this property when looking up for a particular Integer object in the tree.  Instead of scanning the whole tree for the search target, we can compare the search target against the root element and narrow the search to the left subtree or the right subtree if necessary.  So in the worst possible case, the number of comparisons is proportional to the height of the binary tree.  This is a big win if the tree is "more of less" balanced.   This is because when the tree is balanced, that is the difference between the heights of the left and right subtrees is O(1),  its height is O(logN), where N is the number of elements in the tree.

A binary tree with  the BST property is called a binary search tree.  It can serve as an efficient way for storage/retrieval of data.  We are lead to the following question: how to create and maintain a binary search tree.  

2. Binary Search Tree Insertion

Suppose we start with an empty binary tree T and  a given set S of Comparable Objects.  Then T clearly has the BST property with respect the Comparable ordering of S.  The following algorithm (visitor on binary trees) will allow us to insert elements of S into T and at the same time maintain the BST property for T.

package brs.visitor;

import brs.*;

/**
* Inserts totally ordered objects into the host maintaining the host's binary
* search tree property.
* Duplication is not allowed: replaces old data object with the new one.
* Suppose we have a set of objects that can be compared for equality with
* "equal to" and "totally ordered" with an order relation called
* "less or equal to" .
* Define "less than" to mean "less or equal to" AND "not equal to".
* Let T be a BiTree structure that stores such totally ordered objects.
* Definition:
* T is said to satisfy the binary search tree property (BSTP) if
*   T is empty, or
*   when T is not empty, the left and right subtrees of T both satisfy BSTP, and
*   all elements in the left subtree of T are less than the root of T, and
*   the root of T is less than all elements in the right subtree of T.
* @author Dung X. Nguyen - Copyright 2002 - All rights reserved.
*/
public class BSTInserter implements IVisitor {

    // Code for Singleton pattern...

    /**
    * Returns the host tree where the input is inserted as the root.
    * @param host an empty binary tree, which obviously satisfies BSTP.
    * @param input Comparable new data to be inserted.
    * @return host (which is no longer empty).
    */
    public Object emptyCase(BiTree host, Object input) {
        host.insertRoot (input);
        return host;
    }

    /**
    * If input.compareTo(input, host.getRootDat) == 0 then the old root data is
    * replaced by input.
    * @param host non-empty and satisfies BSTP.
    * @param input Comparable new data to be inserted.
    * @return the tree where input is inserted as the root.
    */
    public Object nonEmptyCase(BiTree host, Object input) {
        Comparable root = (Comparable)host.getRootDat();
	Comparable inp = (Comparable)input;
        if (inp.compareTo(root) < 0) {
            return host.getLeftSubTree().execute(this, input);
        }
        if (inp.compareTo(root) == 0) {
            host.setRootDat(input);
            return host;
        }
        return host.getRightSubTree().execute(this, input);
    }
}

3. Binary Search Tree Lookup

Suppose we have a binary search tree based on a given Comparable ordering.  The following algorithm will allow us to lookup and retrieve a particular data object from the tree.


package brs.visitor;

import brs.*;

/**
 * Finds a data object in a binary host tree that satisfies the
 * Binary Search Tree Property with respect to a given Comparator.
 */
public class BSTFinder implements IVisitor {
    // Code for Singleton pattern...

    /**
    * Returns null.
    * @param host an empty binary (which obviously satisfies the
    * Binary Search Tree Property).
    * @param input not used
    */
    public Object emptyCase(BiTree host, Object input) {
        return null;
    }

    /**
    * Returns the tree whose root is equal to input via the ordering order,
    * otherwise returns null.
    * @param host non empty and satisfies BSTP.
    * @param input Comparable object to be looked up.
    * @return null or a BiTree object whose root is equal input (via order).
    */
    public Object nonEmptyCase(BiTree host, Object input) {
        Comparable root = (Comparable)host.getRootDat();
	Comparable inp = (Comparable)input;
        if (inp.compareTo(root)< 0) {
            return host.getLeftSubTree().execute(this, input);
        }
        if (inp.compareTo(root) == 0) {
            return host;
        }
        return host.getRightSubTree().execute(this, input);
    }
}

4. Binary Search Tree Deletion

The algorithm to remove a particular data object from a binary search tree is more involved.  When the element to be removed is not at a leaf node, we can replace it with the largest element of the left subtree (if it's not empty) or the smallest element of the right subtree (if it's not empty).  In preparation, we write two visitors: one to find the subtree containing the largest element at the root, the other to find the subtree containing the smallest element at the root.


package brs.visitor;

import brs.*;

/**
 * Returns the subtree of the host with the max value in the root if the tree
 * is not empty, otherwise returns the host itself.
 */
public class MaxTreeFinder implements IVisitor {
    public static final MaxTreeFinder Singleton = new MaxTreeFinder ();

    private MaxTreeFinder ()  {
    }

    /**
    * The host tree is empty: the tree containing the max is the host itself.
    * @param host satisfies the binary search tree property.
    * @param input not used.
    * @return host.
    */
    public Object emptyCase(BiTree host, Object input) {
        return host;
    }

    /**
    * Asks the right subtree of the host to find the max.
    * Uses anynonymous class as helper.
    * @param host satisfies the binary search tree property.
    * @param input a BiTree
    * @return subtree with maximum root.
    */
    public Object nonEmptyCase(BiTree host, Object input)  {
        return host.getRightSubTree().execute (new IVisitor () {
            public Object emptyCase (BiTree h, Object inp) {
               return inp;
            }

            public Object nonEmptyCase(BiTree h, Object inp) {
               return h.getRightSubTree().execute (this, h);
            }
        }, host);
    }
}

////////////////////////////////////////////////////////////////////

package brs.visitor;

import brs.*;

/**
 * Returns the subtree of the host with the min value in the root
 * if the tree is not empty, otherwise returns the host itself.
 */
public class MinTreeFinder implements IVisitor {
    public final static MinTreeFinder Singleton = new MinTreeFinder ();

    private MinTreeFinder() {
    }

    /**
    * The host tree is empty: the tree containing the min is the host itself.
    * @param host satisfies the binary search tree property.
    * @param input not used.
    * @return host
    */
    public Object emptyCase(BiTree host, Object input) {
        return host;
    }

    /**
    * Asks the left subtree of the host to help find the min.  Use anonymous class as helper.
    * @param host satisfies the binary search tree property.
    * @param input a 
    * @return subtree with minimum root.
    */
    public Object nonEmptyCase (BiTree host, Object input) {
        return host.getLeftSubTree().execute(new IVisitor () {
            public Object emptyCase(BiTree h, Object inp) {
               return inp;
            }

            public Object nonEmptyCase(BiTree h, Object inp) {
               return h.getLeftSubTree().execute (this, h);
            }
        }, host);
    }
}

We are now ready to write the deletion algorithm for a binary search tree.

package brs.visitor;

import brs.*;

/**
* Deletes the given input from the host tree.
* Invariant: host contains unique objects and satisfies the BST property.
   Post: host does not contains the input.
   Algorithm:
   Case host is empty:
      returns null because there is nothing to remove.
   Case host is not empty:
      if input < root
          ask the host's left subtree to delete the input;
      else if root < input
          ask the host's right subtree to delete the input
      else (at this point, input == root)
      if host is a leaf
          ask the host to remove its root;
      else if host's left subtree is empty (i.e. right subtree is NOT empty) {
         ask host to replace the root with the minimum value of its right subtree;
         ask host's right subtree to delete this minimum value;
      }
      else (at this point, the left subtree must NOT be empty)  {
         ask host to replace the root with the maximum value of its left subtree;
         ask host's left subtree to delete this maximum value;
      }
   NOTE:
   In the above algorithm, the case when host is a leaf can be implemented in
   many ways.  One way is to check for the emptiness of the subtrees.
   As you have been "indoctrinated", it is "impolite" do ask something for what
   it is. So we ask the subtrees to help do the deletion instead.
*/
public class BSTDeleter implements IVisitor {
    // Code for Singleton pattern...

    /**
    * Returns null.
    * @param host is empty and obviously satisfies the BST Property.
    * @param input not used
    * @return null
    */
    public Object emptyCase(BiTree host, Object input) {
        return null;
    }

    /**
    * if input < root
    *   ask the host's left subtree to delete the input;
    * else if root < input
    *   ask the host's right subtree to delete the input
    * else (at this point, input == root)
    *   pass the host to its left subtree and ask it to help to remove the host's root;
    * @param host non-empty and satifies BST property
    * @param input object to be deleted from the host.
    * @return null.
    */
    public Object nonEmptyCase(BiTree host, Object input) {
        Comparable root = (Comparable)host.getRootDat();
	Comparable inp = (Comparable)input;
        if (input.compareTo(root) < 0) {
            return host.getLeftSubTree().execute (this, input);
        }
        if (input.compareTo(root) == 0) {
            // asks the left subtree to help delete the root.
            return host.getLeftSubTree().execute(LeftDeleter.Singleton, host);
        }
        return host.getRightSubTree().execute (this, input);
    }
}

As shown in the nonEmptyCase above, when we find the element to be removed at the root of some tree, we ask its left subtree for help to remove the root, passing it the current host tree and the current deletion algorithm.  That's what the LeftDeleter algorithm is about.  We purposely write LeftDeleter as a separate named visitor because we want you to rewrite it as an anonymous inner class yourself.  Do it!  It is a good exercise for writing anonymous inner classes.


package brs.visitor;

import brs.*;

/**
* Removes the root of the host's parent. The host's parent and the calling
* deletion algorithm are passed to this IVisitor in the constructor.
*/
public class LeftDeleter implements IVisitor {
	// Singleton pattern code...
    /**
    * Asks the _hostParent's right subtree to help delete _hostParent's root.
    * @param host the left subtree of _hostParent.
    * @param input BiTree parent of host
    * @return null.
    */
    public Object emptyCase (BiTree host, Object input) {
        return ((BiTree)input).getRightSubTree().
                    execute(RightDelDontAsk.Singleton, input);
    // EXERCISE: Replace RightDelDontAsk with an anonymous inner class.
    }

    /**
    * Finds the maximum value in the host.
    * Asks the host's parent to set its root to this maximum value, and removes
    * this maximum value from the host.
    * @param host the left subtree of host's parent.
    * @param input BiTree parent of host
    * @return null.
    */
    public Object nonEmptyCase (BiTree host, Object input) {
        BiTree maxTree = (BiTree)host.execute(MaxTreeFinder.Singleton, null);
        Object max = maxTree.getRootDat();
        ((BiTree)input).setRootDat(max);
        maxTree.execute(BSTDeleter.Singleton, max);
        return null;
    }
}

In the emptyCase above, we asks the right subtree of the host's parent to help remove the root from the host's parent.


package brs.visitor;

import brs.*;

/**
* Removes the root of the host's parent.  Called by the (empty) left sibling to
* help delete the parent's root, so don't ask for the sibling to help!
*/
public class RightDelDontAsk implements IVisitor {
	// Singleton pattern code...
    /**
    * Asks the host's parent to remove its root.
    * @param host the right subtree of _hostParent.
    * @param input BiTree parent of host
    * @return
    */
    public Object emptyCase(BiTree host, Object input) {
        ((BiTree)input).remRoot ();
        return null;
    }

    /**
    * Finds the minimum value in the host.
    * Asks the _hostParent to set its root to this minimum value, and removes
    * this minimum value from the host.
    * @param host the right subtree of _hostParent.
    * @param input BiTree parent of host
    * @return
    */
    public Object nonEmptyCase(BiTree host, Object input) {
        BiTree minTree = (BiTree)host.execute (MinTreeFinder.Singleton, null);
        Object min = minTree.getRootDat();
        ((BiTree)input).setRootDat(min);
        minTree.execute (BSTDeleter, min);
        return null;
    }
}

We now have all the needed ingredient to implement an efficient IDictionary, using the BiTree framework together with the above visitors!