CS136, Lecture 26

  1. Binary Search Trees
    1. Splay Trees
      1. Implementation
    2. Iterators for binary tree.
      1. Preorder traversal
      2. Inorder traversal
      3. Post-order traversal
      4. Level-order traversal

Binary Search Trees

Started discussing implementation of binary search trees last time.

public class BinarySearchTree implements OrderedStructure
{
    protected BinaryTreeNode root; 
    protected int count;

    public BinarySearchTree()
    // post: constructs an empty binary search tree.
    {
        root = null;
        count = 0;
    }

    protected BinaryTreeNode locate(BinaryTreeNode subRoot,
                    Comparable value)
    // pre: subRootand value are non-null
    // post: returned: 1 - existing tree node with the desired value, or
    //                 2 - node to which value shd be added.

    protected BinaryTreeNode predecessor(BinaryTreeNode root)
    // pre: tree is not empty, root node has left child.
    // post: returns pointer to predecessor of root

    protected BinaryTreeNode successor(BinaryTreeNode root)
    // pre: tree is not empty, root node has right child.
    // post: returns pointer to successor of root

    public void add(Object val)
    // post: adds a value to the binary search tree.

    public boolean contains(Object val)
    // post: returns true iff val is found within the tree
    {
        if (root == null) return false;

        BinaryTreeNode possibleLocation = locate(root,(Comparable)val);
        return val.equals(possibleLocation.value());
    }

    public Object get(Object val)
    // post: returns object found in tree, or null
    {
        if (root == null) return null;

        BinaryTreeNode possibleLocation = locate(root,(Comparable)val);
        if (val.equals(possibleLocation.value()))
            return possibleLocation.value();
        else
            return null;
    }

    public Object remove(Object val) 
    // post: removes one instance of val, if found
    {
        // remove value from a binary search tree
        // no root, just quit
        Comparable cval = (Comparable)val;

        if (isEmpty()) return null;
      
        if (val.equals(root.value())) // delete root value
        {
            BinaryTreeNode newroot = removeTop(root);
            count--;
            Object result = root.value();
            root = newroot;
            return result;
        }
        else
        {
            BinaryTreeNode location = locate(root,cval);

            if (cval.equals(location.value())) {
                count--;
                BinaryTreeNode parent = location.parent();
                 if (parent.right() == location) {
                    parent.setRight(removeTop(location));
                } else {
                    parent.setLeft(removeTop(location));
                }
                 return location.value();
            }
        }
        return null;
    }

    protected BinaryTreeNode removeTop
                                                (BinaryTreeNode topNode)
    // pre: tree is not empty.
    // post: root of tree (topNode) is disconnected from tree 
    //       & new root is returned, new root has no parent.

    {
        // remove topmost BinaryTreeNode from binary search tree
        BinaryTreeNode left  = topNode.left();
        BinaryTreeNode right = topNode.right();
        // disconnect top node
        topNode.setLeft(null);
        topNode.setRight(null);
        
        // Case a, no left BinaryTreeNode
        //   easy: right subtree is new tree
        if (left == null)  return right; 
        
        // Case b, no right BinaryTreeNode
        //   easy: left subtree is new tree
        if (right == null)  return left; 
        
        // Case c, left node has no right subtree
        //   easy: make right subtree of left
        BinaryTreeNode predecessor = left.right();
        if (predecessor == null)
        {
            left.setRight(right);
            return left;
        }
        
        // General case, slide down left tree
        //   harder: successor of root becomes new root
        //           parent always points to parent of n
        BinaryTreeNode parent = left;
        while (predecessor.right() != null)
        {
            parent = predecessor;
            predecessor = predecessor.right();
        }
        parent.setRight(predecessor.left());
        predecessor.setLeft(left);
        predecessor.setRight(right);
        return predecessor;
    }

    ...
}

Complexity of add, get, contains, and remove all proportional to height of tree. If balanced then O(log n), owise O(n) in worst case.

Can we guarantee that methods have complexity O(log n)?

AVL trees (which keep a measure of the differences of heights of subtrees in each node) guarantee remain balanced and therefore ops fast.

Splay trees need not be balanced, but average performance guaranteed O(log n) (like vector additions!).

Splay Trees

Most techniques for keeping trees balanced involve rotations. The idea is that one side of a tree might be significantly longer than the other side and we might want to (efficiently) change the shape to shorten the longest paths.

Rotate right in order to move y to root. Moves everything in a up by one, while elts in c go down by one (and elts in b stay same level). Notice that rotation preserves ordering in binary search tree.

Similarly for left rotation.

Code from BinaryTreeNode:

protected void rotateRight()
// pre: this node has a left subtree
// post: rotates local portion of tree so left child is root
{
    BinaryTreeNode parent = parent();
    BinaryTreeNode newRoot = left();
    boolean wasChild = parent != null;
    boolean wasLeftChild = isLeftChild();

        // hook in right child of new root to left of old root
    setLeft(newRoot.right());

        // put this to right of new root below it
    newRoot.setRight(this);

        // attach newRoot to parent
    if (wasChild) {
        if (wasLeftChild) parent.setLeft(newRoot);
        else              parent.setRight(newRoot);
    }
}

Idea behind splay tree. Every time find, get, add, or remove an element x, move it to the root by a series of rotations.

