Chapter 7: Stacks
Introduction
Learning Outcomes
Explain the last-in, first-out behavior of a stack.
Implement a stack with an array and with a linked list.
Use stacks to parse expressions and manage method calls.
Analyze the time and space cost of the basic stack operations.
The Stack ADT
The Stack
- Items are added and removed at one end only: the top.
- Think of a pile of plates or boxes.
- To reach the bottom item, you must remove everything above it, one at a time.
- The rule is LIFO (last in, first out), or equivalently FILO (first in, last out).
A new plate goes on top, and you take plates from the top.
The Stack ADT
Basic Operations
| Operation | Description |
|---|---|
push(x) | Adds a new item x on top. |
pop() | Removes and returns the top item. |
top() | Returns the top item without removing it (Java: peek()). |
isEmpty() | Is the stack empty? |
size() | Returns the number of items. |
The Stack ADT
push and pop, Step by Step
The stack starts empty: size = 0, nothing on top.
push(10): 10 goes onto the empty stack; the top is now 10.
push(15): 15 goes on top of 10.
push(20): 20 goes on top; size = 3.
top(): reads the top item, 20, but does not remove it.
pop(): 20, the last in, is the first out (LIFO).
pop(): removes 15; the top is now 10.
pop(): removes 10; the stack is empty again.
pop() on an empty stack → underflow error.
The Stack ADT
The Stack ADT as a Java Interface
public interface Stack<E> { int size(); boolean isEmpty(); void push(E e); E top(); // null if empty E pop(); // throws if empty }
- ADT = what? The interface states the operations and what they mean, not how the data is stored.
- Implementation = how?
ArrayStack<E> implements Stack<E>, or a linked-list class. - Code written against the interface lets you swap the implementation without touching the callers.
| On an empty stack | pop() | top() / peek() |
|---|---|---|
Our Stack<E> (above) | throws an exception | returns null |
java.util.Stack | EmptyStackException | EmptyStackException |
java.util.ArrayDeque | NoSuchElementException | returns null |
The same abstract operation handles “empty” differently from library to library, so check isEmpty() first. Our interface shares its name with java.util.Stack; don't import both in the same file.
The Stack ADT
Algorithms for the Operations (Array)
The stack lives in an array; top is the index of the top item, and an empty stack has top = −1.
begin if stack is full // top = cap−1 error: overflow else increment top by 1 stack[top] ← x end
begin if stack is empty // top = −1 error: underflow else x ← stack[top]; decrement top by 1 return x end
begin if stack is empty return null else return stack[top] end
isEmpty: true if top = −1, else false size: return top + 1
The Stack ADT
Time and Space Complexity
| Operation | Time | Why? |
|---|---|---|
push | O(1) | only the top changes |
pop | O(1) | only the top changes |
top | O(1) | the top is known directly |
isEmpty, size | O(1) | a counter / index is kept |
| searching for an item | O(n) | items must be removed one by one |
- The basic operations take the same time no matter how many items the stack holds.
- A stack of n items uses O(n) memory; the operations need only O(1) extra.
- A dynamic array grows when it fills up, so push is amortized O(1).
Implementation
Kinds of Stacks
- Capacity is set up front; it can neither grow nor shrink.
- Pushing onto a full stack → overflow.
- Popping an empty stack → underflow.
- Backed by a fixed-size array.
- Grows and shrinks at run time.
- Increases its capacity automatically when full (e.g., doubles it).
- May shrink when it holds far fewer items than its capacity.
- Implemented with a dynamic array or a linked list.
Popping an empty stack (underflow) is an error in both kinds; overflow happens only in a fixed-size stack.
Implementation
Ways to Implement a Stack
The stack ADT can be implemented with an array or a linked list.
push: increment top, then write the item to arr[top].pop: read arr[top], then decrement top.push: add a new node at the head of the list.pop: remove the head node and return its value.Implementation
Array-Based Stack
This is also called a bounded stack: its capacity is fixed when it is created and never changes.
Advantages
- Simple and easy to implement.
- No per-node references → saves memory.
- Memory use is tightly bounded, which makes it safer against attacks that try to exhaust memory.
- Items are contiguous → cache-friendly.
Watch out for
- Check that the stack is not full before pushing.
- Too small a capacity overflows; too large wastes memory.
- In an array of objects, set a popped cell to
null; otherwise the object lingers in memory.
Implementation
Array-Based Stack: Java
public class ArrayStack<E> implements Stack<E> { private E[] data; private int top = -1; // -1 → empty stack public ArrayStack(int capacity) { data = (E[]) new Object[capacity]; } public boolean isEmpty() { return top == -1; } public int size() { return top + 1; } public E top() { return isEmpty() ? null : data[top]; } public void push(E x) { if (top == data.length - 1) throw new IllegalStateException("Overflow"); data[++top] = x; } public E pop() { if (isEmpty()) throw new IllegalStateException("Underflow"); E x = data[top]; data[top--] = null; // drop the reference return x; } }
Java cannot create a generic array directly, so we cast an Object[] (the compiler issues an “unchecked” warning). Every operation is O(1).
Implementation · Step through the code
Code Trace: Array-Based Stack
new ArrayStack<>(4): four cells, all null; an empty stack has top = −1.
push(10): full? top = -1, data.length − 1 = 3 → no, continue.
data[++top] = x: top goes -1 → 0 first, then data[0] = 10. One assignment → O(1).
push(15): full? top = 0, data.length − 1 = 3 → no, continue.
data[++top] = x: top goes 0 → 1 first, then data[1] = 15. One assignment → O(1).
push(20): full? top = 1, data.length − 1 = 3 → no, continue.
data[++top] = x: top goes 1 → 2 first, then data[2] = 20. One assignment → O(1).
top(): the stack is not empty → returns data[2] = 20. The array and top do not change.
pop(): isEmpty()? top = 2 ≠ −1 → not empty, continue.
E x = data[top] → x = 20 (save the top item).
data[top--] = null: clears the cell (the object is left to the garbage collector); top → 1.
return 20: last in, first out (LIFO).
push(25): full? top = 1, data.length − 1 = 3 → no, continue.
data[++top] = x: top goes 1 → 2 first, then data[2] = 25. One assignment → O(1).
push(30): full? top = 2, data.length − 1 = 3 → no, continue.
data[++top] = x: top goes 2 → 3 first, then data[3] = 30. One assignment → O(1).
push(40): full? top = 3, data.length − 1 = 3 → yes, full!
The array is full: IllegalStateException("Overflow") is thrown. This is overflow in a fixed-capacity stack; a dynamic array would grow here instead.
public void push(E x) { if (top == data.length - 1) throw new IllegalStateException("Overflow"); data[++top] = x; } public E pop() { if (isEmpty()) throw new IllegalStateException("Underflow"); E x = data[top]; data[top--] = null; // drop the reference return x; } public E top() { return isEmpty() ? null : data[top]; }
Implementation
Dynamic Array: Doubling When Full
public void push(E x) { if (top == data.length - 1) resize(2 * data.length); data[++top] = x; } private void resize(int newCap) { E[] bigger = (E[]) new Object[newCap]; for (int i = 0; i <= top; i++) bigger[i] = data[i]; data = bigger; }
- When the array is full, we allocate one twice as large and copy the items over, so a push that grows the array takes O(n).
- Growth is rare: over n pushes the copies total 2 + 4 + 8 + … < 2n, and the total cost is < 3n → push is amortized O(1).
- Adding a fixed number of cells (+c) each time gives O(n²) in total, O(n) per push; capacity must grow proportionally.
Amortized cost is an average over a long sequence of operations, not the cost of any single one.
Implementation
Linked-List Stack
- Items live in a chain of linked nodes → the size is flexible.
- The size changes at run time as needed; no overflow (until memory runs out).
- Each node stores a reference (next), which costs extra memory.
- No random access, but reaching the top item (the head of the list) is quick and simple.
- Insertion and removal happen at the head, so both are O(1).
class Node { int data; Node next; Node(int data) { this.data = data; } // next = null }
Why insert at the head? Deleting from the tail of a singly linked list requires finding the node before the tail → O(n).
Implementation · Step through the code
Code Trace: push on a Linked List
Start: top = null, size = 0 → empty stack.
push(10): creates new Node(10); temp points to it, and its next field defaults to null.
temp.next = top: top is null, so next stays null (the new node stands alone).
top = temp: the new node is now the top of the stack.
size++ → 1. The method returns; the local temp goes away, and the node is reached through top.
push(15): creates node 15. It is not linked to the list yet; top still points to 10.
temp.next = top: 15 links to 10, the old top. We link first so the old list isn't lost.
top = temp: the new top is 15. Insertion at the head, no shifting → O(1).
size++ → 2.
push returns. Stack (top to bottom): 15 → 10. The last item pushed is on top.
Node top; // top of the stack int size; public void push(int data) { Node temp = new Node(data); temp.next = top; top = temp; size++; }
Implementation · Step through the code
Code Trace: pop on a Linked List
The stack holds 15 and 10. The loop will call pop() 3 times.
pop(): check for empty first; top is not null → continue.
result = top.data → save 15 (before the node is unlinked).
top = top.next: top moves down one → node(10). Nothing refers to the old top node anymore.
size-- → 1.
return 15. The garbage collector reclaims the unreachable node.
main prints the returned value, 15.
pop(): check for empty first; top is not null → continue.
result = top.data → save 10 (before the node is unlinked).
top = top.next: top moves down one → null. Nothing refers to the old top node anymore.
size-- → 0.
return 10. The garbage collector reclaims the unreachable node.
main prints the returned value, 10.
pop(): top == null → the stack is empty!
An underflow exception stops the program. Without the check, top.data would throw a NullPointerException.
public int pop() { if (top == null) throw new RuntimeException("empty"); int result = top.data; top = top.next; size--; return result; } // in main (stack: 15 → 10): for (int k = 0; k < 3; k++) System.out.println(pop());
Implementation
Array or Linked List?
| Criterion | Array-based | Linked-list-based |
|---|---|---|
| Size | fixed capacity (a dynamic array grows) | flexible, changes at run time |
| push / pop | O(1) (amortized O(1) with growth) | O(1) |
| Overflow | possible | only if memory runs out |
| Memory per item | data only | data + next reference |
| Wasted space | unused capacity | none |
| Java | ArrayDeque, Stack (Vector-based) | LinkedList (as a Deque) |
Deque<Integer> s = new ArrayDeque<>(); with push/pop/peek; the legacy Stack class is synchronized and therefore slower.Applications
Where Stacks Are Used
Applications
Browser Back and Forward: Two Stacks
The browser opens on Home; both stacks are empty.
visit(News): push the old page onto the back stack and open the new one.
visit(Sports): push the old page onto the back stack and open the new one.
back(): push the current page onto the forward stack; pop the back stack → News.
back(): push the current page onto the forward stack; pop the back stack → Home.
forward(): push the current page onto the back stack; pop the forward stack → News.
visit(Weather): push the old page onto the back stack and open the new one. A new path begins, so the forward stack is cleared.
back(): push the current page onto the forward stack; pop the back stack → News.
Applications · Step through the code
The Call Stack: fact(3) Step by Step
main is running and is about to call fact(3). Every call pushes a new stack frame.
The frame for fact(3) is pushed. n <= 1? 3 <= 1 → false.
3 * fact(2) can't be computed until fact(2) finishes. This frame waits, suspended.
The frame for fact(2) is pushed. n <= 1? 2 <= 1 → false.
2 * fact(1) can't be computed until fact(1) finishes. This frame waits, suspended.
The frame for fact(1) is pushed. n <= 1? 1 <= 1 → true.
Base case: return 1. Now the stack starts unwinding.
fact(1) is popped and returns 1. fact(2) resumes: 2 * 1 = 2.
fact(2) is popped and returns 2. fact(3) resumes: 3 * 2 = 6.
fact(3) = 6 is printed. Depth n → O(n) memory; recursion that goes too deep throws StackOverflowError.
int fact(int n) { if (n <= 1) return 1; return n * fact(n - 1); } // in main: System.out.println(fact(3));
Applications · Step through the code
Reversing a String, Step by Step
main calls reverse("ABCD").
Create an empty stack of characters.
Copy the string into a char array (we will overwrite it).
First loop: c = 'A' → push('A').
First loop: c = 'B' → push('B').
First loop: c = 'C' → push('C').
First loop: c = 'D' → push('D'). Every character is on the stack, the last one on top.
Second loop, i = 0: pop() → 'D', chars[0] = 'D'. Items come out in reverse order.
Second loop, i = 1: pop() → 'C', chars[1] = 'C'. Items come out in reverse order.
Second loop, i = 2: pop() → 'B', chars[2] = 'B'. Items come out in reverse order.
Second loop, i = 3: pop() → 'A', chars[3] = 'A'. Items come out in reverse order.
return "DCBA". Time and extra memory are O(n).
main prints the result: DCBA.
String reverse(String str) {
Deque<Character> stack = new ArrayDeque<>();
char[] chars = str.toCharArray();
for (char c : chars)
stack.push(c);
for (int i = 0; i < str.length(); i++)
chars[i] = stack.pop();
return new String(chars);
}
// in main:
System.out.println(reverse("ABCD"));
Applications
Bracket Matching
Let the string S contain only the characters ( ) { } [ ]. S is balanced if and only if
- every opening bracket is closed by a bracket of the same type, and
- brackets are closed in the right order (the last one opened is the first one closed → LIFO!).
- Opening bracket → push it.
- Closing bracket → if the stack is empty, unbalanced; otherwise pop, and if the popped bracket is a different type, unbalanced.
- At the end of S, an empty stack means balanced; anything left over is an unclosed bracket.
Applications
Bracket Matching: Java
boolean isBalanced(String s) { Deque<Character> stack = new ArrayDeque<>(); for (char c : s.toCharArray()) { if (c == '(' || c == '[' || c == '{') { stack.push(c); } else { if (stack.isEmpty()) return false; char open = stack.pop(); if (!matches(open, c)) return false; } } return stack.isEmpty(); }
boolean matches(char open, char close) { return (open == '(' && close == ')') || (open == '[' && close == ']') || (open == '{' && close == '}'); }
"())" or "]" pops an empty stack → exception.Applications · Step through the code
Code Trace: isBalanced("{{[]}()}[]")
Create an empty stack to hold the unmatched opening brackets.
'{' is an opening bracket → push. It waits for its closer.
'{' is an opening bracket → push. It waits for its closer.
'[' is an opening bracket → push. It waits for its closer.
']' is a closing bracket; the stack is not empty → pop the most recent opener, '['.
matches('[', ']') → true: same type, right order. Continue.
'}' is a closing bracket; the stack is not empty → pop the most recent opener, '{'.
matches('{', '}') → true: same type, right order. Continue.
'(' is an opening bracket → push. It waits for its closer.
')' is a closing bracket; the stack is not empty → pop the most recent opener, '('.
matches('(', ')') → true: same type, right order. Continue.
'}' is a closing bracket; the stack is not empty → pop the most recent opener, '{'.
matches('{', '}') → true: same type, right order. Continue.
'[' is an opening bracket → push. It waits for its closer.
']' is a closing bracket; the stack is not empty → pop the most recent opener, '['.
matches('[', ']') → true: same type, right order. Continue.
The string is done and the stack is empty → return true: balanced. Each character was handled once → O(n).
boolean isBalanced(String s) { Deque<Character> stack = new ArrayDeque<>(); for (char c : s.toCharArray()) { if (c == '(' || c == '[' || c == '{') { stack.push(c); } else { if (stack.isEmpty()) return false; char open = stack.pop(); if (!matches(open, c)) return false; } } return stack.isEmpty(); }
Applications · Step through the code
Code Trace: isBalanced("([)]")
Create an empty stack to hold the unmatched opening brackets.
'(' is an opening bracket → push. It waits for its closer.
'[' is an opening bracket → push. It waits for its closer.
')' is a closing bracket; the stack is not empty → pop the most recent opener, '['.
matches('[', ')') → false: ')' arrives, but the most recent opener is '['. return false. Likewise, (() fails because the stack is not empty at the end, and ()) because a closer meets an empty stack.
boolean isBalanced(String s) { Deque<Character> stack = new ArrayDeque<>(); for (char c : s.toCharArray()) { if (c == '(' || c == '[' || c == '{') { stack.push(c); } else { if (stack.isEmpty()) return false; char open = stack.pop(); if (!matches(open, c)) return false; } } return stack.isEmpty(); }
Applications
Next Greater Element
For each item in an array, find the first larger item to its right, or −1 if there is none.
| arr | 4 | 7 | 3 | 4 | 8 | 1 |
|---|---|---|---|---|---|---|
| result | 7 | 8 | 4 | 8 | −1 | −1 |
- Brute force: scan to the right of every item → O(n²).
- With a stack: scan the array right to left, keeping only “candidate” larger values → O(n).
- From bottom to top, the stack stays in decreasing order (a monotonic stack).
int[] nextGreater(int[] arr) { int[] result = new int[arr.length]; Deque<Integer> stack = new ArrayDeque<>(); for (int i = arr.length-1; i >= 0; i--) { while (!stack.isEmpty() && stack.peek() <= arr[i]) stack.pop(); result[i] = stack.isEmpty() ? -1 : stack.peek(); stack.push(arr[i]); } return result; }
Applications · Step through the code
Code Trace: nextGreater({4, 7, 3, 4, 8, 1})
Create result (all 0s) and an empty stack. The array is scanned right to left.
i = 5 (arr[i] = 1): the stack is empty → −1.
push(1): a new candidate for the items to the left. The stack stays decreasing from bottom to top.
i = 4: the top, 1, is ≤ 8 → pop. 1 can never be the answer for anything left of 8 (the larger 8 is in the way).
i = 4 (arr[i] = 8): the stack is empty → −1.
push(8): a new candidate for the items to the left. The stack stays decreasing from bottom to top.
i = 3 (arr[i] = 4): the top, 8, is > 4 → result[3] = 8.
push(4): a new candidate for the items to the left. The stack stays decreasing from bottom to top.
i = 2 (arr[i] = 3): the top, 4, is > 3 → result[2] = 4.
push(3): a new candidate for the items to the left. The stack stays decreasing from bottom to top.
i = 1: the top, 3, is ≤ 7 → pop. 3 can never be the answer for anything left of 7 (the larger 7 is in the way).
i = 1: the top, 4, is ≤ 7 → pop. 4 can never be the answer for anything left of 7 (the larger 7 is in the way).
i = 1 (arr[i] = 7): the top, 8, is > 7 → result[1] = 8.
push(7): a new candidate for the items to the left. The stack stays decreasing from bottom to top.
i = 0 (arr[i] = 4): the top, 7, is > 4 → result[0] = 7.
push(4): a new candidate for the items to the left. The stack stays decreasing from bottom to top.
return [7, 8, 4, 8, -1, -1]. Each item is pushed at most once and popped at most once → O(n).
int[] nextGreater(int[] arr) { int[] result = new int[arr.length]; Deque<Integer> stack = new ArrayDeque<>(); for (int i = arr.length-1; i >= 0; i--) { while (!stack.isEmpty() && stack.peek() <= arr[i]) stack.pop(); result[i] = stack.isEmpty() ? -1 : stack.peek(); stack.push(arr[i]); } return result; }
Applications · Step through the code
Code Trace: A Stack with O(1) getMin()
Two stacks: s holds every item, and min always has the current minimum on top. Goal: O(1) getMin().
push(5): s.push(5).
min is empty → min.push(5): a new (or equal) minimum.
push(3): s.push(3).
3 <= 5 → true → min.push(3): a new (or equal) minimum.
push(7): s.push(7).
7 <= 3 → false: 7 is not a new minimum, so min does not change.
push(3): s.push(3).
3 <= 3 → true → min.push(3): a new (or equal) minimum.
getMin(): min.peek() = 3. No searching → O(1).
pop(): 3 comes off s. 3 == min.peek()? yes → pop min too; the new minimum is 3.
getMin(): min.peek() = 3. No searching → O(1).
pop(): 7 comes off s. 7 == min.peek()? no → min does not change.
pop(): 3 comes off s. 3 == min.peek()? yes → pop min too; the new minimum is 5.
getMin(): min.peek() = 5. No searching → O(1). Thanks to <=, the minimum stayed 3 after the second 3 was popped.
Deque<Integer> s = new ArrayDeque<>(); Deque<Integer> min = new ArrayDeque<>(); void push(int x) { s.push(x); if (min.isEmpty() || x <= min.peek()) min.push(x); } int pop() { int x = s.pop(); if (x == min.peek()) min.pop(); return x; } int getMin() { return min.peek(); }
Towers of Hanoi
Towers of Hanoi: A Non-Recursive Solution with a Stack
Rules: each move takes the single top disk; a larger disk never goes on a smaller one.
// Task: move n disks from 'from' to 'to' using 'via' record Task(int n, char from, char via, char to) {} void hanoi(int n) { Deque<Task> stack = new ArrayDeque<>(); stack.push(new Task(n, 'A', 'B', 'C')); while (!stack.isEmpty()) { Task t = stack.pop(); if (t.n() == 1) System.out.println(t.from() + " → " + t.to()); else { // reverse order stack.push(new Task(t.n()-1, t.via(), t.from(), t.to())); stack.push(new Task(1, t.from(), t.via(), t.to())); stack.push(new Task(t.n()-1, t.from(), t.to(), t.via())); } } }
Towers of Hanoi · Step through the code
Code Trace: hanoi(3) (with a stack)
The initial task is (3, A, B, C): “move 3 disks from A to C, using B”.
pop → t = (3,A,B,C): this is the task to do now.
n = 3 > 1 → push three subtasks in reverse order; (2, A, C, B), which runs first, ends up on top (LIFO).
pop → t = (2,A,C,B): this is the task to do now.
n = 2 > 1 → push three subtasks in reverse order; (1, A, B, C), which runs first, ends up on top (LIFO).
pop → t = (1,A,B,C): this is the task to do now.
n = 1 → move directly: disk 1, A → C (move 1).
pop → t = (1,A,C,B): this is the task to do now.
n = 1 → move directly: disk 2, A → B (move 2).
pop → t = (1,C,A,B): this is the task to do now.
n = 1 → move directly: disk 1, C → B (move 3).
void hanoi(int n) { Deque<Task> stack = new ArrayDeque<>(); stack.push(new Task(n, 'A', 'B', 'C')); while (!stack.isEmpty()) { Task t = stack.pop(); if (t.n() == 1) System.out.println(t.from() + " → " + t.to()); else { // reverse order stack.push(new Task(t.n()-1, t.via(), t.from(), t.to())); stack.push(new Task(1, t.from(), t.via(), t.to())); stack.push(new Task(t.n()-1, t.from(), t.to(), t.via())); } } }
Towers of Hanoi · Step through the code
Code Trace: hanoi(3) (with a stack, continued)
pop → t = (1,A,B,C): this is the task to do now.
n = 1 → move directly: disk 3, A → C (move 4).
pop → t = (2,B,A,C): this is the task to do now.
n = 2 > 1 → push three subtasks in reverse order; (1, B, C, A), which runs first, ends up on top (LIFO).
pop → t = (1,B,C,A): this is the task to do now.
n = 1 → move directly: disk 1, B → A (move 5).
pop → t = (1,B,A,C): this is the task to do now.
n = 1 → move directly: disk 2, B → C (move 6).
pop → t = (1,A,B,C): this is the task to do now.
n = 1 → move directly: disk 1, A → C (move 7).
The stack is empty → the loop ends. 7 = 2³ − 1 moves; every disk is on C.
void hanoi(int n) { Deque<Task> stack = new ArrayDeque<>(); stack.push(new Task(n, 'A', 'B', 'C')); while (!stack.isEmpty()) { Task t = stack.pop(); if (t.n() == 1) System.out.println(t.from() + " → " + t.to()); else { // reverse order stack.push(new Task(t.n()-1, t.via(), t.from(), t.to())); stack.push(new Task(1, t.from(), t.via(), t.to())); stack.push(new Task(t.n()-1, t.from(), t.to(), t.via())); } } }
Stock Span
The Stock Span Problem
- An array holds daily closing prices.
- The span of a day is the number of consecutive days, counting back from and including that day, whose price is less than or equal to that day's price.
- In financial analysis it shows “for how many days” a price has not been beaten.
| Day | Price | Span | Why? |
|---|---|---|---|
| 1 | 100 | 1 | first day |
| 4 | 70 | 2 | 60 ≤ 70, 80 > 70 |
| 6 | 75 | 4 | 60, 70, 60 ≤ 75; 80 > 75 |
| 7 | 85 | 6 | 75, 60, 70, 60, 80 ≤ 85 |
Prices [100, 80, 60, 70, 60, 75, 85] → spans [1, 1, 1, 2, 1, 4, 6]. The bold numbers under the bars are the spans.
Stock Span
Stock Span: An O(n) Solution with a Stack
int[] span(int[] prices) { int[] spans = new int[prices.length]; Deque<Integer> stack = new ArrayDeque<>(); stack.push(0); spans[0] = 1; for (int i = 1; i < prices.length; i++) { while (!stack.isEmpty() && prices[i] >= prices[stack.peek()]) stack.pop(); spans[i] = stack.isEmpty() ? i + 1 : i - stack.peek(); stack.push(i); } return spans; }
Stock Span · Step through the code
Code Trace: span({100, 80, 60, 70, 60, 75, 85})
First day: push index 0; spans[0] = 1 (no earlier days).
i = 1 (price 80): the top, day 0 (100), is higher → span = 1 − 0 = 1.
push(1): a possible boundary for later days.
i = 2 (price 60): the top, day 1 (80), is higher → span = 2 − 1 = 1.
push(2): a possible boundary for later days.
i = 3: 70 ≥ prices[2] = 60 → pop. Day 2 can no longer be anyone's boundary.
i = 3 (price 70): the top, day 1 (80), is higher → span = 3 − 1 = 2.
push(3): a possible boundary for later days.
i = 4 (price 60): the top, day 3 (70), is higher → span = 4 − 3 = 1.
push(4): a possible boundary for later days.
i = 5: 75 ≥ prices[4] = 60 → pop. Day 4 can no longer be anyone's boundary.
i = 5: 75 ≥ prices[3] = 70 → pop. Day 3 can no longer be anyone's boundary.
i = 5 (price 75): the top, day 1 (80), is higher → span = 5 − 1 = 4.
push(5): a possible boundary for later days.
i = 6: 85 ≥ prices[5] = 75 → pop. Day 5 can no longer be anyone's boundary.
i = 6: 85 ≥ prices[1] = 80 → pop. Day 1 can no longer be anyone's boundary.
i = 6 (price 85): the top, day 0 (100), is higher → span = 6 − 0 = 6.
push(6): a possible boundary for later days.
return [1, 1, 1, 2, 1, 4, 6]. Each index is pushed once and popped at most once → O(n).
int[] span(int[] prices) { int[] spans = new int[prices.length]; Deque<Integer> stack = new ArrayDeque<>(); stack.push(0); spans[0] = 1; for (int i = 1; i < prices.length; i++) { while (!stack.isEmpty() && prices[i] >= prices[stack.peek()]) stack.pop(); spans[i] = stack.isEmpty() ? i + 1 : i - stack.peek(); stack.push(i); } return spans; }
Expression Notation
Infix, Prefix, and Postfix Notation
| Notation | Where the operator goes | A * (B + C) / D |
|---|---|---|
| Infix | between the operands | A * ( B + C ) / D |
| Prefix · Polish notation | before the operands | / * A + B C D |
| Postfix · reverse Polish (RPN) | after the operands | A B C + * D / |
- Prefix and postfix need no parentheses and no precedence rules: a stack evaluates the expression in a single left-to-right (or right-to-left) pass.
- Compilers and interpreters usually translate expressions into postfix form (e.g., stack-based bytecode), and some calculators use RPN.
- They also appear in symbolic algebra and in work with expression trees.
* A / + B C D is wrong; it means A * ((B + C) / D). The correct prefix, which keeps the left-to-right order of operations, is / * A + B C D.Expression Notation
Precedence, Associativity, and Converting by Hand
| Operator | Precedence | Associativity |
|---|---|---|
^ (power) | 3 | right to left |
* / | 2 | left to right |
+ − | 1 | left to right |
Equal precedence groups left to right: A − B + C = (A − B) + C → postfix A B − C +.
- Parenthesize every operation:
((A * (B + C)) / D) - Postfix: move each operator to the right of its parentheses →
A B C + * D / - Prefix: move each operator to the left of its parentheses →
/ * A + B C D
Expression Notation
Infix → Postfix: The Algorithm
Read the input from left to right, holding operators and opening parentheses on a stack.
| Token read | Action |
|---|---|
| Operand (A, B, 5 …) | Write it straight to the output. |
( | Push it. |
) | Pop to the output until you reach (; discard the (. |
| Operator op | While the top is not ( and has precedence ≥ op, pop it to the output; then push op. |
| End of input | Pop every remaining operator to the output. |
The “≥” rule gives left-to-right associativity. For right-associative ^, pop only operators with precedence >. Time O(n).
Expression Notation
Infix → Postfix: Java
String infixToPostfix(String expr) {
StringBuilder out = new StringBuilder();
Deque<Character> stack = new ArrayDeque<>();
for (char c : expr.toCharArray()) {
if (Character.isLetterOrDigit(c)) out.append(c);
else if (c == '(') stack.push(c);
else if (c == ')') {
while (stack.peek() != '(')
out.append(stack.pop());
stack.pop(); // discard '('
} else { // operator
while (!stack.isEmpty() &&
prec(stack.peek()) >= prec(c))
out.append(stack.pop());
stack.push(c);
}
}
while (!stack.isEmpty()) out.append(stack.pop());
return out.toString();
}
int prec(char op) { switch (op) { case '+': case '-': return 1; case '*': case '/': return 2; default: return 0; // '(' } }
(; only ) discards it.Expression Notation · Step through the code (loop only)
Code Trace: infixToPostfix("A*(B+C)/D")
The input is read left to right. Operands go to the output; operators and '(' go on the stack.
A is an operand → straight to the output.
*: the stack is empty → push *.
( → always pushed. It shields the operators inside until it is closed.
B is an operand → straight to the output.
+: '(' is on top → push +.
C is an operand → straight to the output.
): pop until '(' → + goes to the output.
'(' is popped and discarded; parentheses never reach the output.
/: precedence of * on top (2) = precedence of / (2) → * goes to the output.
/: the stack is empty → push /.
D is an operand → straight to the output.
End of input: the remaining operator / goes to the output. Result: A B C + * D /
for (char c : expr.toCharArray()) { if (Character.isLetterOrDigit(c)) out.append(c); else if (c == '(') stack.push(c); else if (c == ')') { while (stack.peek() != '(') out.append(stack.pop()); stack.pop(); // discard '(' } else { // operator while (!stack.isEmpty() && prec(stack.peek()) >= prec(c)) out.append(stack.pop()); stack.push(c); } } while (!stack.isEmpty()) out.append(stack.pop());
Expression Notation · Step through the code (loop only)
Code Trace: infixToPostfix("A+B*C-D/E")
The input is read left to right. Operands go to the output; operators and '(' go on the stack.
A is an operand → straight to the output.
+: the stack is empty → push +.
B is an operand → straight to the output.
*: precedence of + on top (1) < 2 → push *.
C is an operand → straight to the output.
-: precedence of * on top (2) > precedence of - (1) → * goes to the output.
-: precedence of + on top (1) = precedence of - (1) → + goes to the output.
-: the stack is empty → push -.
D is an operand → straight to the output.
/: precedence of - on top (1) < 2 → push /.
E is an operand → straight to the output.
End of input: the remaining operator / goes to the output.
End of input: the remaining operator - goes to the output. Result: A B C * + D E / − = (A + B·C) − D/E
for (char c : expr.toCharArray()) { if (Character.isLetterOrDigit(c)) out.append(c); else if (c == '(') stack.push(c); else if (c == ')') { while (stack.peek() != '(') out.append(stack.pop()); stack.pop(); // discard '(' } else { // operator while (!stack.isEmpty() && prec(stack.peek()) >= prec(c)) out.append(stack.pop()); stack.push(c); } } while (!stack.isEmpty()) out.append(stack.pop());
Expression Notation
Evaluating a Postfix Expression
- Read left to right.
- Number → push it.
- Operator → pop twice, the right operand first and then the left; push the result.
- At the end, the one item left on the stack is the answer. No parentheses or precedence → O(n).
int apply(int left, char op, int right) { switch (op) { case '+': return left + right; case '-': return left - right; case '*': return left * right; default: return left / right; } }
int evaluatePostfix(String expr) { Deque<Integer> stack = new ArrayDeque<>(); for (char c : expr.toCharArray()) { if (Character.isDigit(c)) { stack.push(c - '0'); } else { int right = stack.pop(); int left = stack.pop(); stack.push(apply(left, c, right)); } } return stack.pop(); }
Assumes single-digit numbers, no spaces, and a valid expression.
Expression Notation · Step through the code
Code Trace: evaluatePostfix("412+*3/")
With A=4, B=1, C=2, D=3, A B C + * D / → "412+*3/". Create an empty operand stack.
4 is a digit → push c - '0' = 4.
1 is a digit → push c - '0' = 1.
2 is a digit → push c - '0' = 2.
+ is an operator: pop right = 2 first, then left = 1 (order matters for − and /).
apply(1, '+', 2) = 3 → push the result back.
* is an operator: pop right = 3 first, then left = 4 (order matters for − and /).
apply(4, '*', 3) = 12 → push the result back.
3 is a digit → push c - '0' = 3.
/ is an operator: pop right = 3 first, then left = 12 (order matters for − and /).
apply(12, '/', 3) = 4 → push the result back.
The expression is done; the single value on the stack is the answer: return 4. No parentheses or precedence needed → O(n).
int evaluatePostfix(String expr) { Deque<Integer> stack = new ArrayDeque<>(); for (char c : expr.toCharArray()) { if (Character.isDigit(c)) { stack.push(c - '0'); } else { int right = stack.pop(); int left = stack.pop(); stack.push(apply(left, c, right)); } } return stack.pop(); }
Expression Notation
Infix → Prefix Conversion
- Reverse the infix expression.
- Swap the parentheses:
(↔). - Run the postfix algorithm, but don't pop on equal precedence; pop only operators with precedence >.
- Reverse the output.
| Step | A * (B + C) / D |
|---|---|
| 1–2. Reverse, swap parentheses | D / ( C + B ) * A |
| 3. Postfix (no pop on equal) | D C B + A * / |
| 4. Reverse → prefix | / * A + B C D |
* arrives, / of equal precedence is on top and is not popped. After the final reversal this preserves left associativity: first A * (B + C), then / D.A + B * C − D / E → − + A * B C / D EExpression Notation · Step through the code
Code Trace: infixToPrefix("A*(B+C)/D")
main calls infixToPrefix("A*(B+C)/D").
1. The string is reversed. The parentheses flip too: ')' is now on the left, '(' on the right.
2. Swap ( ↔ ); now it is a valid infix expression: D / (C + B) * A.
3. The postfix algorithm, but without popping on equal precedence → p = D C B + A * /.
4. Reverse p → returns / * A + B C D.
main prints the result. Check: the operands are still in the order A, B, C, D.
String rev(String s) {
return new StringBuilder(s).reverse().toString();
}
String infixToPrefix(String expr) {
String t = rev(expr); // 1. reverse
t = swapParens(t); // 2. ( ↔ )
String p = postfixForPrefix(t); // 3. no pop on equal
return rev(p); // 4. reverse
}
// in main:
System.out.println(infixToPrefix("A*(B+C)/D"));
Expression Notation
Step 2: swapParens
Reversing also flips the direction of the parentheses. This method writes ) for every ( and ( for every ), leaving all other characters unchanged.
String swapParens(String s) {
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
if (c == '(') sb.append(')');
else if (c == ')') sb.append('(');
else sb.append(c);
}
return sb.toString();
}
One pass over the string: O(n) time, O(n) extra memory.
Expression Notation · Step through the code (loop only)
Code Trace: postfixForPrefix("D/(C+B)*A")
The input is read left to right. Operands go to the output; operators and '(' go on the stack.
D is an operand → straight to the output.
/: the stack is empty → push /.
( → always pushed. It shields the operators inside until it is closed.
C is an operand → straight to the output.
+: '(' is on top → push +.
B is an operand → straight to the output.
): pop until '(' → + goes to the output.
'(' is popped and discarded; parentheses never reach the output.
*: precedence of / on top (2) = 2, and we don't pop on equal → push *.
A is an operand → straight to the output.
End of input: the remaining operator * goes to the output.
End of input: the remaining operator / goes to the output. p = D C B + A * / → reversed: / * A + B C D
for (char c : expr.toCharArray()) { if (Character.isLetterOrDigit(c)) out.append(c); else if (c == '(') stack.push(c); else if (c == ')') { while (stack.peek() != '(') out.append(stack.pop()); stack.pop(); // discard '(' } else { // operator while (!stack.isEmpty() && prec(stack.peek()) > prec(c)) out.append(stack.pop()); stack.push(c); } } while (!stack.isEmpty()) out.append(stack.pop());
Summary
Common Mistakes
isEmpty() first; otherwise you get an exception or null, depending on the library. In bracket matching, "())" triggers exactly this bug.top is the index of the last item, so size = top + 1. The stack is full when top == capacity − 1.data[top--] = null keeps the object away from the garbage collector (a memory leak called loitering).5 2 − = 5 − 2 = 3, not 2 − 5.ArrayDeque goes top to bottom, but the legacy Stack goes bottom to top, so their output looks reversed.StackOverflowError occurs (the call stack overflows).Lab · Java code, step by step
Lab: Bracket Matching and Postfix
Summary
Summary: Problems Solved with a Stack
| Problem | What the stack holds | Time |
|---|---|---|
| push / pop / top / isEmpty / size | — | O(1) |
| Reversing a string | characters | O(n) |
| Bracket matching | unmatched opening brackets | O(n) |
| Next greater element | candidate values to the right (decreasing) | O(n) |
| Stock span | indices of days not yet beaten | O(n) |
| Towers of Hanoi | subtasks (n, source, aux., destination) | O(2n) |
| Infix → postfix / prefix | operators and ( | O(n) |
| Postfix evaluation | intermediate results | O(n) |
Min stack (getMin) | the current minimum | O(1) |
| Back / forward navigation | visited pages (two stacks) | O(1) |
The common pattern: when we care about “the most recent item that is still unresolved,” we reach for a stack (LIFO).
Summary
Interactive Simulators
Summary
Sample Problems
- Starting from an empty stack, we run push(5), push(3), pop(), push(8), push(1), pop(), pop(), push(4). What do the pop() calls return, and what is left on the stack?
- Which of
{[()()]}and{[(])}is balanced? In the other one, at which character is the error detected? - Convert
(A + B) * (C − D) / Eto postfix and to prefix. - What is the value of the postfix expression
5 1 2 + 4 * + 3 −? - Find the spans for the prices [10, 4, 5, 90, 120, 80].
- Find the next greater element of each item in {2, 5, 3, 1, 4}.
Show answers
- The pops return 3, 1, 8. The stack ends as (bottom to top) 5, 4.
{[()()]}is balanced.{[(])}: when]arrives,(is on top → unbalanced.- Postfix:
A B + C D − * E /· Prefix:/ * + A B − C D E - 1+2 = 3, 3·4 = 12, 5+12 = 17, 17−3 = 14.
- [1, 1, 2, 4, 5, 1]
- {5, −1, 4, 4, −1}.
Summary
Exercises
- We push 1, 2, 3, 4 in that order, with pops mixed in anywhere. Are the output orders
2 4 3 1and4 1 2 3possible? - Two stacks share one array (one grows from the front, the other from the back). When do they overflow?
- Sort a stack using a single auxiliary stack. What is the complexity?
- Use a stack to check whether a word is a palindrome.
- How would you check that HTML tags such as
<b><i>…</i></b>are properly nested? - If a full array grew by +10 instead of doubling, what would n pushes cost in total?
Hints / answers
2 4 3 1yes;4 1 2 3no (once 4 is out, 3 is on top).- top1 starts at −1 and top2 at N; full when
top1 + 1 == top2. - Pop x; move the auxiliary stack's items smaller than x back, then push x → O(n²).
- Push the letters, pop them to build the reversed string, and compare → O(n).
- The bracket algorithm: push each opening tag; on a closing tag, pop and compare names.
- The copies total 10 + 20 + 30 + … ≈ n²/20 → O(n²); O(n) per push.
Summary
Further Reading
- Goodrich, Tamassia, and Goldwasser, Data Structures and Algorithms in Java, 6th ed., 2014.
- Lafore, Data Structures and Algorithms in Java, 2nd ed., 2002.
- Oracle, Java SE 17 API documentation for
DequeandArrayDeque.