SEN2211 · Week 5 · Stacks

Stack: with an array and with a linked list

LIFO logic, push/pop/peek, two implementations with an array and a linked list, overflow and underflow. The exam asks for the state of the stack after a sequence of operations and for pop order questions.

stackLIFOpushpoppeektopisEmptystack overflowstack underflowarray-based stacklinked stackabstract data type

A stack is like plates piled on top of each other: you put a plate on the top, and when taking one you take the one on top. You can’t pull a plate from the middle. This rule is called LIFO (Last In, First Out). It looks simple, but compilers’ expression evaluation, method calls, the undo feature and parenthesis checking all work with a stack. Exam questions are of two types: finding what is printed or what is left in the stack after a sequence of operations, and writing push/pop with an array or a linked list.

Golden rules:

  1. You only reach the top. push puts on the top, pop takes from the top.
  2. peek looks, it doesn’t remove. When tracing, don’t confuse pop with peek.
  3. In an array, top is the index of the last element. -1 in an empty stack.
  4. In a linked list the top is the head of the list. Not the end.
  5. pop from an empty stack = underflow, push to a full array = overflow.

Abstract data type (ADT)

A stack is an abstract data type: it says which operations exist, not how they are done. The operations:

OperationWhat it doesTime
push(x)puts x on the topO(1)
pop()removes and returns the top elementO(1)
peek() (or top())returns the top element, doesn’t remove itO(1)
isEmpty()is the stack emptyO(1)
size()number of elementsO(1)

We can implement the same ADT in two ways: with an array or with a linked list. The code that uses it sees no difference.

Stack with an array (array-based stack)

class ArrayStack {
    private int[] data;
    private int top = -1;   // index of the last element

    ArrayStack(int capacity) { data = new int[capacity]; }

    boolean isEmpty() { return top == -1; }
    boolean isFull()  { return top == data.length - 1; }
    int size()        { return top + 1; }

    void push(int x) {
        if (isFull()) throw new RuntimeException("Stack overflow");
        data[++top] = x;    // increment first, then write
    }

    int pop() {
        if (isEmpty()) throw new RuntimeException("Stack underflow");
        return data[top--]; // read first, then decrement
    }

    int peek() {
        if (isEmpty()) throw new RuntimeException("Stack underflow");
        return data[top];
    }
}

The difference between ++top and top-- is critical here. If you write data[top++] = x, the first element is written to data[-1] and you get an ArrayIndexOutOfBoundsException.

Worked example

ArrayStack s = new ArrayStack(5);
s.push(10); s.push(20); s.push(30);
System.out.println(s.pop());
System.out.println(s.peek());
s.push(40);
System.out.println(s.size() + " " + s.raw());   // raw: the raw state of the array
30
20
3 [10, 20, 40, 0, 0] top=2
push 10     [10, _, _, _, _]   top=0
push 20     [10, 20, _, _, _]  top=1
push 30     [10, 20, 30, _, _] top=2
pop → 30    [10, 20, 30, _, _] top=1   (30 is still in the array but not in the stack)
peek → 20   no change
push 40     [10, 20, 40, _, _] top=2   (written over the 30)

PITFALL · pop doesn’t erase from the array. If the exam asks “what are the contents of the array?”, the popped values are still there; they just sit above the top index. A new push writes over them.

Growing when full

Instead of a fixed capacity, when full you can open a new array of double the size and copy. That push takes O(n), but since the capacity doubles each time, this happens rarely. The total cost of n pushes is O(n), that is O(1) amortized per push. Java’s ArrayList and ArrayDeque classes work this way.

Stack with a linked list (linked stack)

Inserting at the head of a singly linked list and deleting from the head are already O(1). Top = head.

class LinkedStack {
    private static class Node {
        int data; Node next;
        Node(int d, Node n) { data = d; next = n; }
    }
    private Node top;

    boolean isEmpty() { return top == null; }

    void push(int x) { top = new Node(x, top); }

    int pop() {
        if (top == null) throw new RuntimeException("Stack underflow");
        int x = top.data;
        top = top.next;
        return x;
    }

    int peek() {
        if (top == null) throw new RuntimeException("Stack underflow");
        return top.data;
    }
}

In this implementation there is no overflow (until memory runs out). The cost is one next reference per node.

ArrayLinked list
push / pop / peekO(1)O(1)
Capacityfixed (or amortized with growing)unbounded
Overflowyesno
Extra memoryunused array cellsa next in every node

Classic jobs done with a stack

Reversing. Push the letters in order, then pop: “DATA” → “ATAD”. The order that goes into the stack is reversed on the way out.

Decimal to binary. Push the remainders of n % 2, halve n; then pop everything. 13 → 1101, 6 → 110, 32 → 100000.

