Return to the Lecture Notes Index

15-200 Lecture 21 (Monday, March 27, 2006)

Binary Search Trees

Today we actually started writing code for a Binary Search Tree class. First, we needed a Node class. These will be the building blocks of our tree. Since they are Binary Trees, each Node needs three references. It needs two Node references to each of its children, and a reference to the Comparable item stored in the Node. This object MUST be a Comparable, since a Binary Search Tree requires that the data stored in it has a complete ordering, which means we need a compareTo method. As you recall, a Binary Search Tree must maintain the property that for every Node, every value in its left subtree must be less than the parent's value, and every value in its right subtree must be greater than the parents value. Without a compareTo, we have no way of knowing where to insert a new value. Our Node class will also need the appropriate constructors, accessors and mutators.

class Node {

	private Node left;
	private Comparable item;
	private Node right;

	// Here's a basic constructor that just takes an item. Note that it sets
	// the left and right references to null. 

	public Node (Comparable item) {
		this.item = item;
		left = right = null;
	}

	// Another constructor that takes both an item and references to 
	// the children nodes.	

	public Node (Node left, Comparable item, Node right) {
		this.left = left;
		this.right = right;
		this.item = item;
	}

	// Accessor methods

	public Node getRight() {
		return right;
	}

	public Node getLeft() {
		return left;
	}

	public Comparable getItem() {
		return item;
	}

	// Mutator methods
	
	public void setLeft (Node left) {
		this.left = left;
	}

	public void setRight (Node right) {
		this.right = right;
	}
	
	public boolean equals (Object o) {
		Node n = (Node) o;
		if (this.right != n.right)
			return false;
		if(this.left != n.left)
			return false;
		if(!this.item.equals(n.item))
			return false;

		return true;
	}

	public String toString() {
		return "[" + left.item.toString() + "," +
				  item.toString() + "," +
			    right.item.toString() + "," +
			System.getProperty("line_separator");
	}
	
}

Note that there is no setItem method. This is because when we alter the tree by adding and removing items, we will do so by manipulating the Node references, not by swapping values in and out of Nodes. Trust us, its easier this way. So when we insert a new item into our tree, we will actually create a new Node just for that item. That Node will then be inserted into the tree in the correct place using the setLeft or setRight methods.

Now lets look at the actual BST class. Of course, it will need a constructor and a toString(). We will also need to store the root, and it will be very helpful to store the count as well. Otherwise, we'd need to actually traverse the tree each time we wanted to know how large it is. Today, we also wrote insert and find methods. On Friday, we will write the dreaded remove method.

class BST {

	private Node root;
	private int count;

	public BST() {
		root = null;
		count = 0;
	}

	public void insert (Comparable item) {
		
		// Code for insert method 

	}

	private void insert (Node root, Comparable item) {

		// Recursive helper method for insert

	}

	public Comparable find (Comparable item) {
		
		// Code for find method

	}

	private Comparable find (Node root, Comparable item) {
		
		// Recursive helper method for find

	}

	public String toString() {

		// toString code
	
	}

	private String toString (Node root, String soFar) {

		// Recursive helper method for toString

	}
}

Note that our three methods all have recursive helper methods. This is because the recursive method needs additional parameters that are not intuitive from the users point of view. If someone wants to use the insert method, they should only need to specify the item that they are inserting. However, the recursive method also needs an additional Node argument passed to it. So we have our standard find method just call the recursive version with the appropriate arguements.

Lets look at the insert method first.

public void insert(Comparable item) {
	if(root == null)
		root = new Node (item);
	else
		insert (root, item);

	count++;
}

public void insert (Node, root, Comparable item) {
	if( root.getItem().compareto (item) > 0) {
		// item is less, so it goes to the left
		if(root.getLeft() == null)
			root.setLeft(new Node(item));
		else
			insert(root.getLeft(),item);
	} else {
		// item is greater or equal, it goes to the right
		if(root.getRight() == null)
			root.setRight(new Node(item));
		else
			insert(root.getRight(),item);
	}
}

The first thing the insert(Comparable item) method does is to check if the current root is null. If it is, then we can just set the root to a new Node containing item. If the root is not null, it immediately calls its recursive counterpart, but adds the root Node as an additional arguement. Now, at any Node, we have two options. We compare the item to the value stored at our current Node using compareTo. Now, if its less, we know the item needs to go to the left. Otherwise, we know it needs to go to the right. Before we recursively call insert on the left or right children, we want to make sure that they are not null. If they are null, we just use the setLeft or setRight to add our new Node onto the tree. Otherwise, we recursievly call insert. When we return to the non-recursive insert, we must remember to increment the count.

The code for the find method is very similar.

public Comparable find (Comparable item) {
	return find(root, item);
}

public Comparable find (Node root, Comparable item) {
	if( root == null)
		return null;
	if(item.compareTo(root.getItem()) == 0) {
		return root.getItem();
	}
	if(item.compareTo(root.getItem()) < 0) {
		return find (root.getLeft(), item);
	}
	if(item.compareTo(root.getItem()) > 0) {
		return find (root.getRight(), item);
	}
}

Like the insert method, the non-recursive find just calls the recursive version with the root as its extra argument. Once in the recursive method, it is a very simple search strategy. If our current Node is null, that means that the item is not in the Tree. The ordering of the BST is such that there is only one place that the item could possibly be. If we run into a null instead, it can only mean that the item isn't there. So we return null. Otherwise, we check to see if our current Node contains what we're looking for. If it does, we return it immediately. Otherwise, we use compareTo to decide which subtree we should look in next. If our item is less than the item stored in the current Node, we need to check the left subtree. Otherwise, we'll search the right subtree. In either case, we just return the result of the recursive call.

Finally, we wrote a toString() method. There is no particular order that the toString has to return the items, but we chose to display them in order. This means we will want to do an inorder traversal of the tree. For that, we'll need another recursive helper.

public String toString() {
	return toString(root, "");
}

public String toString( Node root, String soFar) {
	if(root == null)
		return soFar;
	toString(root.getLeft(), soFar);
	soFar += root.getItem();
	toString(root.getRight(), soFar);
	return soFar;
}

In this case, our recursive helper takes two arguments, the Node it is currently at, and an accumulator string that will store our String as we go. Recall that for an inorder traversal, we first visit the left subtree, then the current Node, and finally the right subtree. In our case "visiting" the Node will consist of adding its item's toString onto our accumulator string. Of course when we run into a node that is null, we've run off the end of the Tree and don't want to add anything to our string.

Next time, we'll work on removing items from BSTs, which is much trickier.