CS136, Lecture 12

      1. Doubly-linked lists
  1. Design Issues
  2. Linear Structures
    1. Stacks
      1. Applications of stacks:
        1. Running mazes
        2. Evaluation of arithmetic expressions.

We will have weekly quizzes for at least the next two weeks: next Wednesday and following Monday. Occur from 11-11:10 am. Two purposes: preparation for exam and to ensure you are well-prepared for the lab. Generally will involve writing code similar to that of lab (and occasional analysis). Grades will be part of the "homework and other" category that occupies 5-10% of the grade.


Doubly-linked lists

class DoublyLinkedListElement {
    Object value;                       // value held in elt
    DoublyLinkedListElement next;       // successor elt
    DoublyLinkedListElement previous;   // predecessor elt

    DoublyLinkedListElement(Object v,
                DoublyLinkedListElement next,
                DoublyLinkedListElement previous)
    ...
}


public class DoublyLinkedList implements List {
    protected DoublyLinkedListElement head;
    protected DoublyLinkedListElement tail;
    protected int count;

    ...

    public Object peek()
    // pre: list is not empty
    // post: returns first value in list.
    {
        return head.value;
    }

    public Object tailPeek()
    // pre: list is not empty
    // post: returns last value in list.
    {
        return tail.value;
    }

    public boolean contains(Object value)
    // pre: value not null
    // post: returns true iff value is in the list
    {
        DoublyLinkedListElement finger = head;
        while ((finger != null) && (!finger.value.equals(value)))
            finger = finger.next;
        return finger != null;
    }

    public Object remove(Object value)
    // pre: value is not null.  List can be empty.
    // post: first element matching value is removed from list
    {
        DoublyLinkedListElement finger = head;
        while (finger != null && !finger.value.equals(value))
            finger = finger.next;
        if (finger != null)
        {
            // fix next field of previous element
            if (finger.previous != null)
                finger.previous.next = finger.next;
            else 
                head = finger.next;
            // fix previous field of following element
            if (finger.next != null)
                finger.next.previous = finger.previous;
            else
                tail = finger.previous;
            count--;        // fewer elements
            return finger.value;
        }                                   // Didn't find value
        return null;
    }

    public int size()
    // post: returns the number of elements in list
    {
        return count;
    }

    public boolean isEmpty()
    // post: returns true iff the list has no elements.
    {
        return size() == 0;
    }

    public void clear()
    // post: removes all the elements from the list
    {
        head = tail = null;
        count = 0;
    }
}
RemoveFromTail is now O(1), but tradeoff is now all addition and removal operations must set one extra pointer in element. Must also worry about changing head and tail of the list.

Having looked at both circularly-linked list and doubly-linked list, the next obvious variation to look at is the doubly-linked circularly-linked list. That will be part of your next homework assignment!

With singly-linked circular list we kept track of tail rather than the head of the list - why? With doubly-linked circular list we just keep track of the head since it is easy to find the tail.

You will find it helpful to add method rotateBack as well as the method rotate found in CircularList. Using rotate and rotateBack should allow you to only have to write one addition and one deletion routine for the head or tail and get the other by doing some sort of rotate and calling the other (similar to the way addToTail was written in CircularList).

Hints: Write DblyCircularList first, and write a simple program to test it. It should do things like add elements at the front and rear (printing the entire list after each operation), search for and remove an element, and then remove elements at the front and rear until the list is empty. Then add one more element to make sure that you left it in a consistent state. Only after you are certain it works should you write the CircularJosList which implements Circular using a DblyCircularList rather than the Vector used this week. You should write all code out in advance and convince yourself that it will work before getting near a keyboard!

Design Issues

Symmetry: Most data structures have a great deal of symmetry. This appears both in method declarations and in code itself.

Methods can be classified as constructors, destructors, and query routines.
Constructors add to a data structure, while destructors shrink it.
Query routines poke at a data structure to extract information w/out changing the state.

Constructors and Destructors usually come in pairs: for every way of adding something to a data structure, there is a corresponding way of removing something.

We've seen plenty of examples with Lists: addToHead, removeFromHead, etc.

There is also symmetry that we have the same operations from front and rear.

Moreover, once the data structure itself became more symmetric (e.g. doubly-linked lists), the operations became more symmetric (replace occurrences of head by tail and vice-versa).

Look for the symmetries and wonder when they don't appear.

Notice similar symmetries in trade-offs between time and space. Generally speeding up requires more memory, while saving memory will cost more space.

Simplicity: Simple code is more likely to be correct than complex code. Ignoring "{","}", my solution to this week's homework occupied 13 lines of code. Most of the solutions that were incorrect occupied 3 times as many lines. If the code is convoluted, it is probably wrong. Method bodies in object-oriented languages are distinguished by their simplicity and by the fact that most methods occupy less than 10 lines of code. Think it through carefully, strive for simplicity, and write it out before attempting to type it in!

