Stacks & Queues
Table of Contents
Adapted from Stuart Reges and Sedgewick & Wayne:
- https://courses.cs.washington.edu/courses/cse143/20wi/notes/notes04.html
- https://www.cs.princeton.edu/courses/archive/fall20/cos126/lectures/CS.12.StacksQueues.pdf
- https://www.cs.princeton.edu/courses/archive/fall20/cos126/lectures/CS.12.StacksQueues.pdf
1 Reading
After watching the video and/or reading the notes below, explore Djikstra's Two-Stack Algorithm for evaluating arithmetic expressions
- https://www.youtube.com/watch?v=7H56Bmk1jZQ
- https://introcs.cs.princeton.edu/java/43stack/Evaluate.java.html
For a nice, concise overview of stack and queues, read https://introcs.cs.princeton.edu/java/43stack/.
2 Abstract Data Types (ADTs)
We have seen the difference between an interface and an implementation.
For example, we had a Shape
interface that declared a set of operations and then a Circle
class that implemented those operations for a particular type of shape (a circle).
When thinking about data structures, we often refer to the interface as the Abstract Data Type or ADT.
The abstract data type of a List declares operations such as add
, remove
, contains
and size
.
It is abstract because it only defines the external behavior of these operations—it specifies nothing about the internal implemenation.
The implementation could be accomplished using an array, a linked list, or some other structure.
In computer science, two of the most fundamental ADTs are called stacks and queues. It is useful to study stacks and queues as a way to understand a minimal kind of data structure. We'll find, for example, that they are less powerful than the list structures we have been looking at. But we often find ourselves wanting to think in terms of the simplest possible solution to a problem, as in, "You could solve that with a stack."
Like lists, stacks and queues store an ordered sequence of values. A minimal set of operations for such a structure would require at least:
- We need some way to put values into the structure (an adding operation)
- We need a way to take values out (a removing operation)
- We need a way to test whether there is anything left in the structure
These three operations are the bare bones that you'd need for such a structure and in their purest form, stacks and queues have just these three operations. I have put together a version of these that also includes a size method that lets you ask for the number of elements in the structure. Stacks and queues are similar in that they each store a sequence of values in a particular order. But stacks are what we call LIFO structures while queues are FIFO structures:
stacks queues L-ast F-irst I-n I-n F-irst F-irst O-ut O-ut
3 Applications
The analogy for stacks is to think of a cafeteria and how trays are stacked up. When you go to get a tray, you take the one on the top of the stack. You don't bother to try to get the one on the bottom, because you'd have to move a lot of trays to get to it. Similarly if someone brings clean trays to add to the stack, they are added on the top rather than on the bottom. The result is that stacks tend to reverse things. Each new value goes to the top of the stack, and when we take them back out, we draw from the top, so they come back out in reverse order.
The analogy for queues is to think about standing in line at the grocery store. As new people arrive, they are told to go to the back of the line. When the store is ready to help another customer, the person at the front of the line is helped. In fact, the British use the word "queue" the way we use the word "line" telling people to "queue up" or to "go to the back of the queue".
Applications for queues:
- First-come-first-served resource allocation.
- "First 100 customers get 20% off!"
- Asynchronous data transfer
- Computer systems use queues to manage various forms of input/output
- For example, when your program prompts the user for input (say 3 numbers), it probably processes the numbers one at a time (e.g.,
scanner.nextInt()
) - Where does the rest of the text input exist while the first number is being processed? In a queue!
- Dispensing requests on a shared resource.
- Certain number of rooms available in various dorms (the shared resource), fill them in some order (a queue)
- Simulations of the real world (e.g., oscillating sound waves, transportation, logistics)
Applications for stacks
- Basic mechanism in interpreters, compilers (used to manage memory and function calls)
- Take 208 and/or 251 to learn a lot more about this
- "Back" button in a web browser
- Visit a page.
- Click a link to another page.
- Click a link to another page.
- Click a link to another page.
- Click "back" button.
- Click "back" button.
- Click "back" button.
- Each use of "back" should return to the most recent previous page
- Last-in-first-out behavior!
- PostScript (Warnock-Geschke, 1980s): the language used to communicate how to print/display a document (primarily used by laser printers)
- Revolutionized publishing
- The PDF format is based on PostScript
4 APIs
In the case of a stack, the adding operation is called "push" and the removing operation is called "pop".
All operations occur at one end of the stack, at the top.
We push values onto the top and we pop them off the top.
There is also a method for testing whether the stack is empty and an operation for requesting the current size of the stack. So for a
Stack<E>
, the basic operations are:
public void push(E value); // add value to the stack public E pop(); // remove and return the value most recently pushed public boolean isEmpty(); // is the stack empty? public int size(); // number of objects on the stack
Notice that we are using Java generics to define the Stack
in terms of an unspecified element type E
.
That way we'll be able to have a Stack<String>
or Stack<Integer>
or a Stack
of any other kind of element type we are interested in.
For queues, we have a corresponding set of operations but they have different names. The operations for a Queue<E>
are:
public void add(E value); // add value to queue public E remove(); // remove and return the value least recently added public boolean isEmpty(); // is the queue empty? public int size(); // number of objects in the queue
The Java collections framework (the part of Java that provides data structures like ArrayList
) does the right thing in terms of Queue<E>
by making it an interface.
One implementation of this interface is LinkedList<E>
.
The stack version is much older and was not done as well.
In particular, Stack<E>
is a class, not an interface.
Even though we are using the standard Java stack and queue classes, we'll limit the operations we use with them to those listed above.
5 Example Usage
Consider this program that does some simple manipulations on a stack and queue:
import java.util.Stack; import java.util.Queue; import java.util.LinkedList; public class SimpleStackQueue { public static void main(String[] args) { String[] data = {"four", "score", "and", "seven", "years", "ago"}; Queue<String> q = new LinkedList<>(); Stack<String> s = new Stack<>(); for (String str : data) { q.add(str); s.push(str); } System.out.println("initial queue = " + q); while (!q.isEmpty()) { String str = q.remove(); System.out.println("removing " + str + ", now queue = " + q); } System.out.println(); System.out.println("initial stack = " + s); while (!s.isEmpty()) { String str = s.pop(); System.out.println("removing " + str + ", now stack = " + s); } } }
It produces the following output:
initial queue = [four, score, and, seven, years, ago] removing four, now queue = [score, and, seven, years, ago] removing score, now queue = [and, seven, years, ago] removing and, now queue = [seven, years, ago] removing seven, now queue = [years, ago] removing years, now queue = [ago] removing ago, now queue = [] initial stack = [four, score, and, seven, years, ago] removing ago, now stack = [four, score, and, seven, years] removing years, now stack = [four, score, and, seven] removing seven, now stack = [four, score, and] removing and, now stack = [four, score] removing score, now stack = [four] removing four, now stack = []
As we expected, the queue values came out in the same order as the array but the stack values came out in reverse order.
6 Fixed-capacity Stack
We might use an array to implement a stack of fixed size. Here's an implementation that specifically holds strings:
public class FixedStackOfStrings { private String[] items; // holds the items private int n; // number of items in stack public FixedStackOfStrings(int capacity) { items = new String[capacity]; } public boolean isEmpty() { return n == 0; } public boolean isFull() { return n == items.length; } public void push(String item) { items[n] = item; n++; } public String pop() { n--; String item = items[n]; return item; } public static void main(String[] args) { FixedStackOfStrings stack = new FixedStackOfStrings(5); stack.push("tasty"); stack.push("cakes"); stack.pop(); stack.push("salad"); } }
The main
method would result in this sequence of states for the fields items
and n
:
FixedStackOfStrings stack = new FixedStackOfStrings(5);
+------+------+------+------+------+ items | null | null | null | null | null | +------+------+------+------+------+ ^ | n (0)
stack.push("tasty");
+---------+------+------+------+------+ items | "tasty" | null | null | null | null | +---------+------+------+------+------+ ^ | n (1)
stack.push("cakes");
+---------+---------+------+------+------+ items | "tasty" | "cakes" | null | null | null | +---------+---------+------+------+------+ ^ | n (2)
stack.pop();
+---------+---------+------+------+------+ items | "tasty" | "cakes" | null | null | null | +---------+---------+------+------+------+ ^ | n (1)
stack.push("sandwiches");
+---------+---------+------+------+------+ items | "tasty" | "salad" | null | null | null | +---------+---------+------+------+------+ ^ | n (2)
Note how the field n
is both the number of elements in the stack
and the index of the first available spot in the array. Handy!
7 ArrayList Stack
We can make our array-based stack more flexible by having it resize
the internal array when it runs out of space.
This is just like the technique we used to implement ArrayList
:
create a new array double the size of the old one and copy over the
current elements of the array.
In fact, since we implemented an ArrayList
in a previous topic, let's just use that!
import java.util.ArrayList; public class ArrayListStack<Item> { private ArrayList<Item> stack; public ArrayListStack() { stack = new ArrayList<>(); } public int size() { return stack.size(); } public boolean isEmpty() { return stack.isEmpty(); } public void push(Item item) { stack.add(item); } public Item pop() { return stack.remove(size() - 1); } public Item peek() { return stack.get(size() - 1); } }
We use the end of the ArrayList
as the top of the stack because adding and removing from the end of an ArrayList
are constant time operations.
If you're curious about an array-based stack implementation that handles the resizing itself instead of using an ArrayList
, see ResizingArrayStack.java.
7.1 Visualization
Let's say we have a file tobe.txt
that contains the following text
to be or not to - be - - that - - - is
and we process this file using our ArrayListStack
via this main
method:
public static void main(String[] args) { ArrayListStack<String> stack = new ArrayListStack<String>(); In infile = new In("tobe.txt"); while (!infile.isEmpty()) { String item = infile.readString(); if (!item.equals("-")) { stack.push(item); } else if (!stack.isEmpty()) { System.out.print(stack.pop() + " "); } } System.out.println("(" + stack.size() + " left on stack)"); }
Every word gets pushed onto the stack, and every ="-"= causes a word to get popped off the stack. The result will look like this:
8 Linked-list Stack
One important property of a stack is that we only interact with one end of the structure, the top of the stack. This makes a linked list an excellent candidate for the internal stack data structure—it provides constant time operations on the head of the list, and we don't have to worry about wasted capacity or resizing like we do when using an array.
import java.util.LinkedList; public class LinkedListStack<Item> { private LinkedList<Item> stack; public LinkedListStack() { stack = new LinkedList<>(); } public int size() { return stack.size(); } public boolean isEmpty() { return stack.isEmpty(); } public void push(Item item) { stack.addFirst(item); } public Item pop() { return stack.removeFirst(); } public Item peek() { return stack.getFirst(); } }
Now, Java's LinkedList
class is a doubly-linked list.
For the constant-time operations at the head of the list that we need, a singly-linked list would be sufficient, and use somewhat less space.
Java doesn't provide a singly-linked list class, so we'd need to implement that ourselves:
public class SinglyLinkedStack<Item> { private int n; // size of the stack private Node first; // top of stack // helper linked list node class private class Node { private Item item; private Node next; } /** * Initializes an empty stack. */ public SinglyLinkedStack() { first = null; n = 0; } /** * Is this stack empty? * @return true if this stack is empty; false otherwise */ public boolean isEmpty() { return first == null; } /** * Returns the number of items in the stack. * @return the number of items in the stack */ public int size() { return n; } /** * Adds the item to this stack. * @param item the item to add */ public void push(Item item) { Node oldfirst = first; first = new Node(); first.item = item; first.next = oldfirst; n++; } /** * Removes and returns the item most recently added to this stack. * @return the item most recently added * @throws java.util.NoSuchElementException if this stack is empty */ public Item pop() { if (isEmpty()) { throw new NoSuchElementException(); } Item item = first.item; // save item to return first = first.next; // delete first node n--; return item; // return the saved item } /** * Returns (but does not remove) the item most recently added to this stack. * @return the item most recently added to this stack * @throws java.util.NoSuchElementException if this stack is empty */ public Item peek() { if (isEmpty()) { throw new NoSuchElementException(); } return first.item; } }
8.1 Visualization
As before we have our file tobe.txt
to be or not to - be - - that - - - is
and a main
method that uses our SinglyLinkedStack
to process it:
public static void main(String[] args) { SinglyLinkedStack<String> stack = new SinglyLinkedStack<String>(); In infile = new In("tobe.txt"); while (!infile.isEmpty()) { String item = infile.readString(); if (!item.equals("-")) { stack.push(item); } else if (!stack.isEmpty()) { System.out.print(stack.pop() + " "); } } System.out.println("(" + stack.size() + " left on stack)"); }
Every word gets pushed onto the stack, and every ="-"= causes a word to get popped off the stack. The result will look like this:
9 Linked-list Queue
Since the Queue ADT only provides operations to add to the end of the queue and remove from the beginning, a singly-linked list with a tail reference is an ideal data structure to use for implementation. We never remove from the end, so we never need the previous references a doubly-linked list provides.
public class SinglyLinkedQueue<Item> { private int n; // size of the queue private Node front; // front of the queue private Node end; // end of the queue // helper linked list node class private class Node { private Item item; private Node next; } /** * Initializes an empty stack. */ public SinglyLinkedQueue() { first = null; end = null; n = 0; } /** * Is this stack empty? * @return true if this stack is empty; false otherwise */ public boolean isEmpty() { return first == null; } /** * Returns the number of items in the stack. * @return the number of items in the stack */ public int size() { return n; } /** * Adds the item to this stack. * @param item the item to add */ public void add(Item item) { if (isEmpty()) { front = new Node(); front.item = item; end = front; } else { end.next = new Node(); end = end.next; end.item = item; } n++; } /** * Removes and returns the item most recently added to this stack. * @return the item most recently added * @throws java.util.NoSuchElementException if this stack is empty */ public Item remove() { if (isEmpty()) { throw new NoSuchElementException(); } Item item = first.item; // save item to return first = first.next; // delete first node n--; return item; // return the saved item } /** * Returns (but does not remove) the item most recently added to this stack. * @return the item most recently added to this stack * @throws java.util.NoSuchElementException if this stack is empty */ public Item peek() { if (isEmpty()) { throw new NoSuchElementException(); } return first.item; } }
9.1 Efficient Array-based Queue
We implemented a stack with both an ArrayList
and a LinkedList
, so why not an ArrayListQueue
?
The key reason is that a queue has two ends while a stack only has one.
As we've seen, an ArrayList
can efficiently (i.e., in constant time) add and remove from one side (the end of the array), but not the other.
This is why Java's LinkedList
implements the Queue
interface, but ArrayList
does not.
However, a clever solution to this problem exists.
Like with the FixedStackOfStrings
, we'll want to use an int
to keep track of the index of the end of the queue.
The trick is to add a second int
to keep track of the index of the beginning of the queue.
This is like the head and tail references we've used for linked lists.
Every time we remove an element from the front of the queue, we add one to our beginning index.
This effectively removes the element by putting it outside the portion of the array considered to be inside the queue.
The final ingredient is to wrap these indexes back around to the start of the array when they reach the end.
Here's a visual example. We start out with a queue q
of three numbers (3, 7, 5).
+---+---+---+---+---+ | 3 | 7 | 5 | 0 | 0 | +---+---+---+---+---+ ^ ^ | | front end (0) (3) size() is 3
We then call q.remove()
which removes 3
from the front of the queue by adjusting front
+---+---+---+---+---+ | 3 | 7 | 5 | 0 | 0 | +---+---+---+---+---+ ^ ^ | | front end (1) (3) size() is 2
Calling q.add(2)
inserts 2
at end
and increases end
by one, moving it to the right.
+---+---+---+---+---+ | 3 | 7 | 5 | 2 | 0 | +---+---+---+---+---+ ^ ^ | | front end (1) (4) size() is 3
Calling q.add(8)
inserts 8
at end
. Since increasing end
by one would move it past the end of our array, we wrap it back around to the start.
+---+---+---+---+---+ | 3 | 7 | 5 | 2 | 8 | +---+---+---+---+---+ ^ ^ | | end front (0) (1) size() is 4
Our queue always logically consists of the indexes between front
and end
, including the wrap around as if the last index in the array and first index were connected.
See ResizingArrayQueue.java for a complete, resizing implementation.
10 Double-ended Queue (Deque)
A related abstract data type to the Queue is the Double-Ended Queue (which is often shortened to Deque, pronouced like deck).
A deque can have elements added and removed from both ends (hence double-ended).
Java defines a Deque
interface which includes the operations
public void addFirst(E item); public void addLast(E item); public E getFirst(); public E getLast(); public E removeFirst(); public E removeLast();
With these operations, a deque can act as both a queue and a stack.
If these seem familiar from our definition of a LinkedList
, it will be no surprise to learn that Java's LinkedList
implements the Deque
interface.
Java also provides an array-based implementation with the ArrayDeque
class (in fact, Java's documentation suggests using an ArrayDeque
instead a Stack
if you want stack-like behavior).
11 Practice Problems1
- Which of the following statements about stacks and queues is true?
- Stacks and queues can store only integers as their data.
- A stack returns elements in the same order as they were added (first-in, first-out).
- A queue’s remove method removes and returns the element at the front of the queue.
- Stacks and queues are similar to lists, but less efficient.
- If you create a new empty stack and push the values 1, 2, and 3 in that order, and call pop on the stack once, what value will be returned?
- If you create a new empty queue and add the values 1, 2, and 3 in that order, and call remove on the queue once, what value will be returned?
- Stacks and queues do not have index-based methods such as get from ArrayList. How can you access elements in the middle of a stack or queue?
- Stacks and queues have less functionality than other similar collections like lists. Why are they still useful despite lacking functionality? What possible advantages are there of using a less powerful collection?
- What is the output of the following code?
Stack<String> s = new Stack<>(); Queue<String> q = new LinkedList<>(); s.push("how"); s.push("are"); s.push("you"); while (!s.isEmpty()) { q.add(s.pop()); } System.out.println(q);
- The following piece of code incorrectly attempts to compute the sum
of all positive values in a queue of integers. What is wrong with
the code, and how would you fix it?
int sum = 0; while (!q.isEmpty()) { if (q.remove() > 0) { sum += q.remove(); } }
- Write a piece of code that finds and prints the longest string in a stack of strings. For example, in the stack
[hello, hi, goodbye, howdy]
, the longest string is ="goodbye"=. When your code is done running, the stack should still contain the same contents as it had at the start. In other words, if you destroy the stack as you examine it, restore its state afterward. If you like, put your code into a method calledprintLongest
that accepts the stack as a parameter. - What data type would you choose to implement an "Undo" feature in a word processor?
- Suppose that you implemented
push
in the linked list implementation ofLinkedListStack
with the following code. What is the mistake?public void push(Object value) { Node second = first; Node first = new Node(); first.value = value; first.next = second; n++; }
- Describe how we might efficiently implement a Queue as a pair of Stacks, called a “stack pair.” (Hint: Think of one of the stacks as the head of the queue and the other as the tail.)
- Write a method called splitStack that accepts a stack of integers as a parameter and rearranges its elements so that all the negatives appear on the bottom of the stack and all the nonnegatives appear on the top. If after this method is called you were to pop numbers off the stack, you would first get all the nonnegative numbers and then get all the negative numbers. It does not matter what order the numbers appear in as long as all the negatives appear lower in the stack than all the nonnegatives. For example, if the stack stores [3, −5, 1, 2, −4], an acceptable result from your method would be [−5, −4, 3, 1, 2]. Use a single queue as auxiliary storage.
- Write a program
Parentheses.java
that reads a string of parentheses, square brackets, and curly braces from standard input and uses a stack to determine whether they are properly balanced. For example, your program should printtrue
for[()]{}{[()()]()}
andfalse
for[(])
Footnotes:
Solutions:
- Only 3 is true
- 3 will be returned
- 1 will be returned
- To access elements in the middle of a stack or queue, you must remove/pop out elements until you reach the one you're looking for. In many cases, it's important to save the removed elements somewhere so that you can put them back into the stack or queue when you are done.
- Stacks and queues are still useful despite their limited functionality because they are simple and easy to use, and because their operations are all efficient to execute. Many common situations are also naturally represented as a stack or queue.
- The code produces the output
[you, are, how]
- The problem with the code is that it calls the remove method twice on each element, which double-removes it and therefore skips elements. Another problem with the code is that it destroys the contents of the queue being examined. The following version of the code fixes both problems:
int sum = 0; Queue<Integer> backup = new LinkedList<Integer>(); while (!q.isEmpty()) { int n = q.remove(); if (n > 0) { sum += n; } backup.add(n); } while (!backup.isEmpty()) { q.add(backup.remove()); }
public static void printLongest(Stack<String> stack) { Stack<String> backup = new Stack<String>(); String longest = stack.pop(); backup.push(longest); while (!stack.isEmpty()) { String s = stack.pop(); if (s.length() > longest.length()) { longest = s; } backup.push(s); } while (!backup.isEmpty()) { // restore stack stack.push(backup.pop()); } System.out.println(longest); }
- A stack, as undo has last-in-first-out behavior.
- By redeclaring
first
(i.e., including a type:Node first
), you are create a new local variable named first, which is different from the instance variable named first. - Suppose that we have two stacks, head and tail. The bottom of head contains the front of the queue, while the bottom of tail contains the end of the queue. Adding to the queue takes time proportional to the size of tail (pop everything off, push new element on, push everything back on), while removing takes time proportional to the size of head (pop everything off, push everything but the last element back on). This is because for each addition/removal, we pop everything off the respective stack as part of the operation.
public void splitStack(Stack<Integer> s) { Queue<Integer> q = new LinkedList<>(); while (!s.isEmpty()) { q.add(s.pop()); } for (int i = 0; i < q.size(); i++) { int temp = q.remove(); if (temp < 0) { s.push(temp); } else { q.add(temp); } } while (!q.isEmpty()) { s.push(q.remove()); } }
- https://introcs.cs.princeton.edu/java/43stack/Parentheses.java.html