Stack model
The stack abstract data type: last-in first-out order, the operations push, pop, peek and isEmpty, what overflow and underflow mean, and the difference between fixed and resizable stacks.
Learning goals
- State the LIFO rule and the stack operations
- Explain underflow and overflow
- Distinguish fixed-capacity from resizable stacks
Prerequisites
LIFO
A stack is a collection with one open end, the top. The element added most recently is the first to be removed: last in, first out (LIFO). Think of a stack of plates. This order matches many natural problems: undoing the latest action, matching the most recent open bracket, returning from the innermost function call.
Only the top element is ever accessible, which is what makes every operation O(1).
| Sequence of operations | Stack after (bottom to top) | Returned |
|---|---|---|
| push 1, push 2, push 3 | 1, 2, 3 | - |
| pop | 1, 2 | 3 |
| push 4 | 1, 2, 4 | - |
| pop, pop | 1 | 4, then 2 |
Abstract data type
As an abstract data type a stack is defined by its operations and their behaviour, not by its storage: push(x), pop(), peek(), isEmpty() and usually size(). The same interface can be implemented with an array, a linked list or a dynamic array, and callers should not care which. In Java code you declare Deque<Integer> stack = new ArrayDeque<>(); and use only the stack operations.
The next lesson shows the implementations.
1interface IntStack {2void push(int x);3int pop(); // removes and returns the top4int peek(); // returns the top without removing it5boolean isEmpty();6int size();7}
Push
push(x) places x on top of the stack in O(1): write it at index size and increment the size (array), or make it the new head (linked list). With a fixed-capacity array it must first check for overflow.
Push values in the animation and watch the top move.
1Deque<Integer> stack = new ArrayDeque<>();2stack.push(10); // addFirst: the top is the front of the deque3stack.push(20);4System.out.println(stack); // [20, 10]: the top comes first when printed
Array-backed stack: push, pop, peek, overflow and underflow
Interactive · edit the inputdata[] · capacity 3 · size 0
An empty stack backed by an array of 3. The top is data[size - 1]; push and pop only touch the end, so both are O(1).
1class ArrayStack {2private int[] data; private int size;3ArrayStack(int capacity) { data = new int[capacity]; }4void push(int x) {5if (size == data.length) data = Arrays.copyOf(data, data.length * 2); // resizable; a fixed stack rejects instead6data[size++] = x;7}8int pop() {9if (size == 0) throw new NoSuchElementException("stack underflow");10return data[--size];11}12int peek() {13if (size == 0) throw new NoSuchElementException("stack underflow");14return data[size - 1]; // look without removing15}16boolean isEmpty() { return size == 0; }17}
Live state
size
0
capacity
3
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 capacity=3 | An empty stack backed by an array of 3. The top is data[size - 1]; push and pop only touch the end, so both are O(1). |
| 2 | L6 | op=push(5) size=1 top=5 | data[size++] = 5: 5 is the new top. |
| 3 | L6 | op=push(8) size=2 top=8 | data[size++] = 8: 8 is the new top. |
| 4 | L6 | op=push(3) size=3 top=3 | data[size++] = 3: 3 is the new top. |
| 5 | L14 | op=peek() top=3 size=3 | peek() returns 3 without removing it: size stays 3. |
| 6 | L10 | op=pop() returned=3 size=2 | Return data[--size] = 3: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 7 | L10 | op=pop() returned=8 size=1 | Return data[--size] = 8: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 8 | L10 | op=pop() returned=5 size=0 | Return data[--size] = 5: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 9 | L9 | op=pop() size=0 errors=1 | pop() on an empty stack: underflow. Java's ArrayDeque.pop throws NoSuchElementException; poll() would return null. |
| 10 | L6 | op=push(7) size=1 top=7 | data[size++] = 7: 7 is the new top. |
| 11 | L16 | size=1 errors=1 return=[7] | Final stack (bottom to top): [7]; 1 rejected operation. |
Pop
pop() removes and returns the top element in O(1): decrement the size and return the element at that index. The vacated slot can be ignored (or set to null for object stacks so the garbage collector can reclaim the object). Pop on an empty stack is an error.
Pop returns values in reverse order of pushing.
1Deque<Integer> stack = new ArrayDeque<>(List.of(3, 2, 1)); // top is 32int a = stack.pop(); // 33int b = stack.pop(); // 2
Array-backed stack: push, pop, peek, overflow and underflow
Interactive · edit the inputdata[] · capacity 3 · size 0
An empty stack backed by an array of 3. The top is data[size - 1]; push and pop only touch the end, so both are O(1).
1class ArrayStack {2private int[] data; private int size;3ArrayStack(int capacity) { data = new int[capacity]; }4void push(int x) {5if (size == data.length) data = Arrays.copyOf(data, data.length * 2); // resizable; a fixed stack rejects instead6data[size++] = x;7}8int pop() {9if (size == 0) throw new NoSuchElementException("stack underflow");10return data[--size];11}12int peek() {13if (size == 0) throw new NoSuchElementException("stack underflow");14return data[size - 1]; // look without removing15}16boolean isEmpty() { return size == 0; }17}
Live state
size
0
capacity
3
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 capacity=3 | An empty stack backed by an array of 3. The top is data[size - 1]; push and pop only touch the end, so both are O(1). |
| 2 | L6 | op=push(5) size=1 top=5 | data[size++] = 5: 5 is the new top. |
| 3 | L6 | op=push(8) size=2 top=8 | data[size++] = 8: 8 is the new top. |
| 4 | L6 | op=push(3) size=3 top=3 | data[size++] = 3: 3 is the new top. |
| 5 | L14 | op=peek() top=3 size=3 | peek() returns 3 without removing it: size stays 3. |
| 6 | L10 | op=pop() returned=3 size=2 | Return data[--size] = 3: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 7 | L10 | op=pop() returned=8 size=1 | Return data[--size] = 8: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 8 | L10 | op=pop() returned=5 size=0 | Return data[--size] = 5: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 9 | L9 | op=pop() size=0 errors=1 | pop() on an empty stack: underflow. Java's ArrayDeque.pop throws NoSuchElementException; poll() would return null. |
| 10 | L6 | op=push(7) size=1 top=7 | data[size++] = 7: 7 is the new top. |
| 11 | L16 | size=1 errors=1 return=[7] | Final stack (bottom to top): [7]; 1 rejected operation. |
Peek
peek() (also called top()) returns the top element without removing it, O(1). It lets you inspect before deciding to pop, as in monotonic stack algorithms. On an empty stack Deque.peek() returns null, whereas ArrayDeque.pop() throws.
Use -1 in the operations field of the animation to peek.
1Deque<Integer> stack = new ArrayDeque<>();2Integer none = stack.peek(); // null when empty3stack.push(7);4int top = stack.peek(); // 7, still on the stack
Array-backed stack: push, pop, peek, overflow and underflow
Interactive · edit the inputdata[] · capacity 3 · size 0
An empty stack backed by an array of 3. The top is data[size - 1]; push and pop only touch the end, so both are O(1).
1class ArrayStack {2private int[] data; private int size;3ArrayStack(int capacity) { data = new int[capacity]; }4void push(int x) {5if (size == data.length) data = Arrays.copyOf(data, data.length * 2); // resizable; a fixed stack rejects instead6data[size++] = x;7}8int pop() {9if (size == 0) throw new NoSuchElementException("stack underflow");10return data[--size];11}12int peek() {13if (size == 0) throw new NoSuchElementException("stack underflow");14return data[size - 1]; // look without removing15}16boolean isEmpty() { return size == 0; }17}
Live state
size
0
capacity
3
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 capacity=3 | An empty stack backed by an array of 3. The top is data[size - 1]; push and pop only touch the end, so both are O(1). |
| 2 | L6 | op=push(5) size=1 top=5 | data[size++] = 5: 5 is the new top. |
| 3 | L6 | op=push(8) size=2 top=8 | data[size++] = 8: 8 is the new top. |
| 4 | L6 | op=push(3) size=3 top=3 | data[size++] = 3: 3 is the new top. |
| 5 | L14 | op=peek() top=3 size=3 | peek() returns 3 without removing it: size stays 3. |
| 6 | L10 | op=pop() returned=3 size=2 | Return data[--size] = 3: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 7 | L10 | op=pop() returned=8 size=1 | Return data[--size] = 8: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 8 | L10 | op=pop() returned=5 size=0 | Return data[--size] = 5: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 9 | L9 | op=pop() size=0 errors=1 | pop() on an empty stack: underflow. Java's ArrayDeque.pop throws NoSuchElementException; poll() would return null. |
| 10 | L6 | op=push(7) size=1 top=7 | data[size++] = 7: 7 is the new top. |
| 11 | L16 | size=1 errors=1 return=[7] | Final stack (bottom to top): [7]; 1 rejected operation. |
IsEmpty
isEmpty() reports whether the stack has no elements, O(1). Check it before pop or peek unless the algorithm guarantees the stack is non-empty (for example after pushing a sentinel). Prefer isEmpty() over size() == 0 for clarity.
Many stack loops have the form while (!stack.isEmpty() && condition).
1Deque<Integer> stack = new ArrayDeque<>();2while (!stack.isEmpty() && stack.peek() < x) stack.pop(); // short-circuit: peek is only called when non-empty
Underflow
Underflow is popping or peeking an empty stack. ArrayDeque.pop() throws NoSuchElementException, poll() and peek() return null instead, and the legacy Stack throws EmptyStackException. Handle it by checking isEmpty() first, or by returning a sentinel. Underflow in an expression parser usually means the input was malformed.
Try the Underflow preset in the animation.
1Deque<Integer> stack = new ArrayDeque<>();2try { stack.pop(); } catch (NoSuchElementException e) { System.out.println("underflow"); }3Integer safe = stack.pollFirst(); // null instead of an exception
Array-backed stack: push, pop, peek, overflow and underflow
Interactive · edit the inputdata[] · capacity 3 · size 0
An empty stack backed by an array of 3. The top is data[size - 1]; push and pop only touch the end, so both are O(1).
1class ArrayStack {2private int[] data; private int size;3ArrayStack(int capacity) { data = new int[capacity]; }4void push(int x) {5if (size == data.length) data = Arrays.copyOf(data, data.length * 2); // resizable; a fixed stack rejects instead6data[size++] = x;7}8int pop() {9if (size == 0) throw new NoSuchElementException("stack underflow");10return data[--size];11}12int peek() {13if (size == 0) throw new NoSuchElementException("stack underflow");14return data[size - 1]; // look without removing15}16boolean isEmpty() { return size == 0; }17}
Live state
size
0
capacity
3
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 capacity=3 | An empty stack backed by an array of 3. The top is data[size - 1]; push and pop only touch the end, so both are O(1). |
| 2 | L6 | op=push(5) size=1 top=5 | data[size++] = 5: 5 is the new top. |
| 3 | L6 | op=push(8) size=2 top=8 | data[size++] = 8: 8 is the new top. |
| 4 | L6 | op=push(3) size=3 top=3 | data[size++] = 3: 3 is the new top. |
| 5 | L14 | op=peek() top=3 size=3 | peek() returns 3 without removing it: size stays 3. |
| 6 | L10 | op=pop() returned=3 size=2 | Return data[--size] = 3: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 7 | L10 | op=pop() returned=8 size=1 | Return data[--size] = 8: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 8 | L10 | op=pop() returned=5 size=0 | Return data[--size] = 5: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 9 | L9 | op=pop() size=0 errors=1 | pop() on an empty stack: underflow. Java's ArrayDeque.pop throws NoSuchElementException; poll() would return null. |
| 10 | L6 | op=push(7) size=1 top=7 | data[size++] = 7: 7 is the new top. |
| 11 | L16 | size=1 errors=1 return=[7] | Final stack (bottom to top): [7]; 1 rejected operation. |
Fixed vs resizable stack
A fixed-capacity stack has an array of constant size and rejects a push when full (overflow). A resizable stack allocates a larger array (typically double) and copies, so pushes cost amortised O(1). Fixed stacks give predictable memory, which suits embedded code and recursion limits; resizable ones are the default in libraries. Java's call stack is a fixed-size stack, and overflowing it throws StackOverflowError.
Switch the animation between the two policies and push past the capacity.
| Fixed capacity | Resizable | |
|---|---|---|
| When full | reject or throw | allocate a bigger array and copy |
| push cost | O(1) | amortised O(1), O(n) on a resize |
| Memory | predictable | up to 2× the used size |
Array-backed stack: push, pop, peek, overflow and underflow
Interactive · edit the inputdata[] · capacity 3 · size 0
An empty stack backed by an array of 3. The top is data[size - 1]; push and pop only touch the end, so both are O(1).
1class ArrayStack {2private int[] data; private int size;3ArrayStack(int capacity) { data = new int[capacity]; }4void push(int x) {5if (size == data.length) data = Arrays.copyOf(data, data.length * 2); // resizable; a fixed stack rejects instead6data[size++] = x;7}8int pop() {9if (size == 0) throw new NoSuchElementException("stack underflow");10return data[--size];11}12int peek() {13if (size == 0) throw new NoSuchElementException("stack underflow");14return data[size - 1]; // look without removing15}16boolean isEmpty() { return size == 0; }17}
Live state
size
0
capacity
3
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 capacity=3 | An empty stack backed by an array of 3. The top is data[size - 1]; push and pop only touch the end, so both are O(1). |
| 2 | L6 | op=push(5) size=1 top=5 | data[size++] = 5: 5 is the new top. |
| 3 | L6 | op=push(8) size=2 top=8 | data[size++] = 8: 8 is the new top. |
| 4 | L6 | op=push(3) size=3 top=3 | data[size++] = 3: 3 is the new top. |
| 5 | L14 | op=peek() top=3 size=3 | peek() returns 3 without removing it: size stays 3. |
| 6 | L10 | op=pop() returned=3 size=2 | Return data[--size] = 3: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 7 | L10 | op=pop() returned=8 size=1 | Return data[--size] = 8: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 8 | L10 | op=pop() returned=5 size=0 | Return data[--size] = 5: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 9 | L9 | op=pop() size=0 errors=1 | pop() on an empty stack: underflow. Java's ArrayDeque.pop throws NoSuchElementException; poll() would return null. |
| 10 | L6 | op=push(7) size=1 top=7 | data[size++] = 7: 7 is the new top. |
| 11 | L16 | size=1 errors=1 return=[7] | Final stack (bottom to top): [7]; 1 rejected operation. |
Practice ladder
Now try it yourself
Easy
- Show the stack after push 1, push 2, pop, push 3
- What does ArrayDeque.pop() do on an empty deque?
Medium
- Why does Deque.push add at the front?
- Explain why a resizable stack still has O(1) amortised push
Advanced
- Design a stack that supports a maximum size and returns false from push when full
Key takeaways
- A stack is LIFO: push, pop, peek and isEmpty are all O(1).
- Underflow (pop on empty) and overflow (push on full) must be handled.
- Fixed stacks reject when full; resizable stacks double.