Stacks & Queues
LIFO vs FIFO. Stacks power DFS and expression evaluation.
Stacks & Queues
Stack — Last In, First Out (LIFO). Push/pop from the same end. Queue — First In, First Out (FIFO). Enqueue one end, dequeue the other.
The Call Stack
Every recursive function call uses the call stack implicitly:
- Each call pushes the current function state (local variables, return address)
- When the function returns, it pops that frame
- Deep or infinite recursion → stack overflow
- This is why iterative solutions with explicit stacks can be more memory-safe
BFS Uses a Queue
In BFS, when you visit a node you add all its unvisited neighbours to the end of the queue. This guarantees level-by-level exploration:
from collections import deque
def bfs(graph, start):
visited = {start}
q = deque([start])
while q:
node = q.popleft()
for neighbour in graph.get(node, []):
if neighbour not in visited:
visited.add(neighbour)
q.append(neighbour)
Python Built-ins
# Stack — use list
stack = []
stack.append(x) # push O(1)
stack.pop() # pop O(1)
stack[-1] # peek O(1)
# Queue — ALWAYS use deque (list.pop(0) is O(n)!)
from collections import deque
q = deque()
q.append(x) # enqueue O(1)
q.popleft() # dequeue O(1)
q[0] # peek O(1)
Complexity
| Operation | Stack | Queue (deque) | |-----------|-------|-------| | Push/Enqueue | O(1) | O(1) | | Pop/Dequeue | O(1) | O(1) | | Peek | O(1) | O(1) | | Search | O(n) | O(n) |
Min Stack — O(1) min()
Maintain a parallel min stack that tracks the current minimum at every level:
class MinStack:
def __init__(self):
self.stack = []
self.min_stack = [] # top = current minimum
def push(self, val):
self.stack.append(val)
# Push new min: either val or keep existing min
current_min = val if not self.min_stack else min(val, self.min_stack[-1])
self.min_stack.append(current_min)
def pop(self):
self.min_stack.pop()
return self.stack.pop()
def min(self):
return self.min_stack[-1]
The key insight: when pushing, push min(val, current_min) — not just val. When popping, always pop both stacks together.
Balanced Parentheses
def is_balanced(s):
stack = []
pairs = {')': '(', ']': '[', '}': '{'}
for c in s:
if c in '([{':
stack.append(c)
elif c in ')]}':
if not stack or stack[-1] != pairs[c]:
return False
stack.pop()
return len(stack) == 0
Sort a Stack (using 1 extra stack)
def sort_stack(s):
temp = []
while s:
val = s.pop()
# Move values back from temp that are smaller than val
while temp and temp[-1] < val:
s.append(temp.pop())
temp.append(val)
return temp # sorted, smallest on top
Time: O(n²), Space: O(n).
Multiple Stacks in One Array
Divide a single array into N equal segments — each segment is a stack:
- Track a
top[i]pointer for each stack - Handle overflow by resizing the segment or throwing an error
- More complex variant: allow segments to grow dynamically using a linked list of "stack frames"
Key Interview Tips
- Stack → DFS, expression evaluation, function call simulation, monotonic problems
- Queue → BFS, task scheduling, sliding window maximum
- Monotonic stack: keeps elements in monotonic order — powerful for "next greater element"
- Two stacks can simulate a queue (and vice versa) — classic interview question
dequesupports O(1) at both ends — use it instead of list for queues