19: Introducing Stacks and Queues
Announcements
Next Monday is a holiday (no discussion). But next Wednesday is a UMass Monday, so discussion!
There will be a quiz in the discussion after Thankgiving Break (the Monday the 26th of November). “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();
- [“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. This is the first of several “barometer” tasks we’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 186 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? An index 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;
}
}