CS136, Lecture 23

    1. File operations and exceptions
    2. Tree Traversals
    3. Examples using trees
      1. Parse Trees:
    4. Array representations of trees
    5. Application: Heaps and Priority Queues

File operations and exceptions

(We'll go over this in lab, not class)

File operations can throw exceptions because not find file, hit end of file when reading, etc.! In Java, one must test for exceptions and provide code to execute when exceptions are raised.

Java file operations may raise exceptions of type IOException. Must surround code that may throw this exception by:

try {
        //code using files
    } catch (IOException exc) {
        //code to execute if exception raised
    }

In the above, exc is a parameter of type IOException. You can send it a toString message to get info on what went wrong in the block that follows it.

You can either put all of the code into one single try-catch block or put each command that may raise the exception in such a block. You will get syntax errors if you do not surround these file messages with a try-catch block.

Your program should not just ignore these exceptions. At a minimum it should print out a message and return to a consistent state.

Tree Traversals

The code for the iterators in the text is quite complex, because it simulates recursion with a stack. We could have approached cycling through nodes in a different way that does not involve iterators if we know exactly what we want to do to the value stored in each node - call it doValueOp();

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

/*
    post:  Do inorder traversal of subtree pointed to by 
            cursor.  Return cursor to starting point when done.
*/
public void doRecInorder()
{
    if (hasLeft())
    {
        moveLeft();
        doRecInorder();
        moveUp();
    }
    value().doValueOp();
    if (hasRight())
    {
        moveRight();
        doRecInorder();
        moveUp();
    }
}

It's very important to move the cursor back to where you started, otherwise the recursive solution will not work.

Examples using trees

Parse Trees:

The following are syntax diagrams for arithmetic expressions.

What do we know about first possible characters of Factor, Term, or Expression?

Can use to help catch errors.

Look at how 3 * 7 + 6 / 2 - (3 + 7) would be understood as a tree.

3*7 is a term because both 3 and 7 are factors, similarly 6/2 is a term.

(3+7) is a factor because 3 + 7 is is an expression.

Because it is a factor, it is also a term.

Therefore 3 * 7 + 6 / 2 - (3 + 7) is of the form term1 + term2 - term3, and hence is an expression!

Exercise: Write out the corresponding tree (remember operations of the same precedence are done from left to right.)

Once you have the tree, how do you evaluate it?

See Parser code on-line.

Two things to notice:

  1. Mutually recursive routines: expression, term, and factor in class Parser (using input from class Lexer).

  2. Each node in tree from class corresponding to its type! (I.e., AddNode for addition, TimesNode for multiplication, NumNode for leaves, etc.)
Note that it would now be easy to create an infix calculator using this code (as long as we add parentheses to the interface).

Array representations of trees

We can also represent a tree in an array:

The array, data[0..n-1], holds the values to be stored in the tree. It does not contain references to the left or right subtrees.

Instead the children of node i are stored in positions 2*i +1 and 2*i + 2, and therefore the parent of a node j, may be found at (j-1)/2

The following example shows how a binary tree would be stored. The notation under the tree is a "parenthesis" notation for a tree. A tree is represented as (Root Left Right) where Root is the value of the root of the tree and Left and Right are the representations of the left and right subtrees (in the same notation). Leaves are represented by just writing their value. When a node has only one subtree, the space for the other is filled with () to represent the absence of a subtree.

Ex. (U (O C (M () P) ) (R (E T () ) S) )

IndexRange: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

data[]: U O R C M E S - - - P T - - -

Save space for links, but it is possible that there is exponentially much wasted storage:

Storing a tree of height n requires an array of length 2sup4(n) - 1 (!), even if the tree only has O(n) elements. This makes this representation very expensive if you have a long, skinny tree. However it is very efficient for holding full or complete trees.

Application: Heaps and Priority Queues

Recall that a complete binary tree is one in which every level is full except possibly the bottom level and that level has all leaves in the leftmost positions. (Note that this is more restrictive than a balanced tree.)

Def: A Min-Heap H is a complete binary tree such that

1) H is empty or

2a) The root value is the smallest value in H and

2b) The left and right subtrees of H are also heaps.

This is equivalent to saying that H[i] <= H[2*i+1], H[2*i+2] for all approp values of i in the array representation of trees. Another way of looking at Min-Heap is that any path from a leaf to the root is in non-ascending order.

This turns out to be exactly what is needed to implement a priority queue.

A priority queue is a queue in which the elements with lowest priority values are removed before elements with higher priority.

public interface PriorityQueue {
    public Comparable peek();
    // pre: !isEmpty()
    // post: returns the minimum value in priority queue

    public Comparable remove();
    // pre: !isEmpty()
    // post: returns and removes minimum value from queue

    public void add(Comparable value);
    // pre: value is non-null comparable
    // post: value is added to priority queue

    public boolean isEmpty();
    // post: returns true iff no elements are in queue

    public int size();
    // post: returns number of elements within queue

    public void clear();
    // post: removes all elements from queue
}

One can implement a priority queue as a regular queue where either you work harder to insert or to remove an element (i.e. store in priority order, or search each time to remove lowest priority elements).

Unfortunately, in these cases either adding or deleting an element will be O(n). (Which one is O(n) depends on which of the two schemes is adopted!)

Can provide more efficient implementation with heap!

- remove element with lowest priority (at root of tree) and then remake heap.