A stack is a linear data structure that follows the Last In, First Out (LIFO) principle — the last element added is the first one removed, like a stack of plates. Stacks appear in function call management, undo/redo systems, expression parsing, and dozens of interview problems.
They are one of the most fundamental data structures, and understanding them deeply unlocks problems involving balanced parentheses, browser history, and depth-first search.
Quick reference
| Operation | Array-based | Linked-list-based | Notes |
|---|---|---|---|
| Push (add to top) | O(1) amortized | O(1) | May trigger resize |
| Pop (remove top) | O(1) | O(1) | Removes and returns |
| Peek (view top) | O(1) | O(1) | Does not remove |
| Search | O(n) | O(n) | No random access |
| Size / isEmpty | O(1) | O(1) | Track with counter |
What is a stack?
A stack is a collection of elements with two primary operations:
- Push — add an element to the top.
- Pop — remove the element from the top.
The key constraint: you can only access the top element. This LIFO discipline is what gives the stack its power.
Stack visualization (top = right):
Initial: []
Push 1: [1]
Push 2: [1, 2]
Push 3: [1, 2, 3] ← top
Pop: [1, 2] → returns 3
Pop: [1] → returns 2
Peek: [1] → returns 1, stack unchanged
LIFO vs FIFO
| Concept | Structure | Order |
|---|---|---|
| LIFO — Last In, First Out | Stack | Most recent element first |
| FIFO — First In, First Out | Queue | Oldest element first |
Stack implementations
Python
Python's built-in list works as a stack out of the box.
class Stack:
def __init__(self):
self._data = []
def push(self, item):
self._data.append(item) # O(1) amortized
def pop(self):
if self.is_empty():
raise IndexError("pop from empty stack")
return self._data.pop() # O(1)
def peek(self):
if self.is_empty():
raise IndexError("peek at empty stack")
return self._data[-1] # O(1)
def is_empty(self):
return len(self._data) == 0
def size(self):
return len(self._data)
def __repr__(self):
return f"Stack({self._data})"
# Usage
s = Stack()
s.push(1)
s.push(2)
s.push(3)
print(s.peek()) # 3
print(s.pop()) # 3
print(s.pop()) # 2
print(s.size()) # 1
For thread-safe or deque-based stacks, use collections.deque:
from collections import deque
stack = deque()
stack.append(1) # push
stack.append(2)
stack.append(3)
top = stack[-1] # peek → 3
val = stack.pop() # pop → 3
JavaScript
class Stack {
constructor() {
this._data = [];
}
push(item) {
this._data.push(item); // O(1) amortized
}
pop() {
if (this.isEmpty()) throw new Error("Stack underflow");
return this._data.pop(); // O(1)
}
peek() {
if (this.isEmpty()) throw new Error("Stack is empty");
return this._data[this._data.length - 1];
}
isEmpty() {
return this._data.length === 0;
}
size() {
return this._data.length;
}
toString() {
return `Stack([${this._data.join(", ")}])`;
}
}
// Usage
const s = new Stack();
s.push("a");
s.push("b");
s.push("c");
console.log(s.peek()); // "c"
console.log(s.pop()); // "c"
console.log(s.size()); // 2
Java
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.EmptyStackException;
public class Stack<T> {
private final Deque<T> data = new ArrayDeque<>();
public void push(T item) {
data.push(item); // O(1)
}
public T pop() {
if (isEmpty()) throw new EmptyStackException();
return data.pop(); // O(1)
}
public T peek() {
if (isEmpty()) throw new EmptyStackException();
return data.peek(); // O(1)
}
public boolean isEmpty() {
return data.isEmpty();
}
public int size() {
return data.size();
}
}
Note: Prefer
Deque<T>(ArrayDeque) overjava.util.Stack, which extendsVectorand is synchronized (unnecessary overhead).
Go
package main
import "fmt"
type Stack[T any] struct {
data []T
}
func (s *Stack[T]) Push(item T) {
s.data = append(s.data, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.data) == 0 {
var zero T
return zero, false
}
top := s.data[len(s.data)-1]
s.data = s.data[:len(s.data)-1]
return top, true
}
func (s *Stack[T]) Peek() (T, bool) {
if len(s.data) == 0 {
var zero T
return zero, false
}
return s.data[len(s.data)-1], true
}
func (s *Stack[T]) IsEmpty() bool { return len(s.data) == 0 }
func (s *Stack[T]) Size() int { return len(s.data) }
func main() {
s := &Stack[int]{}
s.Push(1)
s.Push(2)
s.Push(3)
if v, ok := s.Peek(); ok {
fmt.Println(v) // 3
}
if v, ok := s.Pop(); ok {
fmt.Println(v) // 3
}
fmt.Println(s.Size()) // 2
}
Array-based vs linked-list-based
| Aspect | Array-based | Linked-list-based |
|---|---|---|
| Memory layout | Contiguous (cache-friendly) | Scattered nodes |
| Push/Pop | O(1) amortized (resize) | O(1) strict |
| Memory overhead | Low (no pointers) | Extra pointer per node |
| Max size | Dynamic (grows) | Unbounded |
| Implementation | Simpler | Slightly more code |
| Best for | General use | Strict O(1) guarantee |
For most purposes, the array-based implementation wins on cache performance and simplicity.
The call stack
Every programming language uses a call stack internally to manage function calls:
main() calls factorial(3)
factorial(3) calls factorial(2)
factorial(2) calls factorial(1)
factorial(1) returns 1
factorial(2) returns 2
factorial(3) returns 6
main() continues
Each function call pushes a stack frame (local variables, return address). Each return pops the frame. When recursion is too deep, the call stack overflows — hence "stack overflow."
This is why recursion and explicit stacks are interchangeable: every recursive algorithm can be rewritten with an explicit stack.
Classic interview problems
1. Valid parentheses
Given a string of ()[]{}, determine if it is valid (every opener has a matching closer in correct order).
def is_valid(s: str) -> bool:
stack = []
pairs = {')': '(', ']': '[', '}': '{'}
for ch in s:
if ch in '([{':
stack.append(ch)
elif ch in ')]}':
if not stack or stack[-1] != pairs[ch]:
return False
stack.pop()
return len(stack) == 0
print(is_valid("()[]{}")) # True
print(is_valid("([)]")) # False
print(is_valid("{[]}")) # True
Time: O(n) | Space: O(n)
2. Min stack (O(1) getMin)
Design a stack that supports push, pop, peek, and getMin in O(1).
class MinStack:
def __init__(self):
self._stack = []
self._min_stack = [] # tracks current minimum
def push(self, val: int):
self._stack.append(val)
min_val = min(val, self._min_stack[-1] if self._min_stack else val)
self._min_stack.append(min_val)
def pop(self):
self._stack.pop()
self._min_stack.pop()
def top(self):
return self._stack[-1]
def getMin(self):
return self._min_stack[-1] # O(1)
ms = MinStack()
ms.push(3)
ms.push(5)
ms.push(1)
print(ms.getMin()) # 1
ms.pop()
print(ms.getMin()) # 3
3. Evaluate Reverse Polish Notation
Given tokens like ["2", "1", "+", "3", "*"], evaluate the RPN expression.
def eval_rpn(tokens: list[str]) -> int:
stack = []
ops = {
'+': lambda a, b: a + b,
'-': lambda a, b: a - b,
'*': lambda a, b: a * b,
'/': lambda a, b: int(a / b), # truncate toward zero
}
for t in tokens:
if t in ops:
b, a = stack.pop(), stack.pop()
stack.append(ops[t](a, b))
else:
stack.append(int(t))
return stack[0]
print(eval_rpn(["2", "1", "+", "3", "*"])) # 9 → (2+1)*3
print(eval_rpn(["4", "13", "5", "/", "+"])) # 6 → 4+(13/5)
Time: O(n) | Space: O(n)
4. Daily temperatures (next greater element)
Given daily temperatures, return how many days until a warmer day for each position.
def daily_temperatures(temps: list[int]) -> list[int]:
result = [0] * len(temps)
stack = [] # stores indices
for i, t in enumerate(temps):
while stack and temps[stack[-1]] < t:
idx = stack.pop()
result[idx] = i - idx
stack.append(i)
return result
print(daily_temperatures([73,74,75,71,69,72,76,73]))
# [1, 1, 4, 2, 1, 1, 0, 0]
Pattern: Monotonic stack (decreasing). Pops when a larger element is found.
Time: O(n) | Space: O(n)
5. Largest rectangle in histogram
def largest_rectangle(heights: list[int]) -> int:
stack = [] # monotonic increasing stack of indices
max_area = 0
heights = heights + [0] # sentinel
for i, h in enumerate(heights):
start = i
while stack and stack[-1][1] > h:
idx, height = stack.pop()
max_area = max(max_area, height * (i - idx))
start = idx
stack.append((start, h))
return max_area
print(largest_rectangle([2, 1, 5, 6, 2, 3])) # 10
Time: O(n) | Space: O(n)
6. Decode string
"3[a2[c]]" → "accaccacc"
def decode_string(s: str) -> str:
stack = []
current = ""
k = 0
for ch in s:
if ch.isdigit():
k = k * 10 + int(ch)
elif ch == '[':
stack.append((current, k))
current, k = "", 0
elif ch == ']':
prev_str, repeat = stack.pop()
current = prev_str + current * repeat
else:
current += ch
return current
print(decode_string("3[a2[c]]")) # "accaccacc"
print(decode_string("2[abc]3[cd]")) # "abcabccdcdcd"
7. DFS with explicit stack
Replace recursion with an explicit stack for depth-first search on a graph:
def dfs_iterative(graph: dict, start: str) -> list[str]:
visited = set()
stack = [start]
order = []
while stack:
node = stack.pop()
if node in visited:
continue
visited.add(node)
order.append(node)
for neighbor in reversed(graph[node]): # reversed for left-to-right order
if neighbor not in visited:
stack.append(neighbor)
return order
graph = {
"A": ["B", "C"],
"B": ["D", "E"],
"C": ["F"],
"D": [], "E": [], "F": []
}
print(dfs_iterative(graph, "A")) # ['A', 'B', 'D', 'E', 'C', 'F']
Monotonic stack pattern
A monotonic stack maintains elements in strictly increasing or decreasing order. It solves "next greater/smaller element" problems in O(n) instead of O(n²).
Monotonic decreasing stack (for "next greater element"):
Iterate left to right. For each element:
While stack not empty AND top < current element:
Pop top → its "next greater" is current element
Push current element
Common problems solved with monotonic stack:
| Problem | Stack type |
|---|---|
| Next Greater Element | Monotonic decreasing |
| Next Smaller Element | Monotonic increasing |
| Daily Temperatures | Monotonic decreasing |
| Largest Rectangle in Histogram | Monotonic increasing |
| Trapping Rain Water | Monotonic decreasing |
| Stock Span Problem | Monotonic decreasing |
Stack vs queue vs deque
| Feature | Stack | Queue | Deque |
|---|---|---|---|
| Access end | Top only | Front (dequeue) + Rear (enqueue) | Both ends |
| Order | LIFO | FIFO | Both |
| Use case | DFS, undo, call stack | BFS, task scheduling | Sliding window, palindrome |
| Python | list / deque |
deque |
deque |
| Java | ArrayDeque |
LinkedList / ArrayDeque |
ArrayDeque |
When to use a stack
| Scenario | Why stack fits |
|---|---|
| Undo / redo operations | Last action is first to undo |
| Browser back button | Most recent page first |
| Balanced parentheses / brackets | Match closest opener |
| Expression evaluation (RPN) | Operands wait for operators |
| Function call tracking | Callee returns before caller |
| DFS graph traversal | Explore deepest path first |
| Next greater/smaller element | Monotonic stack O(n) |
| Syntax parsing / compilers | Nested structure resolution |
When NOT to use a stack
| Scenario | Better alternative |
|---|---|
| FIFO ordering needed | Queue |
| Random access by index | Array / list |
| Sorted access by priority | Heap (priority queue) |
| Find min/max efficiently (general) | Heap |
| Bidirectional traversal | Deque |
Common mistakes
| Mistake | Fix |
|---|---|
Calling pop() on an empty stack |
Always check isEmpty() first |
Using java.util.Stack in Java |
Use ArrayDeque instead (faster, no sync) |
| Forgetting to reverse neighbors in iterative DFS | Push in reverse to match recursive order |
| Off-by-one in monotonic stack | Use > vs >= carefully for strict vs non-strict |
| Mutating items while they're on the stack | Store indices, not mutable objects |
| Stack overflow from deep recursion | Convert to iterative with explicit stack |
| Using a queue where a stack is needed (BFS vs DFS) | Stack → DFS; Queue → BFS |
| Not handling the sentinel value in histogram problems | Append 0 to force flush |
Stack vs related structures
| Structure | Order | Access | Python | Java |
|---|---|---|---|---|
| Stack | LIFO | Top only | list / deque |
ArrayDeque |
| Queue | FIFO | Front only | deque |
LinkedList |
| Deque | Both | Both ends | deque |
ArrayDeque |
| Heap | Priority | Min/Max only | heapq |
PriorityQueue |
| Linked list | N/A | Any node | Custom | LinkedList |
Frequently asked questions
Q: Is Python's list a stack?
Yes. list.append() is push and list.pop() is pop — both O(1) amortized. For performance-critical code prefer collections.deque which guarantees O(1) appends and pops.
Q: Why not use java.util.Stack?
It extends Vector, which synchronizes every method. Use ArrayDeque as a Deque — it's faster and the Java docs explicitly recommend it over Stack.
Q: How does a call stack differ from a data structure stack?
A call stack is a specific use of the stack data structure by the language runtime. It stores stack frames (local variables, return addresses). A data structure stack is a programmable container you implement yourself.
Q: Stack vs recursion — which should I use?
They are equivalent in power. Recursion is cleaner when the depth is bounded. An explicit stack avoids stack overflow for deep problems (e.g., DFS on a large graph) and is useful when you need fine-grained control.
Q: What is a monotonic stack and when is it useful?
A monotonic stack maintains elements in sorted order (increasing or decreasing) by popping elements that violate the property. It solves "next greater/smaller element" family of problems in O(n) — a major improvement over O(n²) nested loops.
Q: What is the maximum stack size?
In Python the default recursion limit is 1000 (adjustable via sys.setrecursionlimit). In Java, the JVM stack size is typically 256–1024 KB per thread (configurable via -Xss). An explicit stack on the heap is only limited by available memory.