static String toBinary(int n) {
    if (n == 0) return "0";
    Deque<Integer> s = new ArrayDeque<>();
    while (n > 0) { s.push(n % 2); n /= 2; }
    StringBuilder sb = new StringBuilder();
    while (!s.isEmpty()) sb.append(s.pop());
    return sb.toString();
}

Method calls. On every method call, Java puts the local variables in a stack frame; when the method ends, the frame is popped. Infinite recursion fills this stack and gives a StackOverflowError (week 10).

Other applications (parenthesis balance, infix → postfix, postfix evaluation) are the subject of next week.

Pop order questions

“1, 2, 3, 4 are pushed in order, and a pop can happen at any moment in between. Which output order is possible?” To solve it, simulate: push up to the number that needs to come out, then check whether the top is that number.

Target 3 1 4 2:
  3 needed → push 1, 2, 3 → pop 3       stack [1, 2]
  1 needed → top is 2, not 1            IMPOSSIBLE

Target 3 2 4 1:
  push 1,2,3 → pop 3 → [1,2]
  pop 2 → [1]
  push 4 → pop 4 → [1]
  pop 1                                 POSSIBLE

Short rule: after a number is popped, the numbers smaller than it that have not yet come out must come out in decreasing order.

A ready-made stack in Java

The java.util.Stack class is old (derived from Vector, synchronized, slow). In modern Java the recommended one is the Deque interface and the ArrayDeque class:

Deque<Integer> st = new ArrayDeque<>();
st.push(1); st.push(2);
st.peek();   // 2
st.pop();    // 2

On an empty ArrayDeque, pop() throws NoSuchElementException, while peek() returns null. Details in week 9.

Common mistakes

WrongCorrect
peek removes the elementIt only looks
Putting the top at the end of the list in a linked stackHead = top, both are O(1)
data[top++] = x (when top = -1)data[++top] = x
return data[--top]return data[top--]
pop erases the value from the arrayOnly top decreases, the value stays
No pop check on an empty stackif (isEmpty()) throw ...
Answering a pop order question by instinctSimulate step by step
isFull: top == data.lengthtop == data.length - 1

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1easy

What is printed? (s is an empty stack.)

s.push(5); s.push(3);
System.out.print(s.pop() + " ");
s.push(8); s.push(1);
System.out.print(s.pop() + " ");
System.out.print(s.pop() + " ");
s.push(6);
System.out.println(s.peek());
Show answer
3 1 8 6

[5,3] → pop 3 → [5] → [5,8,1] → pop 1 → pop 8 → [5] → push 6 → [5,6], peek 6 (does not remove). The output: 3 1 8 6.

Question 2medium

What is printed, and what is left in the stack (top first)?

for (int i = 1; i <= 5; i++) {
    s.push(i * 2);
    if (i % 2 == 0)
        System.out.print(s.pop() + " ");
}
Show answer
4 8; stack top -> 10 6 2

i=1: push 2. i=2: push 4, pop 4. i=3: push 6. i=4: push 8, pop 8. i=5: push 10. Printed: 4 8. What remains (from the top): 10 6 2.

Question 3medium

An array stack has capacity 4, top starts at -1 and push does data[++top] = x. After the calls below, what are the contents of the array and the value of top?

push(7); push(2); push(9);
pop(); pop();
push(4);
Show answer
[7, 4, 9, 0], top = 1

After the pushes: [7,2,9,0], top 2. The two pops only decrease top: top 0. The values stay in the array but logically are not in the stack. push(4) writes to index 1 and lands on top of 2: [7,4,9,0], top 1. 9 is still in the array but not part of the stack.

Question 4medium

An array stack with capacity 3 runs this loop. What is printed?

try {
    for (int i = 1; i <= 4; i++) {
        s.push(i);
        System.out.print(i + " ");
    }
} catch (RuntimeException e) {
    System.out.println("-> " + e.getMessage());
}
Show answer
1 2 3 -> Stack overflow

Three pushes succeed, and the number is printed after each. At the fourth push the array is full (top == length - 1), push throws an exception, and it falls into the catch without printing 4.

Question 5easy

Write int pop() for an array-based stack with fields int[] data and int top (top = index of the top element, -1 when empty). Throw an exception on underflow.

Show answer
Check top == -1, else return data[top--].

int pop() {
    if (top == -1)
        throw new RuntimeException("Stack underflow");
    return data[top--];   // read first, then decrement
}
If you write data[--top] you return the element below. Since top-- is postfix, it is read with the old value of top first. O(1).

Question 6medium

Write void push(int x) and int pop() for a linked-list stack whose only field is Node top (Node has data and next, and a constructor Node(int d, Node n)).