Splay means to spread outwards

  1. if x is root, done.

  2. if x is left (or right) child of root, rotate it to the root

  3. if x is left child of p, which is left child of g, do right rotation about g and then about p to get x to grandparent position. (Similarly for double right grandchild). Continue splaying until at root.

  4. if x is right child of p, which is left child of g, then rotate left about p and then right about g. (Similarly for left child of right child of g) Continue splaying until at root.

Tree can get more unbalanced w/ splay operations, but ave depth of nodes on original path is (on average) cut in half. Therefore if repeatedly look for same elts, then will find much faster (happens in practice).

Implementation

Specialization of binary search tree - same interface, but when perform add, contains, or get, splay the elt. When remove an elt, splay its parent.

Skip actual code to do splay - follows rules given earlier, but a bit ugly - look at it on-line.

Example:

    public boolean contains(Object val)
    // post: returns true iff val is found within the tree
    {
        if (root == null) return false;

        BinaryTreeNode possibleLocation = locate(root,(Comparable)val);
        if (val.equals(possibleLocation.value())) {
            root = possibleLocation // node becomes root
            splay(root);
            return true;
        } else
            return false;
    }

Others are similar.

Iterators for binary tree.

Skipped earlier

Preorder traversal

Look at preorder traversal. Recall recursive version:

public void doPreorder()
{
    reset();    // move cursor to root
    if (root != null) doRecPreorder();
}

/*
    post:  Do preorder traversal of subtree pointed to by 
            cursor.  Return cursor to starting point when done.
*/
public void doRecPreorder()
{
    value().doValueOp();
    if (hasLeft())
    {
        moveLeft();
        doRecInorder();
        moveUp();
    }
    if (hasRight())
    {
        moveRight();
        doRecPreorder();
        moveUp();
    }
}
Only problem is can't take one step, and then stop and save where are so can come back later to do it. No problem with iterative since save stack.

Idea is top element on stack is element currently examining. Must traverse subtree headed by node on stack as well as all subtrees headed by all other nodes on stack.

All but nextElement() are pretty trivial:

class BinaryTreePreorderIterator implements Iterator {
    protected BinaryTreeNode root; // root of tree
    protected Stack s;              // helper stack

    public BinaryTreePreorderIterator(BinaryTreeNode root)
    // post: constructs an iterator to traverse in preorder
    {
        s = new StackList();
        this.root = root;
        reset();
    }   

    public void reset()
    // post: resets the iterator to retraverse
    {
        while (!s.isEmpty()) s.pop();
        // stack is empty.  Push on the current node.
        if (root != null) s.push(root);
    }

    public boolean hasMoreElements()
    // post: returns true iff iterator is not finished
    {
        return !s.isEmpty();
    }

    public Object value()
    // pre: hasMoreElements()
    // post: returns reference to current value
    {   
        return ((BinaryTreeNode)s.peek()).value();
    }

    public Object nextElement()
    // pre: hasMoreElements();
    // post: returns current value, increments iterator
    {
        BinaryTreeNode old = (BinaryTreeNode)s.pop();
        Object result = old.value();

        if (old.right() != null) s.push(old.right());
        if (old.left() != null) s.push(old.left());
        return result;
    }
}

This code for nextElement() is simpler than code in text and on-line.

Inorder traversal

The code for an in-order traversal is not much harder. Idea now is if node is on stack, have already traversed left subtree, but still need to do node and right subtree. Note first node to traverse is obtained by going all way down left branch. After do root of tree, go to right child, and then follw leftmost branch all way down.

class BinaryTreeInorderIterator implements Iterator {
    protected BinaryTreeNode root;  // root of tree
    protected Stack s;              // helper stack

    public BinaryTreeInorderIterator(BinaryTreeNode root)
    // post: constructs an iterator to traverse inorder
    {
            s = new StackList();
            this.root = root;
            reset();
    }   

    public void reset()
    // post: resets the iterator to retraverse
    {
            while (!s.isEmpty())  s.pop();
            // stack is empty.  Push on nodes from root to
            // leftmost descendent
            BinaryTreeNode current = root;
            while (current != null) {
                s.push(current);
                current = current.left();
            }
    }

    public boolean hasMoreElements()
    // post: returns true iff iterator is not finished
    {
            return !s.isEmpty();
    }

    public Object value()
    // pre: hasMoreElements()
    // post: returns reference to current value
    {   
            return ((BinaryTreeNode)s.peek()).value();
    }

    public Object nextElement()
    // pre: hasMoreElements();
    // post: returns current value, increments iterator
    {
            BinaryTreeNode old = (BinaryTreeNode)s.pop();
            Object result = old.value();
            // this node has no unconsidered left children.
            // if this node has a right child, 
            //  push the right child and its leftmost descendants:
            // now top elt of stack is next node to be visited.
            if (old.right() != null) {
                BinaryTreeNode current = old.right();
                do {
                    s.push(current);
                    current = current.left();
                } while (current != null);
            }
            return result;
    }
}


Post-order traversal

Postorder is hardest - skip that here

Level-order traversal

Level order easy with queue. When process element, put its children on queue!
    public Object nextElement()
    // pre: hasMoreElements();
    // post: returns current value, increments iterator
    {
        BinaryTreeNode current = (BinaryTreeNode)q.dequeue();
        Object result = current.value();
        if (current.left() != null) q.enqueue(current.left());
        if (current.right() != null) q.enqueue(current.right());
        return result;
    }

Note similarity to preorder traversal!