DSA Tutorial
🔍

Stack Operations

Operations Overview

A stack exposes a small, well-defined set of operations. Every interview problem that uses a stack only calls these operations — understanding them precisely is the foundation.

Core operations (must know):
  push(value)   → add value to the top            O(1)
  pop()         → remove and return the top value  O(1)
  peek()        → read top value without removing  O(1)
  isEmpty()     → true if the stack has no elements O(1)

Supporting operations:
  size()        → number of elements in the stack  O(1)
  clear()       → remove all elements              O(n) or O(1)
  search(value) → distance from top to value       O(n)

All core operations are O(1). This is the defining property of a stack — no matter how many elements it holds, accessing the top is always one step.

Implementation 1: Array-Backed Stack

An array-backed stack uses a dynamic array (ArrayList, Python list, C++ vector, JavaScript Array) as the underlying storage. The top of the stack is always the last element of the array.

Array-backed stack state:

  Internal array: [10, 20, 30, ___ , ___]
                    0   1   2   3     4
                            ↑
                          topIndex = 2

  Push 40:  array[3] = 40, topIndex = 3
  Pop:      return array[3], topIndex = 2
  Peek:     return array[topIndex] = array[2] = 30
Java
1import java.util.ArrayList; 2import java.util.EmptyStackException; 3 4public class ArrayStack<T> { 5 6 private ArrayList<T> data; 7 8 public ArrayStack() { 9 data = new ArrayList<>(); 10 } 11 12 // PUSH — add to top (end of array) — O(1) amortized 13 public void push(T value) { 14 data.add(value); 15 } 16 17 // POP — remove and return top — O(1) 18 public T pop() { 19 if (isEmpty()) { 20 throw new EmptyStackException(); 21 } 22 return data.remove(data.size() - 1); 23 } 24 25 // PEEK — read top without removing — O(1) 26 public T peek() { 27 if (isEmpty()) { 28 throw new EmptyStackException(); 29 } 30 return data.get(data.size() - 1); 31 } 32 33 // ISEMPTY — O(1) 34 public boolean isEmpty() { 35 return data.isEmpty(); 36 } 37 38 // SIZE — O(1) 39 public int size() { 40 return data.size(); 41 } 42 43 // CLEAR — remove all elements — O(1) (just drop the list reference) 44 public void clear() { 45 data.clear(); 46 } 47 48 // SEARCH — distance of value from top (1-indexed) — O(n) 49 // Returns -1 if not found 50 public int search(T value) { 51 for (int i = data.size() - 1; i >= 0; i--) { 52 if (data.get(i).equals(value)) { 53 return data.size() - i; // 1 = top, 2 = second from top, ... 54 } 55 } 56 return -1; 57 } 58 59 @Override 60 public String toString() { 61 if (isEmpty()) return "Stack: [] (empty)"; 62 StringBuilder sb = new StringBuilder("Stack (top→bottom): ["); 63 for (int i = data.size() - 1; i >= 0; i--) { 64 sb.append(data.get(i)); 65 if (i > 0) sb.append(", "); 66 } 67 sb.append("]"); 68 return sb.toString(); 69 } 70 71 public static void main(String[] args) { 72 ArrayStack<Integer> stack = new ArrayStack<>(); 73 74 stack.push(10); 75 stack.push(20); 76 stack.push(30); 77 System.out.println(stack); // [30, 20, 10] 78 System.out.println("Peek: " + stack.peek()); // 30 79 System.out.println("Size: " + stack.size()); // 3 80 System.out.println("Pop: " + stack.pop()); // 30 81 System.out.println("Pop: " + stack.pop()); // 20 82 System.out.println(stack); // [10] 83 System.out.println("Search 10: " + stack.search(10)); // 1 (at top) 84 System.out.println("isEmpty: " + stack.isEmpty()); // false 85 stack.clear(); 86 System.out.println("After clear: " + stack.isEmpty()); // true 87 } 88}
Output:
Stack (top→bottom): [30, 20, 10]
Peek:   30
Size:   3
Pop:    30
Pop:    20
Stack (top→bottom): [10]
Search 10: 1
isEmpty: false
After clear isEmpty: true

Implementation 2: Linked-List-Backed Stack

A linked-list-backed stack uses a linked list as the underlying storage. The head of the list is the top of the stack. Push prepends a new node; pop removes the head node.

Linked-list stack state:

  head → [30] → [20] → [10] → null
          ↑ top

  Push 40:  new node(40) → [40] → [30] → [20] → [10] → null
                            head
  Pop:      remove head, return 40, head = node(30)
  Peek:     return head.data = 30
Java
1import java.util.EmptyStackException; 2 3public class LinkedStack<T> { 4 5 private static class Node<T> { 6 T data; 7 Node<T> next; 8 Node(T data) { this.data = data; this.next = null; } 9 } 10 11 private Node<T> head; // top of stack 12 private int size; 13 14 public LinkedStack() { 15 head = null; 16 size = 0; 17 } 18 19 // PUSH — prepend new node — O(1) 20 public void push(T value) { 21 Node<T> newNode = new Node<>(value); 22 newNode.next = head; 23 head = newNode; 24 size++; 25 } 26 27 // POP — remove head node — O(1) 28 public T pop() { 29 if (isEmpty()) throw new EmptyStackException(); 30 T value = head.data; 31 head = head.next; 32 size--; 33 return value; 34 } 35 36 // PEEK — read head without removing — O(1) 37 public T peek() { 38 if (isEmpty()) throw new EmptyStackException(); 39 return head.data; 40 } 41 42 // ISEMPTY — O(1) 43 public boolean isEmpty() { return head == null; } 44 45 // SIZE — O(1) 46 public int size() { return size; } 47 48 // CLEAR — O(1) — just drop the head reference 49 public void clear() { head = null; size = 0; } 50 51 // SEARCH — O(n) 52 public int search(T value) { 53 Node<T> curr = head; 54 int dist = 1; 55 while (curr != null) { 56 if (curr.data.equals(value)) return dist; 57 curr = curr.next; 58 dist++; 59 } 60 return -1; 61 } 62 63 @Override 64 public String toString() { 65 if (isEmpty()) return "Stack: [] (empty)"; 66 StringBuilder sb = new StringBuilder("Stack (top→bottom): ["); 67 Node<T> curr = head; 68 while (curr != null) { 69 sb.append(curr.data); 70 if (curr.next != null) sb.append(", "); 71 curr = curr.next; 72 } 73 sb.append("]"); 74 return sb.toString(); 75 } 76 77 public static void main(String[] args) { 78 LinkedStack<Integer> stack = new LinkedStack<>(); 79 80 stack.push(10); stack.push(20); stack.push(30); 81 System.out.println(stack); // [30, 20, 10] 82 System.out.println("Peek: " + stack.peek()); // 30 83 System.out.println("Size: " + stack.size()); // 3 84 System.out.println("Pop: " + stack.pop()); // 30 85 System.out.println("Pop: " + stack.pop()); // 20 86 System.out.println(stack); // [10] 87 System.out.println("Search 10: " + stack.search(10)); // 1 88 } 89}
Output:
Stack (top→bottom): [30, 20, 10]
Peek:   30
Size:   3
Pop:    30
Pop:    20
Stack (top→bottom): [10]
Search 10: 1

Dry Run: Push and Pop Sequence

Trace every pointer and index change for the array-backed stack:

ArrayStack, internal data = []

push(10):
  data.add(10) → data = [10]
  top = data[size-1] = data[0] = 10

push(20):
  data.add(20) → data = [10, 20]
  top = data[1] = 20

push(30):
  data.add(30) → data = [10, 20, 30]
  top = data[2] = 30

peek():
  return data[size-1] = data[2] = 30   ← no change to data

pop():
  value = data.remove(data.size()-1) = data.remove(2) = 30
  data = [10, 20]
  return 30

pop():
  value = data.remove(1) = 20
  data = [10]
  return 20

isEmpty():
  data.size() == 0? → false

pop():
  value = data.remove(0) = 10
  data = []
  return 10

isEmpty():
  data.size() == 0? → true

Dry Run: Linked-List Stack Push and Pop

LinkedStack, head = null, size = 0

push(10):
  newNode(10).next = null (head)
  head = newNode(10)
  size = 1
  List: head → [10] → null

push(20):
  newNode(20).next = head = Node(10)
  head = newNode(20)
  size = 2
  List: head → [20] → [10] → null

push(30):
  newNode(30).next = head = Node(20)
  head = newNode(30)
  size = 3
  List: head → [30] → [20] → [10] → null

peek():
  return head.data = 30   ← head unchanged

pop():
  value = head.data = 30
  head  = head.next = Node(20)
  size  = 2
  return 30
  List: head → [20] → [10] → null

pop():
  value = head.data = 20
  head  = head.next = Node(10)
  size  = 1
  return 20
  List: head → [10] → null

Operation 1: Push — Deep Dive

Push is the only write operation that adds data. It always places the new element at the top — no searching, no shifting.

Array-backed:
  Append to end of dynamic array.
  If array is full → resize (double capacity), then append.
  Amortized O(1): occasional O(n) resize is absorbed over n pushes.

Linked-list-backed:
  Allocate a new node.
  Set newNode.next = head.
  Set head = newNode.
  Always O(1) — no resizing ever needed.

Edge case — pushing null:
  Java: null is a valid element — push(null) works.
  Python: None is valid — stack.append(None) works.
  C++: depends on type — pointers can be nullptr.
  JavaScript: null and undefined are valid values.

  If null means "empty" in your algorithm, track it separately
  rather than relying on stack.isEmpty().
Java
1import java.util.Deque; 2import java.util.ArrayDeque; 3 4public class PushDemo { 5 6 public static void main(String[] args) { 7 Deque<Integer> stack = new ArrayDeque<>(); 8 9 // Push sequence 10 stack.push(10); 11 stack.push(20); 12 stack.push(30); 13 14 System.out.println("After pushes: " + stack); // [30, 20, 10] 15 16 // Push edge cases 17 // stack.push(null); // NullPointerException for ArrayDeque — it disallows null 18 // Use Integer or box type; ArrayDeque does not permit null elements 19 20 // Capacity — ArrayDeque resizes automatically 21 Deque<Integer> big = new ArrayDeque<>(); 22 for (int i = 0; i < 1000; i++) big.push(i); 23 System.out.println("After 1000 pushes, size: " + big.size()); // 1000 24 } 25}

Operation 2: Pop — Deep Dive

Pop removes and returns the top element. The most critical edge case is popping from an empty stack — every production implementation must guard against this.

Approaches to empty stack:
  1. Throw an exception  — caller must check isEmpty() first
  2. Return null/None    — safe but loses type safety
  3. Return Optional     — Java Optional<T> pattern

Best practice for interviews:
  Always check isEmpty() before pop() in your code.
  State the assumption: "I'll assume the stack is non-empty here."
  or: "Let me add a guard for the empty case."
Java
1import java.util.Deque; 2import java.util.ArrayDeque; 3import java.util.Optional; 4 5public class PopDemo { 6 7 // Safe pop — returns Optional to avoid null returns 8 public static <T> Optional<T> safePop(Deque<T> stack) { 9 if (stack.isEmpty()) return Optional.empty(); 10 return Optional.of(stack.pop()); 11 } 12 13 public static void main(String[] args) { 14 Deque<Integer> stack = new ArrayDeque<>(); 15 stack.push(10); stack.push(20); stack.push(30); 16 17 // Standard pop — throws on empty 18 System.out.println("Pop: " + stack.pop()); // 30 19 System.out.println("Pop: " + stack.pop()); // 20 20 System.out.println("Pop: " + stack.pop()); // 10 21 22 // Safe pop pattern 23 Optional<Integer> result = safePop(stack); 24 System.out.println("Safe pop empty: " + result.isPresent()); // false 25 26 // poll() — returns null instead of throwing 27 Deque<Integer> s2 = new ArrayDeque<>(); 28 s2.push(42); 29 System.out.println("Poll: " + s2.poll()); // 42 30 System.out.println("Poll: " + s2.poll()); // null (not exception) 31 } 32}
Output (Java):
Pop: 30
Pop: 20
Pop: 10
Safe pop empty: false
Poll: 42
Poll: null

Operation 3: Peek — Reading Without Consuming

Peek (also called top() in C++) reads the top element without removing it. It is O(1) and does not change the stack's state.

When to use peek vs pop:

  Use PEEK when:
    You need to know what the top is to decide what to do next,
    but you are not sure yet whether to remove it.

  Use POP when:
    You are done with the top element and want to process it.

Example — balanced parentheses:
  When you see ')', peek the stack to check what's on top.
  If it matches, THEN pop (consume the match).
  If it does not match, the expression is invalid.
  Peeking first avoids popping prematurely.
Java
1import java.util.Deque; 2import java.util.ArrayDeque; 3 4public class PeekDemo { 5 6 public static void main(String[] args) { 7 Deque<Character> stack = new ArrayDeque<>(); 8 stack.push('('); 9 stack.push('['); 10 stack.push('{'); 11 12 System.out.println("Peek: " + stack.peek()); // { 13 System.out.println("Size: " + stack.size()); // 3 — unchanged 14 15 // Peek then decide 16 char top = stack.peek(); 17 if (top == '{') { 18 System.out.println("Top is opening brace — match it"); 19 stack.pop(); 20 } 21 22 System.out.println("Peek after pop: " + stack.peek()); // [ 23 System.out.println("Size: " + stack.size()); // 2 24 } 25}

Array-Backed vs Linked-List-Backed — When to Choose

Array-backed stack:
  Memory:         Contiguous — good cache locality
  Overhead:       Zero per element (just the value)
  Resize:         Automatic doubling — O(n) occasionally, O(1) amortized
  Max capacity:   Limited by available heap memory
  Best for:       Most use cases — faster in practice due to cache

Linked-list-backed stack:
  Memory:         Scattered nodes — cache misses on every push/pop
  Overhead:       One pointer per node (~8 bytes on 64-bit systems)
  Resize:         Never resizes — each node allocated individually
  Max capacity:   Limited only by heap memory (no pre-allocation)
  Best for:       When you need guaranteed O(1) (no amortized),
                  or when stack and linked list structure must be shared

For interview problems:
  Use the built-in (Java Deque/ArrayDeque, Python list, C++ stack, JS array).
  Implementing from scratch is only asked when the problem specifically requires
  showing you understand the underlying mechanics.

Operation Complexity Summary

OperationArray-BackedLinked-List-BackedNotes
pushO(1) amortizedO(1)Array may resize occasionally
popO(1)O(1)Array just decrements size
peekO(1)O(1)Read last/head element
isEmptyO(1)O(1)Compare size to 0
sizeO(1)O(1)Stored as a field
clearO(1)*O(n) C++, O(1) othersC++ must free each node
searchO(n)O(n)Linear scan from top

*O(1) for languages with GC — just drop the reference. O(n) in C++ to free each node.

Common Mistakes Beginners Make

Popping from an empty stack without checking. Always call isEmpty() before pop() or peek(). Java's ArrayDeque.pop() throws NoSuchElementException; C++'s stack::pop() causes undefined behavior; Python raises IndexError. Develop the habit of checking before every pop.

Using Stack<E> in Java. Java's legacy Stack<E> class extends Vector, which is synchronized. Every operation acquires a lock — unnecessary overhead in single-threaded code. Use Deque<E> with ArrayDeque<E> instead.

Forgetting that C++ stack::pop() does not return the value. In C++, pop() removes the top element without returning it. You must call top() first to read the value, then call pop() to remove it. Calling pop() and trying to use its return value (void) is a compile error.

Using pop(0) on a Python list as a stack operation. list.pop(0) removes from the front — it is O(n). A stack uses list.pop() (no argument) to remove from the end in O(1). Using index 0 accidentally turns your stack into a queue with O(n) dequeue cost.

Confusing peek and pop in logic flow. Peek reads the top without consuming it. Pop reads AND removes. In balanced-parentheses problems, peek to check the match, then pop to confirm the consumption. Calling pop when you meant peek can silently empty your stack.

Interview Questions

Q: What is the difference between peek() and pop() on a stack?

peek() reads the top element and leaves it in the stack — the stack's state is unchanged. pop() reads the top element AND removes it — the stack shrinks by one. Both are O(1). Use peek() when you need to inspect the top to make a decision but are not ready to commit to removing it. Use pop() when you have processed the top and are done with it.

Q: Why is ArrayDeque preferred over Stack for stack operations in Java?

Java's Stack extends Vector, which synchronizes every method with a lock. In single-threaded code (all algorithm problems), this is wasted overhead — every push and pop acquires and releases a lock even though no other thread is accessing the stack. ArrayDeque provides identical push/pop/peek semantics without synchronization, making it 2-3x faster in practice. The Java documentation explicitly recommends ArrayDeque over Stack.

Q: How do you implement a stack that returns the minimum element in O(1)?

Maintain two stacks: the main data stack and an auxiliary min-stack. On every push, push to the data stack. If the new value is ≤ the min-stack's current top (or if the min-stack is empty), also push to the min-stack. On every pop from the data stack, if the popped value equals the min-stack's top, also pop from the min-stack. getMin() returns the top of the min-stack — O(1). This is the "Min Stack" interview problem.

FAQs

Does Python's list behave exactly like a stack?

Yes, for the three core operations: append(x) is push O(1) amortized, pop() is pop O(1), and list[-1] is peek O(1). Python's list is array-backed with automatic resizing — the same implementation as Java's ArrayDeque. The difference is that list also supports O(n) operations like insert(0, x) and pop(0) that violate the stack abstraction — you can call them, but doing so makes the structure behave as something other than a pure stack.

Is the call stack the same as the stack data structure?

Yes — the call stack is a specific instance of the stack data structure managed by the language runtime. Each function call pushes a stack frame containing local variables, parameters, and the return address. Returning pops the frame. The LIFO property means the most recently called function is the first to return — which is exactly how nested function calls must behave. A "stack overflow" error means the call stack exceeded its memory limit, usually caused by infinite recursion.

Can a stack contain elements of different types?

In statically typed languages (Java, C++), a stack is typed — all elements must be the same declared type. Use generics (Stack<Object> in Java, stack<variant<...>> in C++) for mixed types, though this is rare. In Python and JavaScript, a single stack can hold any mix of types since the languages are dynamically typed. In algorithm problems, stacks are almost always homogeneous — all integers or all characters.

Quick Quiz

Question 1: You push A, B, C onto a stack. What is returned by two consecutive pops?

  • A) A, B
  • B) B, C
  • C) C, B
  • D) C, A

Answer: C) C, B. The stack is [A (bottom), B, C (top)]. First pop returns C (the top). Second pop returns B (the new top). LIFO — last in, first out.

Question 2: What does peek() return for a stack containing [10 (bottom), 20, 30 (top)]?

  • A) 10
  • B) 20
  • C) 30
  • D) The entire stack