Linear Structures

Lists allowing insertion and deletion from multiple spots. At least from head and tail and often from inside. Linear structures are more restricted, allowing only a single add and single remove method. More restrictions on a structure generally allows for more efficient implementation of operations.

public interface Linear extends Container
{  // get size, isEmpty, & clear from Container.

    public void add(Object value);
    // pre: value is non-null
    // post: the value is added to the collection,
    //       the replacement policy not specified.

    public Object remove();
    // pre: structure is not empty.
    // post: removes an object from container
}
Look at two particular highly restricted linear structures:

Stack: all additions and deletions at same end: LIFO (last-in, first-out)

Queue: all additions at one end, all deletions from the other: FIFO (first-in, first-out)

Stacks

Stacks can be described recursively: Here is the picture of a stack of integers:

All additions take place at the top of the stack and all deletions take place there as well.

Traditionally refer to addition as "Push" and removal as "Pop" in analogy with spring-loaded stack of trays. Here we'll use both names interchangeably:

public interface Stack extends Linear {
    public void push(Object item);
    // post: item is added to stack
    //       will be popped next if no further push

    public Object pop();
    // pre: stack is not empty
    // post: most recently pushed item is removed & returned

    public void add(Object item);
    // post: item is added to stack
    //       will be popped next if no further add

    public Object remove();
    // pre: stack is not empty
    // post: most recently added item is removed and returned

    public Object peek();
    // pre: stack is not empty
    // post: top value (next to be popped) is returned

    public boolean empty();
    // post: returns true if and only if the stack is empty

    public boolean isEmpty();
    // post: returns true if and only if the stack is empty
}

Applications of stacks:

a. Running mazes

Keep cells on path from start on a stack. Mark cells when visit them.

See Maze program on-line.

  public void runMaze() 
  {
    success = false;            // No solution yet
    
    current = start;            // Initialize current to start
    current.visit();
    
    path = new StackList(); // Create new path
    path.push(current);     // with start on it
    
    while (!path.empty() && !success)   
      {             // Search until success or run out of cells
                current = nextUnvisited(); // Get new cell to visit
                
                if (current != null) {     // If unvisited cell, 
                  current.visit();         // move to it &
                  path.push(current);      // push it onto path
                  success = current.equals(finish); // Done yet?
                } 
                else    // No neighbors to visit so back up
                {       // Pop off last cell visited
                  current = (Position)path.pop();   
                  if (!path.empty()) // If path non-empty take last 
                                     //  visited as new current
                    current = (Position)path.peek();
                }
      }
  }
Marking of cells and use of stack keeps from searching around in circles and yet not missing any possible paths.

b. Evaluation of arithmetic expressions.

Some machines have stack based machine language instructions.

E.g. PUSH, POP, ADD (pop off top two elements from stack, add them and push result back onto stack), SUB, MULT, DIV, etc.

Ex. Translate X * Y + Z * W to:

  PUSH X
  PUSH Y
  MULT
  PUSH Z
  PUSH W
  MULT
  ADD
Trace result if X = 2, Y = 7, Z = 3, W = 4.

How do you generate this code?

Write expression in postfix form: operator after operands

E.g. 2 + 3 -> 2 3 +

General algorithm to convert:

  1. Write expression fully parenthesized.

  2. Recursively move operator after operands, dropping parentheses when done.
E.g.

X * Y + Z * W -> (X * Y) + (Z * W) -> (X Y *) (Z W *) +

-> X Y * Z W * +

Note parentheses no longer needed. Corresponds to reverse Polish calculator.

Once in postfix, easy to generate code as follow:

Therefore above expression compiled as shown earlier: PUSH X, PUSH Y, MULT, ...

Straightforward to write a computer program using stacks to use these instructions (or even the original postfix expression) to calculate values of arithmetic expressions.

On a postfix calculator, "PUSH" key is usually labelled ENTER.

Also usually only first operand must be "ENTER"ed. Rest automatically "ENTER"ed after type and then hit operator key.

This will be your next homework assignment!

Interestingly, algorithm to convert expression to postfix form can either be done recursively (as above) or using a stack.

Notice all operands appear in same order as started - only operations move. Commands to transform above expression (working on it from left to right):

OUTPUT X
PUSH *
OUTPUT Y
POP and OUTPUT operator
PUSH +
OUTPUT Z
PUSH *
OUTPUT W
POP and OUTPUT operator
POP and OUTPUT operator
(Other rules - "(" is always pushed onto stack, ")" causes operators to be popped and output until pop off topmost "(" on stack. )

Big question: When do you push one operator on top of another and when do you pop off topmost operator before pushing on new one?

Answer given in terms of precedence of operators!

Bring answer to class tomorrow!