Show answer
push: top = new Node(x, top); pop: save top.data, top = top.next.

In a linked list the top of the stack is the head of the list; that way both operations are O(1).

void push(int x) {
    top = new Node(x, top);
}

int pop() {
    if (top == null)
        throw new RuntimeException("Stack underflow");
    int x = top.data;
    top = top.next;
    return x;
}
If the top were at the end of the list, pop would need to search for the node before the last, which would be O(n).

Question 7hard

The numbers 1, 2, 3, 4 are pushed in this order; pops may happen at any time. Which of these output sequences are possible? (a) 2 4 3 1 (b) 3 1 4 2 (c) 4 3 2 1 (d) 1 4 2 3

Show answer
(a) and (c)

(a) push 1, push 2, pop 2, push 3, push 4, pop 4, pop 3, pop 1: possible. (c) push all four, pop in order: possible. (b) When 3 comes out, 1 and 2 are in the stack, with 2 on top. To pop 1, 2 must come out first: impossible. (d) When 4 comes out, 2 and 3 are inside, with 3 on top, so 2 can't come out first: impossible. Rule: after a number is popped, the smaller ones must come out in decreasing order.

Question 8easy

What are the time complexities of push, pop and peek for an array stack and for a linked stack?

Show answer
O(1) for all

In the array only the top index changes, in the list only the head node changes. None depends on n. In an implementation that copies to a new array of double the size when the array is full, that one push is O(n), but on average (amortized) it is still O(1).

Question 9easy

What does this method return for "DATA"?

static String f(String w) {
    Deque<Character> st = new ArrayDeque<>();
    for (char ch : w.toCharArray()) st.push(ch);
    StringBuilder sb = new StringBuilder();
    while (!st.isEmpty()) sb.append(st.pop());
    return sb.toString();
}
Show answer
ATAD

The letters enter in the order D, A, T, A; the top is the last A. When popped, A, T, A, D come out. The stack reverses the order: ATAD.

Question 10medium

The values 3 8 6 1 4 are pushed onto s in this order. What is printed?

while (!s.isEmpty()) {
    int x = s.pop();
    if (x % 2 == 0) t.push(x);
}
while (!t.isEmpty())
    System.out.print(t.pop() + " ");
Show answer
8 6 4

The pop order from s is 4 1 6 8 3. The evens enter t in the order 4, 6, 8, with 8 on top. The pop order from t is 8 6 4. Because it was reversed twice, the evens returned to the original insertion order.

Question 11medium

Write static String toBinary(int n) that converts a positive integer to its binary string using a stack.

Show answer
Push n % 2 while n > 0 (n /= 2), then pop everything into a string.

The remainders start from the lowest digit, but when writing we start from the highest. The stack reverses the order.

static String toBinary(int n) {
    if (n == 0) return "0";
    Deque<Integer> s = new ArrayDeque<>();
    while (n > 0) {
        s.push(n % 2);
        n /= 2;
    }
    StringBuilder sb = new StringBuilder();
    while (!s.isEmpty()) sb.append(s.pop());
    return sb.toString();
}
13 → remainders 1, 0, 1, 1 → output 1101. 6 → 110, 32 → 100000.

Question 12hard

Stack s contains (bottom to top) 4 7 2. What is printed?

int sum = 0;
while (s.peek() != 4)
    sum += s.pop();
System.out.println(sum + " " + s.peek());
Show answer
9 4

Top is 2: not 4, pop, sum = 2. Top is 7: pop, sum = 9. Top is 4: the loop ends. 4 stays in the stack, and peek returns 4. The output: 9 4. If there were no 4 at all, peek would throw underflow when the stack became empty.

Verification: ArrayStack and LinkedStack were written in verify/SEN2211/W05.java and run on the JDK (java W05.java). Whether pop orders are possible was checked in the same file with a simulation that pushes 1..n in order.

Review card

STACK = LIFO (Last In, First Out): the last one in is the first one out push(x) put on top | pop() take from the top and remove | peek() look at the top, don't remove | isEmpty() All O(1) ARRAY: int[] data; int top = -1; push: data[++top] = x; pop: return data[top--]; empty: top == -1 full: top == data.length - 1 size = top + 1 pop does not erase the value from the array, only top decreases LIST: top = the HEAD of the list push: top = new Node(x, top); pop: top = top.next; Overflow: push to a full array Underflow: pop/peek from an empty stack A stack REVERSES order (reverse a string, decimal → binary) Pop order test (push 1..n in order): after a number is popped, the smaller ones must come out in DECREASING order Java: Deque<Integer> s = new ArrayDeque<>(); push / pop / peek