19: Introducing Stacks and Queues

Announcements

Quiz in our next discussion (the Monday right after break). “I got back late” is not an acceptable excuse for missing the quiz.

There will be an assignment due the week after break, and the week after that. No work due during break, of course (I view it just like any other weekend; up to you if you want to to be working then or not).

Start thinking hard about whether you should enroll in 187 next semester. Are you hanging on by your fingernails in this class? Maybe have a fallback plan in case computer science is not for you.

Specialized linear ADTs

The “standard” linear ADTs (in Java) are the array and the (generic) List. Arrays are a simple type, with very fast random access but the limitation of fixed sizes. Lists are more flexible, and their underlying implementation is generally written in terms of (resizing) arrays or (sometimes) in terms of a linked list.

But as we’ve mentioned, there are other linear data structures that one might use; they are similar to lists but restrict themselves in various ways. We’re going to revisit them so you’re ready when you see them again (“for the first time”) in 187. We’ll start with behavior, then do implementations.

Stacks

Stacks are a last-in, first-out data structure. They are like lists, but instead of allowing for random access (via get, remove, add), they restrict a user to adding items on one end (the “top”) and removing from that same position. These operations are sometimes called push (add an item), pop (remove an item), and peek (look at but do not remove the top item).

Modern Java suggests we use the Deque interface, which is short for double-ended queue, and use the addFirst, removeFirst, and peekFirst methods. In either case, though, the behavior is the same, LIFO.

s.push("a");
s.push("b");
s.pop();
s.push("c");
s.push("d");
s.peek();

(top on right)

  • After the first operation, the stack contains [“a”]
  • After the second, the stack containes [“a”, “b”].
  • removes and returns “b”, then stack contains, [“a”]
  • [“a”, “c”]
  • [“a”, “c”, “d”]
  • peek returns “d”

In class exercise 1

s.push(1);
s.push(2);
s.push(3);
s.peek()
s.pop();
s.push(1);
s.pop();
s.pop();

What are the contents of the stack after this code executes?

Queues

Queues are a first-in, first-out data structure. Java has a Queue interface you can use, or you can (also) use Deque, as described in its documentation. In a Queue, we typically talk about add (always at one end) remove (always from the other), and sometimes peek (just like a stack, returns but does not remove the next element that remove would return).

q.add("a");
q.add("b");
q.remove();
q.add("c");
q.add("d");
q.peek();

(front on left, rear on right)

  • [“a”]
  • [“a”, “b”]
  • removes and returns “a”, queue contains [“b”]
  • [“b”, “c”]
  • [“b”, “c”, “d”]
  • returns “b”

In-class exercise 2

q.add(1);
q.add(2);
q.add(3);
q.peek()
q.remove();
q.add(1);
q.remove();
q.remove();

What are the contents of the queue after this code executes? (rear on right)

A side note: over/underflow

Stacks and queues can underflow. If you call pop or remove on an empty stack/queue, this will generate an exception.

Some stacks and queues are bounded, which means they have an explicit capacity. If you try to push or add to a stack/queue that is already at capacity, then you will overflow the structure and generate an exception.

Priority queues

A priority queue is like a queue, but it returns the next “smallest” (in Java) thing, rather than the first-in thing, when remove or peek is called.

It’s important to note that the exact order of the items stored in the priority queue is not visible to the user; you can only see the “next” / “top” item (that will be returned by peek or remove). Internally, priority queues are implemented as “heaps”, which are a tree-based structure similar to, but different from, the binary search trees we talked about briefly earlier this semester. Heaps allow for efficient (log n) insertion and removal of the smallest item.

How do we define “smallest”? The usual way, by either depending upon the “natural” ordering of the elements stored in the PriorityQueue<E> (that is, they must implement Comparable) or by passing in a Comparator when constructing the PriorityQueue.

Suppose then we do the following with a priority queue:

pq.add("b");
pq.add("a");
pq.remove();
pq.add("c");
pq.add("d");
pq.peek();

(Note: Fixing this to be consistent with regular queues; this is not what we did in lecture!)

  • [“b”]
  • [“a”, “b”]
  • removes and returns “a”, contents are [“b”]
  • [“b”, “c”]
  • [“b”, “c”, “d”] ; note we don’t know whether “c” or “d” comes first; all we know is “b” is up next to be removed
  • returns “b”

In class exercise 3

pq.add(3);
pq.add(2);
pq.add(1);
pq.peek()
pq.remove();
pq.add(1);
pq.remove();
pq.remove();

Implementing a stack

