1. Introduction to Java
2. Reference Types and Semantics
3. Method Specifications and Testing
4. Loop Invariants
5. Analyzing Complexity
6. Recursion
7. Sorting Algorithms
8. Classes and Encapsulation
9. Interfaces and Polymorphism
10. Inheritance
11. Additional Java Features
12. Collections and Generics
13. Linked Data
14. Iterating over Data Structures
15. Stacks and Queues
15. Stacks and Queues

15. Stacks and Queues

Over the past three lectures, we have focused on the List abstract data type. We wrote two implementations of this ADT using different data structures, a dynamic array and a linked chain, and we used tools from earlier in the course to analyze the complexity of their operations. We also saw how to use Iterators and the iteratee pattern to provide the clients with tools to more efficiently iterate over these collections.

Today, we’ll introduce two more ADTs, the Stack and the Queue. Both of these ADTs provide convenient ways to collect data over time and then retrieve elements in a specified order. For this reason, Stacks and Queues serve as building blocks for many other algorithms. Later in the course, we will use these ADTs to define iterators over trees and traversals over graphs. In the real-world, stacks and queues are ubiquitous in computational geometry (useful for image rendering and sensing), traffic and route planning (such as the organization and coordination of trains in rail yards), and expression parsing (in calculators and programming language compilers).

Stacks

A Stack is an ADT that models storing a collection of physical items in a pile. When we make a stack of books, we always add the next book on top of the existing stack. It would not make sense to add a book lower down in the stack, as doing this would require us to lift some of the other books and slot the new book in. For this reason, a stack is an example of an ordered collection. The positions of the books within the stack encode some useful information, namely the order that they were added; books that were added earlier sit below books that were added later.

When the books are in a stack, we can only see the cover of one book, the book at the top of the stack. Similarly, this is the only book that we can easily remove from the stack. If we tried to remove a book lower in the stack, we’d risk the stack toppling over. This is the defining characteristic of a stack: the only element that we can access or remove from a stack is the most recent element that was added.

Definition: Stack, LIFO

The Stack ADT supports adding elements and accessing and removing the most recent element that was added. For this reason, we say that a stack enforces the LIFO (an acronym for last in, first out) order condition.

This LIFO order condition constrains the operations that a stack can support. There is only one way that an element can be added, at the “top” of the stack. We support this through an operation push() (when we place the book on top of the stack, it pushes down on the other books). There is only one way to remove an element from the stack; we must remove the “top” element with the pop() operation.

previous

next

Remark:

We have already seen the term stack earlier in the course, when we introduced the runtime stack in our very first lecture. As a collection of call frames, the runtime stack realizes the semantics of the stack ADT. New call frames are push()ed on top of the stack, and the topmost frame corresponds to the method that is actively being executed. Call frames are removed (i.e., pop()ped) from the runtime stack when the code reaches a return statement.

There is only one way to access an element within the stack; we can look at (i.e., return) the “top” element through an operation peek(). Finally, it will be useful to know whether or not our stack contains any elements, which we can support with an isEmpty() method. We’ll use this method to guard loops whose body pop()s an element from a stack and does some work with it. In addition, by providing an isEmpty() method, we can add a precondition to the peek() and pop() methods that avoids the need for exceptions or sentinel values.

Stack.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * An ordered collection of elements of type `T` that follows the LIFO order condition.
 */
public interface Stack<T> {

  /**
   * Adds `elem` at the "top" of this stack. Requires that `elem != null`.
   */
  void push(T elem);

  /**
   * Removes and returns the "top" element of this stack. 
   * Requires `!this.isEmpty()`.
   */
  T pop();

  /**
   * Returns the "top" element of this stack without removing it. 
   * Requires `!this.isEmpty()`.
   */
  T peek();

  /**
   * Returns `true` if there are currently no elements stored in this stack, 
   * otherwise returns `false`.
   */
  boolean isEmpty();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * An ordered collection of elements of type `T` that follows the LIFO order condition.
 */
public interface Stack<T> {

  /**
   * Adds `elem` at the "top" of this stack. Requires that `elem != null`.
   */
  void push(T elem);

  /**
   * Removes and returns the "top" element of this stack. 
   * Requires `!this.isEmpty()`.
   */
  T pop();

  /**
   * Returns the "top" element of this stack without removing it. 
   * Requires `!this.isEmpty()`.
   */
  T peek();

