DSA Tutorial
🔍

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
Java
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.

Java
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.

Java
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
Java
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

PropertyFixed ArrayDynamic ArrayLinked List
PushO(1) — guaranteedO(1) amortizedO(1) — guaranteed
PopO(1)O(1)O(1)
PeekO(1)O(1)O(1)
Memory per elementValue onlyValue onlyValue + pointer (~8 bytes)
Max capacityFixed at creationUnlimited (auto-resize)Unlimited (per-node alloc)
Memory layoutContiguousContiguousScattered
Cache performanceExcellentExcellentPoor
Overflow riskYes — if capacity exceededNeverNever
Best forKnown capacity, max performanceGeneral useGuaranteed 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 = -1 for empty, topIndex = capacity - 1 for full
  • data[++topIndex] = value on 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:

  • head node 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: mainStack for data, minStack for 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.

Stack Implementation