The stack, a last-in, first-out data structure. How might we go about building one? Today we’ll build two implementations of a stack in class (time permitting), based upon the ADT specified by a Java interface.

As we go through this, see how well you can follow along, and see if the in-class exercise seems easy or hard. This is the first of several “barometer” tasks you’ll be doing: if you find them easy (or at least, doable), then you should feel reasonable optimistic about 187 next semester. If you find them difficult or confusing, again, maybe have a fallback plan (or plan to study hard over the winter break, perhaps practicing on 190D assignments you had trouble with).

The interface

package stack;

public interface Stack<E> {
    void push(E e) throws StackOverflowException;
    E pop() throws StackUnderflowException;
    E peek() throws StackUnderflowException;
    boolean isEmpty();
    boolean isFull();
    int size();
}

This is a minimal interface; of course you can add more if you like. Note we include thrown exceptions, which is optional depending upon whether the exceptions are “checked” or “unchecked”; the former inherit from Exception and the latter from RuntimeException. See https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html for more details.

We’ll trivially define (empty) classes for each exception ourselves. (in Eclipse)

Array-based stack

Now let’s look at an array-based implementation of a stack. We’ll do a “bounded” stack, that is, a stack that has a fixed maximum capacity. (You can imagine an unbounded, growable array, like the one we talked about for lists earlier in the semester, but we’ll skip that for now.)

What other state, that is, instance variables, does the stack require, other than an array? A pointer into that array, that indicates where the top of the stack “lives”. You have some discretion here – it could index the current top of the stack, or it could index the spot where the next top will be. This is a stylistic choice, but I find pointing it to the current top (or -1, if the stack is empty) to be more natural. Up to you.

(on board)

package stack;

public class BoundedArrayStack<E> implements Stack<E> {
    private E[] array;
    private int top;    

    public BoundedArrayStack(int capacity) {
        top = -1;
        array = (E[]) new Object[capacity]; // references to this array must not escape this class
    }

    @Override
    public void push(E e) throws StackOverflowException {
        if (isFull()) {
            throw new StackOverflowException();
        }
        top += 1;
        array[top] = e;
    }

    @Override
    public E pop() throws StackUnderflowException {
        if (isEmpty()) {
            throw new StackUnderflowException();
        }
        E temp = array[top];
        array[top] = null;
        top--;
        return temp;
    }

    @Override
    public E peek() throws StackUnderflowException {
        if (isEmpty()) {
            throw new StackUnderflowException();
        }
        return array[top];
    }

    @Override
    public boolean isEmpty() {
        return top == -1;
    }

    @Override
    public int size() {
        return top + 1;
    }

    @Override
    public boolean isFull() {
        return top == array.length - 1;
    }

}

Note 1: You can’t instantiate generic, typesafe arrays in Java, for historical and technical reasons that are beyond the scope of the course. Note the workaround: array = (E[]) new Object[capacity]; works, but generates a warning. As long as no references to the array escape the enclosing object (that is, as long as we never return the E[] array to a caller) we’ll be OK.

Note 2: When we pop, we explicitly set the array cell to null. Why? (on board) we are removing the reference (from the stack) to the object so that it can be garbage collected later. This is a minor potential memory leak but it’s worth plugging.

Linked-list based stack

Recall there are two basic ways to agglomerate data in Java: you can use arrays, or you can use references. For this second implementation, let’s consider using linked lists. Again, as we showed earlier in the semester, we’ll need a simple Node structure to link together into the list.

package stack;

// note this is a super-discount Node class; a "real" one would probably have at
// least a constructor (that took an `E data` argument)
public class Node<E> {
    public Node<E> next;
    public E data;
}

And since we’re only interested in the “top” of the stack, it’s pretty easy to do.

package stack;

public class UnboundedLinkedStack<E> implements Stack<E> {
    private Node<E> head;
    private int size;

    @Override
    public void push(E e) throws StackOverflowException {
        Node<E> node = new Node<>();
        node.data = e;
        node.next = head;
        head = node;
        size++;
    }

    @Override
    public E pop() throws StackUnderflowException {
        if (isEmpty()) {
            throw new StackUnderflowException();
        }
        E temp = head.data;
        head = head.next;
        size--;
        return temp;
    }

    @Override
    public E peek() throws StackUnderflowException {
        if (isEmpty()) {
            throw new StackUnderflowException();
        }
        return head.data;
    }

    @Override
    public boolean isEmpty() {
        return head == null;
    }

    @Override
    public boolean isFull() {
        return false;
    }

    @Override
    public int size() {     
        return size;
    }

}