  /**
   * Returns `true` if there are currently no elements stored in this stack, 
   * otherwise returns `false`.
   */
  boolean isEmpty();
}

Implementing a Stack

We can imagine representing the contents of a Stack with a linearly-ordered data structure such as a dynamic array or a linked chain, where the indices in the data structure represent the order in which the elements were push()ed onto the stack. In a Stack implementation leveraging a dynamic array, for example, we’d have larger indices represent later additions to the stack because adding/removing from the end of a dynamic array is a more efficient operation (amortized \(O(1)\) and \(O(1)\) worst-case complexity, respectively) than adding/removing from the beginning (both \(O(N)\) due to the required element shifting).

We could implement DynamicArrayStack class from scratch, representing the state in a similar manner to the DynamicArrayList and developing the code for its methods similarly as well. However, this would lead to a lot of repeated code, something that we try to avoid. Instead, we’d like a way to leverage the code that we have already written for DynamicArrayList within our DynamicArrayStack implementation. A first thought for how to accomplish this is through inheritance. If DynamicArrayStack extends DynamicArrayList, then it gains access to all of the list’s fields and methods. To complete its definition, we just need to supply definitions for the four Stack methods, which can be done by calling DynamicArrayList methods. Give this a try before looking at our implementation below.

DynamicArrayStack with inheritance

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * An implementation of the Stack ADT using a dynamic array. Developed via an 
 * inheritance relationship from the `DynamicArrayList` class.
 */
public class DynamicArrayStack<T> extends DynamicArrayList<T> implements Stack<T> {

  @Override
  public void push(T elem) {
    this.add(elem);
  }

  @Override
  public T pop() {
    return this.remove(this.size() - 1);
  }

  @Override
  public T peek() {
    return this.get(this.size() - 1);
  }