Answer: C) 30. peek() always returns the top element without removing it. The top is 30 — the most recently pushed element that has not yet been popped.

Question 3: In C++, what is the correct way to read and remove the top element from std::stack<int> st?

  • A) int val = st.pop();
  • B) int val = st.top(); st.pop();
  • C) int val = st.peek(); st.pop();
  • D) int val = st.remove();

Answer: B) Read top(), then call pop(). C++'s stack::pop() returns void — it removes but does not return the value. You must call top() first to read the value, then pop() to remove it. Option A is a compile error. Option C — peek() does not exist in C++ (top() is the method name).

Question 4: What happens if you call pop() on an empty ArrayDeque in Java?

  • A) Returns null
  • B) Returns 0
  • C) Throws NoSuchElementException
  • D) Silently does nothing

Answer: C) Throws NoSuchElementException. Java's Deque.pop() throws NoSuchElementException on empty. The alternative is Deque.poll() which returns null instead of throwing. Always check isEmpty() before calling pop() if you are not certain the stack is non-empty.

Summary

Stack operations are simple but their edge cases — especially empty stack handling — are tested constantly in interviews.

The complete operation reference:

  • push(value) — O(1) amortized (array) or O(1) (linked list) — always at the top
  • pop() — O(1) — removes and returns top — throws/crashes if empty — always check isEmpty() first
  • peek() — O(1) — reads top without removing — use to inspect before committing
  • isEmpty() — O(1) — essential guard before every pop/peek
  • size() — O(1) — stored as a field in all implementations
  • clear() — O(1) GC-managed / O(n) C++ — drops all elements
  • search(value) — O(n) — 1-indexed distance from top

Implementation choice: array-backed is faster in practice (cache-friendly, no pointer overhead) and is the right choice for almost all interview problems. Use the built-in Deque/ArrayDeque in Java, list in Python, stack in C++, and Array in JavaScript.

In the next topic, you will explore Stack Patterns — the core interview algorithms that use stacks: balanced parentheses, evaluate expressions, next greater element, and the monotonic stack technique.

Stack Operations