Stack Implementation
What This Page Covers
The previous page covered individual operations. This page builds complete, production-quality stack classes from the ground up — two backend implementations and one advanced design pattern.
Implementation 1: Fixed-Size Array Stack Underlying structure: fixed-capacity array Teaches: topIndex tracking, overflow detection Use when: capacity is known in advance Implementation 2: Dynamic Array Stack Underlying structure: resizable array Teaches: automatic resizing, amortized cost Use when: general-purpose, unknown capacity Implementation 3: Linked List Stack Underlying structure: singly linked list Teaches: pointer-based push/pop, no capacity limit Use when: guaranteed O(1) per operation, no amortized Advanced Pattern: Min Stack Extends any stack to support getMin() in O(1) Teaches: auxiliary stack technique Use when: interview problem requiring O(1) minimum access
Implementation 1: Fixed-Size Array Stack
A fixed-size stack pre-allocates an array of known capacity. The topIndex pointer tracks where the top element lives. Push increments topIndex; pop decrements it.
Fixed array stack internals: capacity = 5 topIndex = -1 (empty state) After push(10): arr = [10, _, _, _, _] topIndex = 0 After push(20): arr = [10, 20, _, _, _] topIndex = 1 After push(30): arr = [10, 20, 30, _, _] topIndex = 2 peek(): return arr[topIndex] = arr[2] = 30 pop(): return arr[topIndex]; topIndex-- → returns 30, topIndex = 1 isEmpty: topIndex == -1 isFull: topIndex == capacity - 1
1public class FixedArrayStack<T> {
2
3 private Object[] data; // Underlying fixed array
4 private int topIndex; // Index of the current top element (-1 = empty)
5 private int capacity;
6
7 @SuppressWarnings("unchecked")
8 public FixedArrayStack(int capacity) {
9 this.capacity = capacity;
10 this.data = new Object[capacity];
11 this.topIndex = -1; // Empty stack — nothing at index 0 yet
12 }
13
14 // PUSH — O(1)
15 public void push(T value) {
16 if (isFull()) {
17 throw new StackOverflowError("Stack is full (capacity: " + capacity + ")");
18 }
19 data[++topIndex] = value; // Increment first, then store
20 }
21
22 // POP — O(1)
23 @SuppressWarnings("unchecked")
24 public T pop() {
25 if (isEmpty()) {
26 throw new java.util.EmptyStackException();
27 }
28 T value = (T) data[topIndex];
29 data[topIndex] = null; // Help garbage collection — clear the slot
30 topIndex--;
31 return value;
32 }
33
34 // PEEK — O(1)
35 @SuppressWarnings("unchecked")
36 public T peek() {
37 if (isEmpty()) {
38 throw new java.util.EmptyStackException();
39 }
40 return (T) data[topIndex];
41 }
42
43 public boolean isEmpty() { return topIndex == -1; }
44 public boolean isFull() { return topIndex == capacity - 1; }
45 public int size() { return topIndex + 1; }
46 public int capacity(){ return capacity; }
47
48 @Override
49 public String toString() {
50 if (isEmpty()) return "Stack: [] (empty)";
51 StringBuilder sb = new StringBuilder("Stack (top→bottom): [");
52 for (int i = topIndex; i >= 0; i--) {
53 sb.append(data[i]);
54 if (i > 0) sb.append(", ");
55 }
56 return sb.append("]").toString();
57 }
58
59 public static void main(String[] args) {
60 FixedArrayStack<Integer> stack = new FixedArrayStack<>(5);
61
62 stack.push(10); stack.push(20); stack.push(30);
63 System.out.println(stack); // [30, 20, 10]
64 System.out.println("Peek: " + stack.peek()); // 30
65 System.out.println("Size: " + stack.size()); // 3
66 System.out.println("Capacity: " + stack.capacity()); // 5
67 System.out.println("isFull: " + stack.isFull()); // false
68 System.out.println("Pop: " + stack.pop()); // 30
69 System.out.println(stack); // [20, 10]
70
71 // Fill to capacity
72 stack.push(40); stack.push(50); stack.push(60);
73 System.out.println("isFull: " + stack.isFull()); // true
74
75 // Overflow attempt
76 try {
77 stack.push(99);
78 } catch (StackOverflowError e) {
79 System.out.println("Overflow: " + e.getMessage());
80 }
81 }
82}Output:
Stack (top→bottom): [30, 20, 10]
Peek: 30
Size: 3
Capacity: 5
isFull: false
Pop: 30
Stack (top→bottom): [20, 10]
isFull: true
Overflow: Stack is full (capacity: 5)
Dry Run: Fixed Array Stack — topIndex Tracking
capacity=5, data=[_, _, _, _, _], topIndex=-1 push(10): topIndex = -1+1 = 0 → data[0]=10 → [10, _, _, _, _] push(20): topIndex = 0+1 = 1 → data[1]=20 → [10, 20, _, _, _] push(30): topIndex = 1+1 = 2 → data[2]=30 → [10, 20, 30, _, _] peek(): return data[topIndex] = data[2] = 30 ← topIndex unchanged pop(): value = data[2] = 30 data[2] = null ← clear the slot topIndex = 2-1 = 1 return 30 data = [10, 20, null, _, _], topIndex=1 isEmpty(): topIndex == -1 ? 1 == -1 → false isFull(): topIndex == 4 ? 1 == 4 → false size(): topIndex + 1 = 1 + 1 = 2
Implementation 2: Dynamic Array Stack
A dynamic array stack grows automatically when full — identical to how ArrayList and Python list work internally. The resize doubles the capacity and copies all elements.
1public class DynamicArrayStack<T> {
2
3 private Object[] data;
4 private int topIndex;
5 private int capacity;
6
7 private static final int INITIAL_CAPACITY = 4;
8
9 @SuppressWarnings("unchecked")
10 public DynamicArrayStack() {
11 capacity = INITIAL_CAPACITY;
12 data = new Object[capacity];
13 topIndex = -1;
14 }
15
16 // PUSH — O(1) amortized (may trigger resize)
17 public void push(T value) {
18 if (topIndex == capacity - 1) {
19 resize();
20 }
21 data[++topIndex] = value;
22 }
23
24 // Resize: double the capacity and copy all elements — O(n)
25 @SuppressWarnings("unchecked")
26 private void resize() {
27 int newCapacity = capacity * 2;
28 Object[] newData = new Object[newCapacity];
29
30 for (int i = 0; i <= topIndex; i++) {
31 newData[i] = data[i];
32 }
33
34 data = newData;
35 capacity = newCapacity;
36 System.out.println(" [Resize] capacity: " + (newCapacity / 2)
37 + " → " + newCapacity);
38 }
39
40 // POP — O(1)
41 @SuppressWarnings("unchecked")
42 public T pop() {
43 if (isEmpty()) throw new java.util.EmptyStackException();
44 T value = (T) data[topIndex];
45 data[topIndex] = null;
46 topIndex--;
47 return value;
48 }
49
50 // PEEK — O(1)
51 @SuppressWarnings("unchecked")
52 public T peek() {
53 if (isEmpty()) throw new java.util.EmptyStackException();
54 return (T) data[topIndex];
55 }
56
57 public boolean isEmpty() { return topIndex == -1; }
58 public int size() { return topIndex + 1; }
59 public int capacity() { return capacity; }
60
61 public static void main(String[] args) {
62 DynamicArrayStack<Integer> stack = new DynamicArrayStack<>();
63
64 System.out.println("Initial capacity: " + stack.capacity()); // 4
65
66 for (int i = 1; i <= 9; i++) {
67 stack.push(i * 10);
68 System.out.printf(" Pushed %2d | size=%d | capacity=%d%n",
69 i * 10, stack.size(), stack.capacity());
70 }
71 }
72}Output:
Initial capacity: 4
Pushed 10 | size=1 | capacity=4
Pushed 20 | size=2 | capacity=4
Pushed 30 | size=3 | capacity=4
Pushed 40 | size=4 | capacity=4
[Resize] capacity: 4 → 8
Pushed 50 | size=5 | capacity=8
Pushed 60 | size=6 | capacity=8
Pushed 70 | size=7 | capacity=8
Pushed 80 | size=8 | capacity=8
[Resize] capacity: 8 → 16
Pushed 90 | size=9 | capacity=16
Resize Analysis — Why Amortized O(1)
Resize occurs at: 4, 8, 16, 32, ... elements (doubling) Elements copied at each resize: 4, 8, 16, 32, ... For 9 pushes (as in the output above): 4 copies at resize 1 (cap 4→8) 8 copies at resize 2 (cap 8→16) Total extra copies: 4 + 8 = 12 For n pushes total: Extra copies = 1 + 2 + 4 + ... + n/2 (geometric series) ≈ n Total work = n pushes + n copies = O(n) Amortized per push = O(n) / n = O(1) Each resize is O(n) but resizes happen so infrequently that the cost is effectively invisible across n operations.
Implementation 3: Linked List Stack
A linked list stack uses a singly linked list. The head is the top — push prepends, pop removes the head.
1public class LinkedListStack<T> {
2
3 private static class Node<T> {
4 T data;
5 Node<T> next;
6 Node(T data, Node<T> next) {
7 this.data = data;
8 this.next = next;
9 }
10 }
11
12 private Node<T> head; // top of stack
13 private int size;
14
15 public LinkedListStack() {
16 head = null;
17 size = 0;
18 }
19
20 // PUSH — prepend to front — O(1), always
21 public void push(T value) {
22 head = new Node<>(value, head); // new node → old head
23 size++;
24 }
25
26 // POP — remove front node — O(1)
27 public T pop() {
28 if (isEmpty()) throw new java.util.EmptyStackException();
29 T value = head.data;
30 head = head.next;
31 size--;
32 return value;
33 }
34
35 // PEEK — read front without removing — O(1)
36 public T peek() {
37 if (isEmpty()) throw new java.util.EmptyStackException();
38 return head.data;
39 }
40
41 public boolean isEmpty() { return head == null; }
42 public int size() { return size; }
43
44 // CLEAR — O(1) GC handles the rest
45 public void clear() { head = null; size = 0; }
46
47 @Override
48 public String toString() {
49 if (isEmpty()) return "Stack: [] (empty)";
50 StringBuilder sb = new StringBuilder("Stack (top→bottom): [");
51 Node<T> curr = head;
52 while (curr != null) {
53 sb.append(curr.data);
54 if (curr.next != null) sb.append(", ");
55 curr = curr.next;
56 }
57 return sb.append("]").toString();
58 }
59
60 public static void main(String[] args) {
61 LinkedListStack<String> stack = new LinkedListStack<>();
62
63 stack.push("A"); stack.push("B"); stack.push("C");
64 System.out.println(stack); // [C, B, A]
65 System.out.println("Peek: " + stack.peek()); // C
66 System.out.println("Pop: " + stack.pop()); // C
67 System.out.println("Pop: " + stack.pop()); // B
68 System.out.println(stack); // [A]
69 System.out.println("Size: " + stack.size()); // 1
70 stack.clear();
71 System.out.println("After clear: " + stack); // [] (empty)
72 }
73}Output:
Stack (top→bottom): [C, B, A]
Peek: C
Pop: C
Pop: B
Stack (top→bottom): [A]
After clear: Stack: [] (empty)
Advanced Pattern: Min Stack — O(1) getMin()
Problem: Design a stack that supports push, pop, peek, and getMin() in O(1) time and O(1) extra space per operation.
Insight: Maintain an auxiliary minStack that tracks the current minimum at every level of the main stack. When pushing onto the main stack, also push the current minimum (the smaller of the new value and the previous minimum) onto the min stack. When popping from the main stack, also pop from the min stack.
Push sequence: 5, 3, 7, 2, 8 Main stack: 5 3 7 2 8 ← top Min stack: 5 3 3 2 2 ← tracks min AT THAT LEVEL getMin() = top of minStack = 2 (always O(1)) Pop 8: main=[5,3,7,2], min=[5,3,3,2] getMin()=2 Pop 2: main=[5,3,7], min=[5,3,3] getMin()=3 Pop 7: main=[5,3], min=[5,3] getMin()=3 Pop 3: main=[5], min=[5] getMin()=5
1import java.util.Deque;
2import java.util.ArrayDeque;
3
4public class MinStack {
5
6 private Deque<Integer> mainStack;
7 private Deque<Integer> minStack; // Auxiliary: tracks min at each level
8
9 public MinStack() {
10 mainStack = new ArrayDeque<>();
11 minStack = new ArrayDeque<>();
12 }
13
14 // PUSH — push to both stacks — O(1)
15 public void push(int value) {
16 mainStack.push(value);
17 // Push the new minimum: smaller of value or current min
18 if (minStack.isEmpty()) {
19 minStack.push(value);
20 } else {
21 minStack.push(Math.min(value, minStack.peek()));
22 }
23 }
24
25 // POP — pop from both stacks — O(1)
26 public int pop() {
27 if (mainStack.isEmpty()) throw new java.util.EmptyStackException();
28 minStack.pop(); // Keep in sync with main stack
29 return mainStack.pop();
30 }
31
32 // PEEK — top of main stack — O(1)
33 public int peek() {
34 if (mainStack.isEmpty()) throw new java.util.EmptyStackException();
35 return mainStack.peek();
36 }
37
38 // GETMIN — top of min stack — O(1)
39 public int getMin() {
40 if (minStack.isEmpty()) throw new java.util.EmptyStackException();
41 return minStack.peek();
42 }
43
44 public boolean isEmpty() { return mainStack.isEmpty(); }
45 public int size() { return mainStack.size(); }
46
47 public static void main(String[] args) {
48 MinStack ms = new MinStack();
49
50 ms.push(5);
51 System.out.println("Push 5 → min=" + ms.getMin()); // 5
52
53 ms.push(3);
54 System.out.println("Push 3 → min=" + ms.getMin()); // 3
55
56 ms.push(7);
57 System.out.println("Push 7 → min=" + ms.getMin()); // 3
58
59 ms.push(2);
60 System.out.println("Push 2 → min=" + ms.getMin()); // 2
61
62 ms.push(8);
63 System.out.println("Push 8 → min=" + ms.getMin()); // 2
64
65 System.out.println("Pop: " + ms.pop() + " → min=" + ms.getMin()); // pop 8, min=2
66 System.out.println("Pop: " + ms.pop() + " → min=" + ms.getMin()); // pop 2, min=3
67 System.out.println("Pop: " + ms.pop() + " → min=" + ms.getMin()); // pop 7, min=3
68 System.out.println("Pop: " + ms.pop() + " → min=" + ms.getMin()); // pop 3, min=5
69 }
70}Output:
Push 5 → min=5
Push 3 → min=3
Push 7 → min=3
Push 2 → min=2
Push 8 → min=2
Pop: 8 → min=2
Pop: 2 → min=3
Pop: 7 → min=3
Pop: 3 → min=5
Min Stack Dry Run
Operations: push(5), push(3), push(7), push(2), push(8) After each push: push(5): main=[5], mins=[5] getMin()=5 push(3): main=[5,3], mins=[5,3] getMin()=3 (3 < 5) push(7): main=[5,3,7], mins=[5,3,3] getMin()=3 (7 > 3, keep 3) push(2): main=[5,3,7,2], mins=[5,3,3,2] getMin()=2 (2 < 3) push(8): main=[5,3,7,2,8],mins=[5,3,3,2,2] getMin()=2 (8 > 2, keep 2) After pop(): pop: remove 8 from main, remove 2 from mins main=[5,3,7,2], mins=[5,3,3,2] getMin()=2 After pop(): pop: remove 2 from main, remove 2 from mins main=[5,3,7], mins=[5,3,3] getMin()=3 Key insight: mins[i] always stores "what is the minimum of all elements from the bottom up to level i?" When we pop level i, that minimum is no longer valid, so we pop it from minStack too — restoring the previous level's min.
Implementation Comparison
| Property | Fixed Array | Dynamic Array | Linked List |
|---|---|---|---|
| Push | O(1) — guaranteed | O(1) amortized | O(1) — guaranteed |
| Pop | O(1) | O(1) | O(1) |
| Peek | O(1) | O(1) | O(1) |
| Memory per element | Value only | Value only | Value + pointer (~8 bytes) |
| Max capacity | Fixed at creation | Unlimited (auto-resize) | Unlimited (per-node alloc) |
| Memory layout | Contiguous | Contiguous | Scattered |
| Cache performance | Excellent | Excellent | Poor |
| Overflow risk | Yes — if capacity exceeded | Never | Never |
| Best for | Known capacity, max performance | General use | Guaranteed O(1), no resizing |
Common Mistakes in Implementation
Forgetting to null out the slot on pop in array stack. After popping, the old slot still holds a reference to the object. In Java, set data[topIndex+1] = null after decrementing topIndex. Without this, the object cannot be garbage collected even though it is "logically" gone — a memory leak for long-running stacks.
Using ++topIndex vs topIndex++ in array push. data[++topIndex] = value increments first, then writes — correct. data[topIndex++] = value writes at the old index, then increments — writes one slot too early if topIndex started at -1 (empty state). The increment-then-write pattern ++topIndex is correct for 0-indexed arrays with -1 as the empty sentinel.
Not protecting topIndex in concurrent environments. In multithreaded code, topIndex++ is not atomic — a race condition between two threads can corrupt the index. Use synchronized methods in Java, threading.Lock in Python, std::mutex in C++, or accept that single-threaded stack implementations are not thread-safe.
In Min Stack — not popping from minStack on every pop. The main stack and min stack must always be the same size. If you forget to pop minStack when popping mainStack, getMin() will return a stale minimum from a previous state. Every main pop must be paired with a min pop.
Interview Questions
Q: What is the difference between a fixed-size and dynamic-size stack implementation?
A fixed-size stack pre-allocates a specific number of slots and throws StackOverflowError if capacity is exceeded. It guarantees O(1) push with no amortization — every push is exactly O(1). A dynamic stack resizes when full, copying all elements to a new larger array. Individual pushes that trigger resize are O(n), but the amortized cost across n pushes is O(1). Dynamic stacks never overflow (barring system memory limits). Fixed stacks are faster and simpler when capacity is known in advance.
Q: How does the Min Stack pattern extend to Max Stack or Median Stack?
For Max Stack: maintain an auxiliary maxStack instead — push max(value, maxStack.peek()) on each push. For Median Stack, the problem is significantly harder — it requires two heaps (a max-heap for the lower half and a min-heap for the upper half), rebalancing on every push and pop. The O(1) min/max pattern using auxiliary stacks only works because min and max are single-value properties that follow the stack's LIFO structure.
Q: Why does the Linked List Stack use the head as the top?
Because pushing to the head is O(1) — create a new node, point it to the current head, update head. Pushing to the tail would require traversing the entire list to find the tail — O(n). By making the head the top, both push and pop are always O(1) with no traversal. This is the standard design for all linked-list-backed stacks.
Quick Quiz
Question 1: In the fixed array implementation, what is topIndex when the stack is empty?
- ›A) 0 — points to the first slot
- ›B) -1 — no valid element exists
- ›C) capacity - 1 — at the top
- ›D) null — no index
Answer: B) -1. The empty sentinel is -1 because array indices start at 0. When the stack is empty, there is no valid top element. The first push sets data[++topIndex] = data[0], making topIndex = 0 the single element.
Question 2: For the Min Stack pattern, after pushing [5, 3, 7, 2], what does the auxiliary minStack contain?
- ›A) [5, 3, 7, 2]
- ›B) [2, 2, 2, 2]
- ›C) [5, 3, 3, 2]
- ›D) [5, 3, 7, 2] reversed
Answer: C) [5, 3, 3, 2]. Each position stores the minimum of all elements from the bottom up to that level. Level 0: min(5) = 5. Level 1: min(5,3) = 3. Level 2: min(3,7) = 3. Level 3: min(3,2) = 2. This ensures getMin() is always O(1).
Question 3: Why must you null out array slots after pop in a Java array stack?
- ›A) To reset the array for reuse
- ›B) To prevent memory leaks — orphaned references keep objects from being GC'd
- ›C) To prevent reading stale values with peek
- ›D) Arrays require null values at the boundary
Answer: B) Memory leak prevention. The array still holds a reference to the popped object even though topIndex no longer includes it. The JVM garbage collector cannot free the object as long as the reference exists. Setting data[topIndex + 1] = null removes the reference so GC can reclaim the memory.
Question 4: In the dynamic array implementation, resize is triggered when topIndex == capacity - 1. What happens next?
- ›A) The push fails with StackOverflowError
- ›B) A new array of double the capacity is allocated, all elements are copied, then the new element is pushed
- ›C) Only the new element is added without copying
- ›D) The capacity grows by 1 to fit exactly the new element
Answer: B) Double, copy, push. The resize method allocates a new array of capacity * 2, copies all existing elements from index 0 to topIndex, updates data and capacity, then the push proceeds normally. This doubling strategy gives O(1) amortized push because the total copy work across all resizes is O(n) for n pushes.
Summary
Three complete stack implementations each with distinct tradeoffs:
Fixed Array Stack:
- ›
topIndex = -1for empty,topIndex = capacity - 1for full - ›
data[++topIndex] = valueon push,data[topIndex--]on pop - ›
isFull()check before push,isEmpty()check before pop/peek - ›Fastest — zero overhead when below capacity
Dynamic Array Stack:
- ›Same as fixed, but resize doubles capacity and copies on overflow
- ›O(1) amortized push — occasional O(n) resize absorbed across n pushes
- ›Geometric series proof: total copies ≈ n, amortized per push = O(1)
- ›General-purpose — no capacity limit
Linked List Stack:
- ›
headnode is the top; push prepends, pop removes head - ›Always O(1) — no amortized, no resize, no overflow
- ›Pointer overhead per node; scattered memory hurts cache
Min Stack Pattern:
- ›Two stacks:
mainStackfor data,minStackfor running minimum - ›Each push: push value to main, push
min(value, minStack.top())to minStack - ›Each pop: pop both stacks in sync
- ›
getMin()=minStack.peek()— always O(1) - ›Main stack and min stack are always the same size — mandatory invariant
In the next topic, you will explore Stack Patterns — the core interview algorithms: balanced parentheses, evaluate postfix expressions, next greater element, and the monotonic stack.