  @Override
  public boolean isEmpty() {
    return this.size() == 0;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * An implementation of the Stack ADT using a dynamic array. Developed via an 
 * inheritance relationship from the `DynamicArrayList` class.
 */
public class DynamicArrayStack<T> extends DynamicArrayList<T> implements Stack<T> {

  @Override
  public void push(T elem) {
    this.add(elem);
  }

  @Override
  public T pop() {
    return this.remove(this.size() - 1);
  }

  @Override
  public T peek() {
    return this.get(this.size() - 1);
  }

  @Override
  public boolean isEmpty() {
    return this.size() == 0;
  }
}

Let’s evaluate this approach. The four Stack methods are all defined correctly, and they leverage properties of dynamic arrays to achieve great performance; \(O(1)\) worst-case time complexity for pop(), remove(), and peek(), and amortized \(O(1)\) time complexity for push(). However, there is a concern with this approach. By establishing an inheritance relationship with DynamicArrayList, we introduce all of the public methods of DynamicArrayList to the client interface of the DynamicArrayStack class. A client can construct a DynamicArrayStack and then call methods such as insert() and delete() on it to alter the order of its elements. In doing this, the client can violate the LIFO order condition (in other words, invalidate an invariant of the DynamicArrayStack class), evidence of poor encapsulation of this code.

Remark:

If the client stores a reference to a DynamicArrayStack object in a variable with static type Stack, then the compile-time reference rule will prevent access to these other list methods. However, we do not want to rely on this as a mechanism for encapsulation. When we defined the DynamicArrayStack class, its specification documented that it would behave like a stack; it did not describe the accessibility of other list methods.

Remark:

Java actually made this mistake in the standard library. Java's Stack class extends the Vector class, an older variant of an ArrayList. Java discourages the use of the Stack class for this reason (but preserves it for backward compatibility).

We’d like an alternate way to relate a DynamicArrayStack class to the DynamicArrayList class that allows for code reuse but also ensures proper encapsulation. A composition relationship is suitable for these goals.

Composition

In a composition relationship, one class is instantiated as a field within another class.

Definition: Composition

A class S is in a composition relationship with a type T, if objects of type S construct an instance of type T to store as one of their fields. The T instance is entirely encapsulated within the S instance and not accessible to outside clients.

Whereas inheritance is used to model “is a” relationships (a CheckingAccount is an Account), composition is used to model “has a” relationships. We can define our DynamicArrayStack using a composition relationship with a DynamicArrayList: a DynamicArrayStack has a DynamicArrayList in which it stores its elements.

We’ll add a private DynamicArrayList<T> field list that is responsible for storing the stack’s elements, and we’ll initialize list to refer to a new DynamicArrayList in the DynamicArrayStack constructor. After this set-up, we can leverage the same logic as in our inheritance code, replacing instances of “this” with “list”.

DynamicArrayStack.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * An implementation of the Stack ADT using a dynamic array, developed via a 
 * composition relationship with DynamicArrayList.
 */
public class DynamicArrayStack<T> implements Stack<T> {
  /** Stores the elements of this stack. */
  private final DynamicArrayList<T> list;

  public DynamicArrayStack() {
    list = new DynamicArrayList<>(); // establish the composition relationship
  }

  @Override
  public void push(T elem) {
    list.add(elem);
  }

  @Override
  public T pop() {
    assert !isEmpty();
    return list.remove(list.size() - 1);
  }

  @Override
  public T peek() {
    assert !isEmpty();
    return list.get(list.size() - 1);
  }

  @Override
  public boolean isEmpty() {
    return list.size() == 0;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * An implementation of the Stack ADT using a dynamic array, developed via a 
 * composition relationship with DynamicArrayList.
 */
public class DynamicArrayStack<T> implements Stack<T> {
  /** Stores the elements of this stack. */
  private final DynamicArrayList<T> list;

  public DynamicArrayStack() {
    list = new DynamicArrayList<>(); // establish the composition relationship
  }

  @Override
  public void push(T elem) {
    list.add(elem);
  }

  @Override
  public T pop() {
    assert !isEmpty();
    return list.remove(list.size() - 1);
  }

  @Override
  public T peek() {
    assert !isEmpty();
    return list.get(list.size() - 1);
  }

  @Override
  public boolean isEmpty() {
    return list.size() == 0;
  }
}

By leveraging a composition relationship, we avoid the issues from above. Now, the only methods supported by DynamicArrayStack are push(), pop(), peek(), and isEmpty(). These methods are able to enforce the LIFO order condition. In addition, we maintain the same good performance from above. This demonstrates that composition relationships provide a good mechanism for restricting the client interface of an existing type.

LinkedStack

For an alternate implementation of the Stack interface, we can use a composition relationship with the SinglyLinkedList class. In which order does it make sense to store the stack’s elements within this linked list? Should the “top” of the stack be at the beginning (i.e., head) of the list or at the end (i.e., immediately preceding the tail)? Think about which option will provide better performance.

Which order is better?

The "top" of the stack should be at the head of the list. While insertion into a linked list is \(O(1)\) at both the beginning and the end (via Node pointers head and tail), removal is only \(O(1)\) at the beginning. Removal from the end of a singly-linked list is an \(O(N)\) operation since we must traverse the entire linked chain structure to reach this node; recall that tail references the "empty" Node after the last element in the chain. Therefore, putting the "top" of the stack at the end of the list would lead to a poor \(O(N)\) runtime for pop(). Having head the "top" of the stack allows us to implement all of the Stack operations with \(O(1)\) worst-case time complexity!

Using this insight, and using the DynamicArrayStack class as a model of a composition relationship, take some time to develop the LinkedStack class.

LinkedStack with composition

LinkedStack.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * An implementation of the Stack ADT using a linked chain, developed via a 
 * composition relationship with SinglyLinkedList.
 */
public class LinkedStack<T> implements Stack<T> {
  /** Stores the elements of this stack. */
  private final SinglyLinkedList<T> list;

  public LinkedStack() {
    list = new SinglyLinkedList<>(); // establish the composition relationship
  }

  @Override
  public void push(T elem) {
    list.insert(0, elem);
  }

  @Override
  public T pop() {
    assert !isEmpty();
    return list.remove(0);
  }

  @Override
  public T peek() {
    assert !isEmpty();
    return list.get(0);
  }

  @Override
  public boolean isEmpty() {
    return list.size() == 0;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * An implementation of the Stack ADT using a linked chain, developed via a 
 * composition relationship with SinglyLinkedList.
 */
public class LinkedStack<T> implements Stack<T> {
  /** Stores the elements of this stack. */
  private final SinglyLinkedList<T> list;

  public LinkedStack() {
    list = new SinglyLinkedList<>(); // establish the composition relationship
  }

  @Override
  public void push(T elem) {
    list.insert(0, elem);
  }

  @Override
  public T pop() {
    assert !isEmpty();
    return list.remove(0);
  }

  @Override
  public T peek() {
    assert !isEmpty();
    return list.get(0);
  }

  @Override
  public boolean isEmpty() {
    return list.size() == 0;
  }
}

Evaluating Arithmetic Expressions

Next, we’ll see an example of a computational task that can naturally be carried out with the help of two stacks, evaluating arithmetic expressions. To keep things simpler for this lecture, we’ll restrict our focus to only very simple expressions that consist of single-digit numerical arguments, addition, multiplication, and parentheses. This ensures that the value of the expression is always a non-negative integer. We say that an arithmetic expression is well-formed if it conforms to our usual mathematical conventions. In the case of our simple subset of expressions, we can capture this notion with the following rules.

These rules can be combined and applied an arbitrary number of times, allowing us to build up more complicated well-formed expressions such as “\(3*(1+(4*6))+7+2*(8*3)\)”. We refer to the two expressions \(e_1\) and \(e_2\) in the third rule as operands and the “\(+\)” or “\(*\)” as an operator.

Remark:

The above rules give a context-free grammar for the language of well-formed expressions. In other words, they describe a subset of Strings that are nicely structured. You'll talk more about these ideas if you take CS 3110 or a theory of computation course.

We’ll evaluate the expression by processing it one character at a time; our evaluate method has the form,

1
2
3
4
5
6
7
public static int evaluate(String expr) {
  // initialize local variables
  for (char c : expr.toCharArray()) { 
    // update the local variables to process c
  }
  // final computations to obtain the return value
}
1
2
3
4
5
6
7
public static int evaluate(String expr) {
  // initialize local variables
  for (char c : expr.toCharArray()) { 
    // update the local variables to process c
  }
  // final computations to obtain the return value
}

We can use an enhanced for-loop to iterate over the characters of the String (transformed to a char[] by the toCharArray() method) since arrays are Iterable. We’ll need to introduce local variables to keep track of the characters we’ve already seen so they can be incorporated into our final calculation. For example, when we process the simple expression,

\[ 3*4, \]

we will read the 3 first and need to remember, “we should apply an operation with 3 as its first operand.” Then, we’ll read the \(*\) and need to remember, “we’ll perform multiplication with whatever I was remembering as the first operand and whatever expression is coming next”. Only after we read the 4 will we have the information that we need to simplify the expression to the value \(3*4=12\).

There are times when we may need to keep track of more information. In the expression,

\[ 1 + 2 * (3 + 4 * (5 + 6)) \]

the parentheses prevent us from carrying out any simplifications until we read the 6. Before this, the second operand of any of the operators was always an incomplete expression. Thus, we’d need a way to keep track of all 5 (or, more generally, an arbitrary number of) other operands and operators before the 6 is read. In mathematic expressions, operands that are closer together interact (i.e., form subexpressions that can be evaluated) before operands that are farther apart. In our example, we know that we’ll make use of the 5 operand in our simplification before we can use the 4, 3, 2, or 1 operands. The value of the parenthesized sub-expression starting “(\(5 + \)” will be needed to evaluate the expressions involving any of the other numbers. Therefore, we really only need immediate access to the 5 operand, and will only use the other operands once the 5 has been dealt with. Similarly, we know that we will evaluate the rightmost addition before dealing with any of the earlier operators. This insight motivates the use of two stacks to keep track of operands and operators (as well as parentheses).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 public static int evaluate(String expr) {

  Stack<Integer> operands = new LinkedStack<>();
  Stack<Character> operators = new LinkedStack<>();

  for (char c : expr.toCharArray()) { 
    // update the local variables to process c
  }
  // final computations to obtain the return value
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 public static int evaluate(String expr) {

  Stack<Integer> operands = new LinkedStack<>();
  Stack<Character> operators = new LinkedStack<>();

  for (char c : expr.toCharArray()) { 
    // update the local variables to process c
  }
  // final computations to obtain the return value
}

As we read operands, we’ll push() them onto the operands stack, and as we read operators and opening parentheses, we’ll push them onto the operators stack. How will we know when we have enough information to start simplifying the expression? We can make the following three observations:

We’ll put these ideas into practice in the following animation, where we trace through the evaluation of the expression “\(3 * 4 + 6 * (2 + 7) * 5\)”.

previous

next

Using this example and the observations from above, try to complete the implementation of the evaluate() method. A basic implementation, which computes the correct result given a well-formed expression of the type described above, is included below.

basic evaluate() implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
 * Evaluates the given well-formed mathematical expression `expr` and returns 
 * its value. For this basic version of the method, the operands must be 
 * single-digit integers and the only supported operations are addition (+) 
 * and multiplication (*). The allowable characters in the expression are, 
 * therefore, {0,1,2,3,4,5,6,7,8,9,+,*,(,)}.
 */
public static int evaluate(String expr) {
  Stack<Integer> operands = new LinkedStack<>();
  Stack<Character> operators = new LinkedStack<>(); // invariant: contains only '(', '+', and '*'

  for (char c : expr.toCharArray()) {
    if (c == '(') {
      operators.push('(');
    } else if (c == '+') {
      while (!operators.isEmpty() && 
             (operators.peek() == '*' || operators.peek() == '+')) {
        oneStepSimplify(operands, operators);
      }
      operators.push('+');
    } else if (c == '*') {
      while (!operators.isEmpty() && operators.peek() == '*') {
        oneStepSimplify(operands, operators);
      }
      operators.push('*');
    } else if (c == ')') {
      while (operators.peek() != '(') {
        oneStepSimplify(operands, operators);
      }
    } else { // c is a digit
      operands.push(c - '0'); // convert c to an int and auto-box
    }
  }
  while (!operators.isEmpty()) {
      oneStepSimplify(operands, operators);
  }
  return operands.pop();
}

/**
 * Helper method that partially simplifies the expression by `pop()`ping one 
 * operator from the `operators` stack, `pop()`ping its two operands from the
 * `operands` stack, evaluating the operator, and then `push()`ing its result 
 * onto the `operands` stack. Requires that `opererators.peek()` is '+' or '*' 
 * and `operands` includes at least two elements.
 */
private static void oneStepSimplify(Stack<Integer> operands, Stack<Character> operators) {
    char op = operators.pop();
    assert op == '+' || op == '*';

    int o2 = operands.pop(); // second operand is higher on stack
    int o1 = operands.pop();
    operands.push(op == '+' ? o1 + o2 : o1 * o2);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
 * Evaluates the given well-formed mathematical expression `expr` and returns 
 * its value. For this basic version of the method, the operands must be 
 * single-digit integers and the only supported operations are addition (+) 
 * and multiplication (*). The allowable characters in the expression are, 
 * therefore, {0,1,2,3,4,5,6,7,8,9,+,*,(,)}.
 */
public static int evaluate(String expr) {
  Stack<Integer> operands = new LinkedStack<>();
  Stack<Character> operators = new LinkedStack<>(); // invariant: contains only '(', '+', and '*'

  for (char c : expr.toCharArray()) {
    if (c == '(') {
      operators.push('(');
    } else if (c == '+') {
      while (!operators.isEmpty() && 
             (operators.peek() == '*' || operators.peek() == '+')) {
        oneStepSimplify(operands, operators);
      }
      operators.push('+');
    } else if (c == '*') {
      while (!operators.isEmpty() && operators.peek() == '*') {
        oneStepSimplify(operands, operators);
      }
      operators.push('*');
    } else if (c == ')') {
      while (operators.peek() != '(') {
        oneStepSimplify(operands, operators);
      }
    } else { // c is a digit
      operands.push(c - '0'); // convert c to an int and auto-box
    }
  }
  while (!operators.isEmpty()) {
      oneStepSimplify(operands, operators);
  }
  return operands.pop();
}

/**
 * Helper method that partially simplifies the expression by `pop()`ping one 
 * operator from the `operators` stack, `pop()`ping its two operands from the
 * `operands` stack, evaluating the operator, and then `push()`ing its result 
 * onto the `operands` stack. Requires that `opererators.peek()` is '+' or '*' 
 * and `operands` includes at least two elements.
 */
private static void oneStepSimplify(Stack<Integer> operands, Stack<Character> operators) {
    char op = operators.pop();
    assert op == '+' || op == '*';

    int o2 = operands.pop(); // second operand is higher on stack
    int o1 = operands.pop();
    operands.push(op == '+' ? o1 + o2 : o1 * o2);
}

A more robust version, which uses assert statements to detect and alert the client about malformed expression is provided in the lecture release code and includes a basic main() method that uses the evaluate() method in a simple calculator program, is included with the lecture release code. Exercise 15.8 outlines many extensions that can be made to enhance the set of expressions that can be evaluated.

Queues

A related ADT to the Stack is the Queue, which models a collection of people waiting in a line (i.e., queueing). When a new (reasonable) person wishes to join the queue, they do so at the “back” of the line. When a representative is ready to accept a person out of the queue, this person is removed from the “front” of the line. As a person stands waiting in the queue, they shift forward every time someone is serviced, making their way toward the “front” of the queue. Queues are another example of an ordered data structure whose order models the insertion time of the elements. Differing from stacks, queues adopt a FIFO (first in, first out) order condition.

Definition: Stack, FIFO

The Queue ADT supports adding elements and accessing and removing the first remaining element that had been added. For this reason, we say that a queue enforces the FIFO (an acronym for first in, first out) order condition.

We call the addition operation to a queue an enqueue() and the removal operation a dequeue(). A Queue also supports a peek() (which accesses the “front” element) and an isEmpty() method, just like a Stack.

previous

next

Queue.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * An ordered collection of elements of type `T` that follows the FIFO order condition.
 */
public interface Queue<T> {

  /**
   * Adds `elem` at the "back" of this queue. Requires that `elem != null`.
   */
  void enqueue(T elem);

  /**
   * Removes and returns the "front" element of this queue. 
   * Requires `!this.isEmpty()`.
   */
  T dequeue();

  /**
   * Returns the "front" element of this queue without removing it. 
   * Requires `!this.isEmpty()`.
   */
  T peek();

  /**
   * Returns `true` if there are currently no elements stored in this queue, 
   * otherwise returns `false`.
   */
  boolean isEmpty();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * An ordered collection of elements of type `T` that follows the FIFO order condition.
 */
public interface Queue<T> {

  /**
   * Adds `elem` at the "back" of this queue. Requires that `elem != null`.
   */
  void enqueue(T elem);

  /**
   * Removes and returns the "front" element of this queue. 
   * Requires `!this.isEmpty()`.
   */
  T dequeue();

  /**
   * Returns the "front" element of this queue without removing it. 
   * Requires `!this.isEmpty()`.
   */
  T peek();

  /**
   * Returns `true` if there are currently no elements stored in this queue, 
   * otherwise returns `false`.
   */
  boolean isEmpty();
}
Remark:

The Java library models both stacks and queues using a single interface, the Deque (short for "double-ended queue"). The nomenclature for the methods varies slightly from the terminology that we have used in this lecture (e.g., addLast() instead of enqueue() and getFirst() instead of dequeue()). Both the LinkedList and the ArrayDeque (which leverages a dynamic array) classes implement the Deque interface and provide the same runtime guarantees as our implementations.

Implementing a Queue

Just as we did for Stacks, we can implement Queues by leveraging our List implementations via composition relationships. First, let’s consider a LinkedQueue implementation. Similar to before, we’ll use a use a composition relationship with the SinglyLinkedList class, and we must consider in which order it makes sense to store the queue’s elements. Should the “front” of the queue be at the beginning (i.e., head) of the list or at the end (i.e., immediately preceding the tail)? Think about which option will provide better performance.

Which order is better?

The "front" of the queue should be at the head of the list. Again, we wish to avoid the \(O(N)\) removal from the end of a singly-linked list, which we can do by having the queue's insertions at its tail and its removals at its head. By making this choice, all of the Queue operations with \(O(1)\) worst-case time complexity!

Take some time to develop the LinkedQueue class using this insight.

LinkedQueue with composition

LinkedQueue.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * An implementation of the Queue ADT using a linked chain, developed via a 
 * composition relationship with SinglyLinkedList.
 */
public class LinkedQueue<T> implements Queue<T> {

  /** Stores the elements of this queue. */
  private final SinglyLinkedList<T> list;

  public LinkedQueue() {
    list = new SinglyLinkedList<>(); // establish the composition relationship
  }

  @Override
  public void enqueue(T elem) {
    list.add(elem);
  }

  @Override
  public T dequeue() {
    assert !isEmpty();
    return list.remove(0);
  }

  @Override
  public T peek() {
    assert !isEmpty();
    return list.get(0);
  }

  @Override
  public boolean isEmpty() {
    return list.size() == 0;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * An implementation of the Queue ADT using a linked chain, developed via a 
 * composition relationship with SinglyLinkedList.
 */
public class LinkedQueue<T> implements Queue<T> {

  /** Stores the elements of this queue. */
  private final SinglyLinkedList<T> list;

  public LinkedQueue() {
    list = new SinglyLinkedList<>(); // establish the composition relationship
  }

  @Override
  public void enqueue(T elem) {
    list.add(elem);
  }

  @Override
  public T dequeue() {
    assert !isEmpty();
    return list.remove(0);
  }

  @Override
  public T peek() {
    assert !isEmpty();
    return list.get(0);
  }

  @Override
  public boolean isEmpty() {
    return list.size() == 0;
  }
}

A Queue with a DynamicArray

Implementing an efficient Queue using dynamic array presents a greater challenge. We will not be able to do this through a composition relationship with a DynamicArrayList. In a Queue both “sides” of the array will be used since the elements are enqueue()d on one side and dequeue()d on the other. However, both inserting and removing elements at the start of a DynamicArrayList are \(O(N)\) operations since both require that all of the later elements are shifted (either one index right to make space for the new first element or one index left to close the gap left by the removed first element). The requirement that the elements “pack” to the left of the DynamicArray leads to this inefficiency. We’ll need to remove this requirement to achieve an \(O(1)\) (amortized) runtime for the queue operations. We can do this in a memory efficient way by modeling the queue as a dynamic circular buffer that adjusts its front index after removals rather than shifting all of the remaining elements.

Definition: Circular Buffer

A circular buffer is a data structure that stores its elements at contiguous indices with the added condition that we view the last index as the immediate predecessor of the first index.

In this way, the indices “wrap around” to form a circular arrangement of the data. We can visualize the “wrapping” of a circular buffer of length 8 as follows:

To represent the circular buffer, we can use the indices of both the “front” element (front) and the next unoccupied cell to fill (back).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * An implementation of the Queue ADT using a dynamic array to model a circular buffer.
 */
public class DynamicArrayQueue<T> implements Queue<T> {

  /**
   * The backing storage of this queue. `buffer[front..back) != null` and 
   * `buffer[back..front) == null`, where these interval is circular, so will 
   * loop from index `length-1` back to index `0` when appropriate. Must 
   * dynamically resize to ensure that at least one index (`back`) is `null`.
   */
  private T[] buffer;

  /**
   * The index of the next element to be `dequeue()`d; equal to `back` if the 
   * queue is empty.
   */
  private int front;

  /**
   * The index where the next element will be `enqueue()`d.
   */
  private int back;

  // ... constructor and Queue methods
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * An implementation of the Queue ADT using a dynamic array to model a circular buffer.
 */
public class DynamicArrayQueue<T> implements Queue<T> {

  /**
   * The backing storage of this queue. `buffer[front..back) != null` and 
   * `buffer[back..front) == null`, where these interval is circular, so will 
   * loop from index `length-1` back to index `0` when appropriate. Must 
   * dynamically resize to ensure that at least one index (`back`) is `null`.
   */
  private T[] buffer;

  /**
   * The index of the next element to be `dequeue()`d; equal to `back` if the 
   * queue is empty.
   */
  private int front;

  /**
   * The index where the next element will be `enqueue()`d.
   */
  private int back;

  // ... constructor and Queue methods
}

We leave it as an exercise (Exercise 15.3) to complete this DynamicArrayQueue implementation, using the same capacity-doubling approach as the DynamicArrayList to ensure an amortize \(O(1)\) time-complexity for push(). The following animation illustrates the expected behavior of the DynamicArrayQueue.

previous

next

While the LinkedQueue implementation has a better worst-case runtime performance, queues that leverage circular buffers (sometimes with a bounded capacity and no resizing) are often preferable in memory constrained applications such as embedded systems.

Main Takeaways:

  • Stacks and Queues are two additional ADTs (beyond Lists) that use an ordered, linear arrangement of their data. In each of these ADTs, only one of its elements can be viewed and/or removed at any particular time.
  • Stacks adhere to the LIFO order condition, so the last element that was push()ed onto the stack is the only one that can be peek()ed or pop()ped. Queues adhere to the FIFO order condition, so the first element that was enqueue()d is the only one that can be peek()ed or dequeue()d.
  • We can implement stacks and queues via a composition relationship with a DynamicArrayList or a SinglyLinkedList. Composition relationships, where a class constructs and manages an instance of another type, are a good mechanism to restrict the client interface of another type.
  • The operations on stacks and queues implemented using linked chains all have \(O(1)\) worst-case time complexity. The operations on stacks and queues implemented using dynamic arrays have amortized \(O(1)\) insertions and \(O(1)\) worst-case time complexities for their other methods.
  • Stacks (in particular) and queues are useful for many tasks ranging from data structure traversal to expression parsing.

Exercises

Exercise 15.1: Check Your Understanding
(a)

You are checking out books from a library with a rather strange return policy: you may check out any number of books as you like at a time, but you must return them in the opposite order that you checked them out.

Which ADT should you use to keep track of which books you have checked out so that it is easy to know which one would need to be returned first?
Check Answer
(b)

Consider the following operations done on a Stack.

1
2
3
4
5
6
7
Stack<Integer> stack = new LinkedStack<>();
for (int i = 0; i < 10; i++) {
  stack.push(i);
  if (i % 2 == 0) {
    stack.pop();
  }
}
1
2
3
4
5
6
7
Stack<Integer> stack = new LinkedStack<>();
for (int i = 0; i < 10; i++) {
  stack.push(i);
  if (i % 2 == 0) {
    stack.pop();
  }
}

What does stack.peek() return?

9

(c)

Suppose we alter the code in the last subproblem to use a Queue<Integer> queue instead of stack, replacing all invocations of push() with enqueue() and pop() with dequeue().

What does queue.peek() return?

5

(d)
The integers in the range \([1..5]\) are push()ed onto a Stack in increasing order. While push()ing the inputs onto the stack, 5 pop() operations were interspersed arbitrarily (while satisfying the precondition). Determine which of the following pop() sequence(s) is/are valid.
Check Answer
(e)

Now suppose the same integers \([1..5]\) are enqueue()d in increasing order into a Queue. 5 dequeue() operations are interspersed arbitrarily (while satisfying the precondition) during the enqueue()s.

List all possible dequeue() sequeunces.

There is only 1 possible sequence: \(1, 2, 3, 4, 5\).

Exercise 15.2: Stack Tracing
For each of the following, draw out the state of the stack as you trace through the evaluation of the code. Suppose that for each subproblem, a Stack<Integer> stack is in scope.
(a)
1
2
3
4
5
6
7
8
stack.push(3)
stack.push(1)
stack.pop()
stack.push(4)
stack.push(1)
stack.push(5)
stack.pop()
stack.pop()
1
2
3
4
5
6
7
8
stack.push(3)
stack.push(1)
stack.pop()
stack.push(4)
stack.push(1)
stack.push(5)
stack.pop()
stack.pop()
(b)
1
2
3
4
5
6
7
stack.push(-2)
stack.push(-5)
stack.pop()
stack.push(stack.pop() + 4)
stack.push(7)
stack.push(2)
stack.push(stack.pop() * 3)
1
2
3
4
5
6
7
stack.push(-2)
stack.push(-5)
stack.pop()
stack.push(stack.pop() + 4)
stack.push(7)
stack.push(2)
stack.push(stack.pop() * 3)
Exercise 15.3: Completing DynamicArrayQueue
(a)
Complete the implementation of DynamicArrayQueue by implementing the methods in the Stack interface. Similar to DynamicArrayList, we should double the size of the backing array when full.
(b)
State the amortized runtime analysis for each method.
Exercise 15.4: Iterators for Stacks and Queues
We've seen that iterators are very helpful to go through all the elements of a collection in an efficient way. Why don't Stacks and Queues implement Iterable?
Exercise 15.5: Implement Stack with Queues
We've seen that Stacks and Queue are both fundamental data structures that store elements in an ordered way, but they differ in how elements are removed. In fact, these two ADTs are so similar that we can implement a Stack with Queues.
(a)

Implement a Queue with two Stacks. Is it possible to do this in a way that achieves an amortized \(O(1)\) runtime of the enqueue() and dequeue() operations?

1
2
3
4
5
6
/** A Queue implemented with two Stacks. */
public class StackedQueue<T> implements Queue<T> { 
  private Stack<T> stack1;
  private Stack<T> stack2;
  // ... 
}
1
2
3
4
5
6
/** A Queue implemented with two Stacks. */
public class StackedQueue<T> implements Queue<T> { 
  private Stack<T> stack1;
  private Stack<T> stack2;
  // ... 
}
(b)

Implement a Stack with two Queues. Is it possible to do this in a way that achieves an amortized \(O(1)\) runtime of the pop() and peek() operations?

1
2
3
4
5
6
/** A Stack implemented with two Queues. */
public class QueuedStack<T> implements Stack<T> { 
  private Queue<T> queue1;
  private Queue<T> queue2;
  // ... 
}
1
2
3
4
5
6
/** A Stack implemented with two Queues. */
public class QueuedStack<T> implements Stack<T> { 
  private Queue<T> queue1;
  private Queue<T> queue2;
  // ... 
}
Exercise 15.6: Matching Parentheses
Suppose we have a string containing only the characters '(' and ')'. A string of parentheses is considered matching if every opening parenthesis '(' has a corresponding closing parenthesis ')' and the pairs are properly nested.
(a)
Write a method that determines whether a given string of parentheses is matching. Write a loop invariant to accomplish this in a single pass.
(b)
Suppose now that this string can contain braces "{}" and brackets "[]". We adopt the convention that "()[]" and "([])" (for example) are considered matching, but "([)]" is not. Write a method that determines whether a given string of parentheses, brackets, and braces is valid.
Exercise 15.7: Max Stack
Design a class that maintains a stack of Integers while also supports \(O(1)\) access to the current maximum element stored in the stack. (Hint: use a second stack)
1
2
3
4
5
6
7
/** A stack that supports querying its maximum element. */
public class MaxStack implements Stack<Integer> {
  // other fields and methods

  /** Returns the maximum value in the stack in O(1) time. */
  public Integer max() { ... }
}
1
2
3
4
5
6
7
/** A stack that supports querying its maximum element. */
public class MaxStack implements Stack<Integer> {
  // other fields and methods

  /** Returns the maximum value in the stack in O(1) time. */
  public Integer max() { ... }
}
Exercise 15.8: Extensions of Expression Evaluator
Our calculator only supports single-digit numbers, addition, and multiplication. Let's add more features to our calculator. After each of these additions, write unit tests to ensure their functionality.
(a)
Incorporate multi-digit numbers.
(b)
Modify the specification to throw a checked exception when it is determined that the string is a malformed expression, rather than relying on cumbersome preconditions.
(c)
Add support for subtraction. Think about how we can keep track of the sign of a number and leverage our existing implementation of addition.
(d)
Support unary negation, so expressions like \(-3 + 2\) or \(-(1+2)\) should work correctly.
(e)
Support “implicit” multiplication by adjacent parentheses, such as \((5 + 1)(3+4)\).
Exercise 15.9: Boolean Expression Evaluator
We've already written an evaluator for arithmetic expressions. Now you’ll implement an evaluator for boolean expressions. Much like arithmetic expressions, boolean expressions must respect operator precedence and parentheses to evaluate correctly. Boolean expressions have the following supported alphabet.
  • Operands: T (true), F (false)
  • Operators (in decreasing precedence order):
    • ! (negation)
    • & (conjunction; i.e., logical and)
    • ^ (exclusive or)
    • | (disjunction; i.e., logical inclusive or)
Write a method to support evaluating a string that is a well-formed boolean expression.
Exercise 15.10: Double Ended Queue
A double ended queue (deque) ADT is an extension of a queue that supports adding elements to the front of the queue and removing elements from the rear of the queue.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/** A double ended queue ADT. */
public interface Deque<T> {
  /** Adds `elem` to the front of the deque. */
  void addFront(T elem);
  
  /** Adds `elem` to the back of the deque. */
  void addBack(T elem);

  /** 
   * Removes and returns the front element of this deque. 
   * Requires !this.isEmpty();
   */
  T removeFront();

  /** 
   * Removes and returns the last element of this deque. 
   * Requires !this.isEmpty();
   */
  T removeBack();

  /**
   * Returns `true` if there are currently no elements stored in this queue, 
   * otherwise returns `false`.
   */
  boolean isEmpty();

  /**
   * Returns the "front" element of this deque without removing it. 
   * Requires `!this.isEmpty()`.
   */
  T peekFront();

  /**
   * Returns the "back" element of this deque without removing it. 
   * Requires `!this.isEmpty()`.
   */
  T peekBack();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/** A double ended queue ADT. */
public interface Deque<T> {
  /** Adds `elem` to the front of the deque. */
  void addFront(T elem);
  
  /** Adds `elem` to the back of the deque. */
  void addBack(T elem);

  /** 
   * Removes and returns the front element of this deque. 
   * Requires !this.isEmpty();
   */
  T removeFront();

  /** 
   * Removes and returns the last element of this deque. 
   * Requires !this.isEmpty();
   */
  T removeBack();

  /**
   * Returns `true` if there are currently no elements stored in this queue, 
   * otherwise returns `false`.
   */
  boolean isEmpty();

  /**
   * Returns the "front" element of this deque without removing it. 
   * Requires `!this.isEmpty()`.
   */
  T peekFront();

  /**
   * Returns the "back" element of this deque without removing it. 
   * Requires `!this.isEmpty()`.
   */
  T peekBack();
}
(a)
Implement this ADT using a dynamic array. State the (amortized) runtime of each method.
(b)
Implement this ADT using a doubly-linked list (see Exercise 13.3). State the runtime of each method.
(c)
Can a deque be used as a queue? What about as a stack?