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
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
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().
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."
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.
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
| Operation | Array-Backed | Linked-List-Backed | Notes |
|---|---|---|---|
| push | O(1) amortized | O(1) | Array may resize occasionally |
| pop | O(1) | O(1) | Array just decrements size |
| peek | O(1) | O(1) | Read last/head element |
| isEmpty | O(1) | O(1) | Compare size to 0 |
| size | O(1) | O(1) | Stored as a field |
| clear | O(1)* | O(n) C++, O(1) others | C++ must free each node |
| search | O(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.