CMPU 102, Spring 2006 Lecture 6

Stacks

A stack is data structure supporting the following operations:

Analogy: cafeteria trays.  You can put a new tray on top, or take the top tray off, but you can't access any of the trays underneath.

When we draw a stack visually, it looks like a bucket with a stack of elements inside.  For example, here is the stack that would result by performing the operations

push "A"
push "B"
push "C"

on an empty stack:

"C" is the top element in this stack.

Stacks are useful in situations where earlier input must be balanced with later input.

Example stack algorithm: balancing parentheses and brackets.

Data structures (such as stacks [and queues]) correct
Data structures (such as stacks [and queues)] incorrect

Algorithm: when you see an opening delimiter ("(" or "["), push it on the stack.  When you see a closing delimiter (")" or "]"), pop an opening delimiter off the stack and make sure that it matches the closing delimiter.

If you see a closing delimiter when the stack is empty, that means you saw something like this:

Hello world)

If the stack is not empty at the end of the input sentence, then there was an opening delimiter with no matching close delimiter:

[Hello world

Try this algorithm on paper to convince yourself that it works.

Preconditions and postconditions

Operations (methods) on a data structure often have preconditions and postconditions.  A precondition is something that must be true in order for the operation to work correctly.  A postcondition is something that is guaranteed to be true after the operation is performed (as long as the preconditions were satisfied.)

Stacks have the following pre and post conditions:

Operation Precondition Postcondition
isEmpty none returns true if the stack is empty, false if not
getTop !isEmpty returns top element
push none element pushed is new top element
pop !isEmpty returns top element, stack size decreases by 1

In general, the caller of a method is responsible for making sure that preconditions are satisfied before the method is called, and the method itself (the callee) is responsible for making sure that the postconditions are satisfied when the method returns.

When a precondition of a method is violated, the right thing to do is to throw a RuntimeException (or a subclass).  Violations of preconditions and postconditions are bugs in the program, so a runtime exception makes sure that the bug is obvious when the program is executed.

Stacks in Java

A stack is a generic data structure.  We should allow it to contain any kind of data.  Also, we should permit many implementations of stacks, and code that uses a stack should work no matter how the stack is implemented.  E.g., ArrayStack and LinkedListStack should both be interchangeable.

Solution: a generic interface, with methods defining the stack operations, and a type parameter E defining the element type:

public interface Stack<E> {
    public void push(E element);
    public E pop();
    public E getTop();
    public boolean isEmpty();
}

Stack implemented with an Array

Arrays are often used to implement a stack.  We must reallocate the array when it becomes too small to hold all of the elements in the stack.

public class ArrayStack<E> implements Stack<E> {
    private Object[] storage;
    private int numElements;

    public ArrayStack() {
        storage = new Object[10];
        numElements = 0;
    }

    public void push(E element) {
        if (numElements >= storage.length) {
            grow();
        }
        storage[numElements] = element;
        numElements++;
    }

    public E pop() {
        E result = getTop();
        numElements--;
        return result;
    }

    public E getTop() {
        if (isEmpty())
            throw new IllegalStateException("stack is empty!");
        return (E) storage[numElements - 1];
    }

    public boolean isEmpty() {
        return numElements == 0;
    }

    private void grow() {
        Object[] bigger = new Object[storage.length * 2];
        for (int i = 0; i < storage.length; i++) {
            bigger[i] = storage[i];
        }
        storage = bigger;
    }
}

Note how the precondition is checked in getTop, and by calling getTop from pop, we also effectively check the precondition in that method as well.