Hash, Stack & Queue
The three workhorse structures behind almost every other pattern. A hash map buys you O(1) lookup; a stack remembers the most recent thing; a queue remembers the oldest. Pick the right one and half the problem disappears.
Hash map: trade space for instant lookup
A hash map converts a key into an array index via a hash function. When two keys land on the same index (a collision), they chain together in the same bucket — which is why worst case degrades to O(n), even though in practice every operation is O(1).
ToolkitThe Python flavors
| Structure | What it adds |
|---|---|
| dict | Basic key-value store. |
| defaultdict | Auto-initializes missing keys — no if key not in d dance. |
| Counter | Specialized for counting occurrences. |
| OrderedDict | Remembers insertion order with move_to_end / popitem — the LRU building block. |
| set | A hash map with only keys, no values. Membership checks. |
| Operation | Average | Worst case |
|---|---|---|
| Lookup | O(1) | O(n) |
| Insert | O(1) | O(n) |
| Delete | O(1) | O(n) |
| Iterate | O(n) | O(n) |
Worst case requires pathological hash collisions — almost never happens in practice.
Stack: last in, first out
Think of stacking plates: you always push onto the top, pop from the top, and can peek at the top without removing. The most recently added item is always the first one back out.
stack = []
stack.append(item) # push - O(1)
stack.pop() # pop - O(1)
stack[-1] # peek - O(1)
len(stack) == 0 # empty check
Use cases: matching brackets, expression evaluation (postfix, infix), undo/redo, DFS with an explicit stack, monotonic-stack problems (next greater element), and simulating the call stack.
TraceValid parentheses, live
The canonical stack problem: every closing bracket must match the most recent unmatched opener. "Most recent" is the giveaway — that's LIFO.
Queue: first in, first out
A line at a store: enqueue at the back, dequeue from the front, and the first person in line is the first one served.
from collections import deque
queue = deque()
queue.append(item) # enqueue (right) - O(1)
queue.popleft() # dequeue (left) - O(1)
queue[0] # peek front - O(1)
len(queue) == 0 # empty check
deque.popleft() is O(1).
Use cases: BFS traversal, level-order tree traversal, sliding window maximum (with a deque), task scheduling / round-robin, time-based rate limiting.
Reading the problem signal
| Problem signal | Data structure |
|---|---|
| "find pair that sums to…" | Hash map |
| "count occurrences" | Counter |
| "group by property" | defaultdict(list) |
| "check for duplicates" | Set |
| "matching brackets" | Stack |
| "evaluate expression" | Stack |
| "next greater element" | Monotonic stack |
| "undo last action" | Stack |
| "shortest path" | Queue (BFS) |
| "level order traversal" | Queue |
| "process in order" | Queue |
| "sliding window max/min" | Deque |
HashWhich hash flavor?
StackFour questions that reveal a stack
- Is there matching or nesting? Parentheses
( [ { } ] ), HTML tags — yes → stack. - Do I need the most recent unprocessed item? Next greater element, daily temperatures — yes → stack (probably monotonic).
- Am I evaluating an expression? Calculator problems, Reverse Polish Notation — yes → stack (maybe two stacks).
- Do I need to undo or go back? Browser back button, backspace in string — yes → stack.
The complement pattern
The single most common hash map trick: instead of searching for a match, store what you need and check whether the current element is what a previous element needed.
The snippets to memorize
S1Two Sum — complement lookup
def two_sum(nums, target):
"""Find two indices whose values sum to target."""
seen = {} # value -> index
for i, num in enumerate(nums):
complement = target - num
if complement in seen: # O(1) lookup
return [seen[complement], i]
seen[num] = i # store for future lookups
return []
S2Valid Parentheses — stack matching
def is_valid(s):
"""Check if brackets are properly matched and nested."""
stack = []
match = {')': '(', ']': '[', '}': '{'} # closing -> opening
for char in s:
if char in match: # closing bracket
if not stack or stack[-1] != match[char]:
return False # nothing to match, or mismatch
stack.pop() # matched -- remove opening
else:
stack.append(char) # opening bracket -- push
return len(stack) == 0 # all brackets must be matched
S3Group Anagrams — defaultdict grouping
from collections import defaultdict
def group_anagrams(strs):
"""Group strings that are anagrams of each other."""
groups = defaultdict(list)
for s in strs:
# Sorted characters form a canonical key for anagrams
# "eat" -> "aet", "tea" -> "aet", "ate" -> "aet"
key = tuple(sorted(s))
groups[key].append(s)
return list(groups.values())
S4Longest Consecutive Sequence — set + sequence start
def longest_consecutive(nums):
"""Find length of longest consecutive element sequence. O(n)."""
num_set = set(nums)
best = 0
for num in num_set:
# Only start counting from the BEGINNING of a sequence.
# This check ensures each sequence is counted exactly once.
if num - 1 not in num_set: # <-- the key insight
length = 1
while num + length in num_set:
length += 1
best = max(best, length)
return best
S5Min Stack — two parallel stacks
class MinStack:
"""Stack that supports O(1) push, pop, top, and getMin."""
def __init__(self):
self.stack = []
self.min_stack = [] # tracks minimum at each level
def push(self, val):
self.stack.append(val)
# Push the new minimum (compare with current min)
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() # min_stack stays in sync
def top(self):
return self.stack[-1]
def getMin(self):
return self.min_stack[-1]
# stack: [5, 3, 7, 2] min_stack: [5, 3, 3, 2]
# After pop: [5, 3, 7] min_stack: [5, 3, 3]
# getMin() = 3 (correct!)
S6LRU Cache — OrderedDict
from collections import OrderedDict
class LRUCache:
"""Least Recently Used cache with O(1) get and put."""
def __init__(self, capacity):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key) # mark as recently used
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key) # mark as recently used
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # evict LEAST recent (front)
S7Reverse Polish Notation — stack evaluation
def eval_rpn(tokens):
"""Evaluate Reverse Polish Notation using a stack."""
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 token in tokens:
if token in ops:
b = stack.pop() # second operand (top)
a = stack.pop() # first operand
stack.append(ops[token](a, b))
else:
stack.append(int(token))
return stack[0]
Complexity quick reference
| Data structure | Access | Search | Insert | Delete | Space |
|---|---|---|---|---|---|
| Hash map (dict) | O(1)* | O(1)* | O(1)* | O(1)* | O(n) |
| Set | — | O(1)* | O(1)* | O(1)* | O(n) |
| Stack (list) | O(n) | O(n) | O(1)** | O(1)** | O(n) |
| Queue (deque) | O(n) | O(n) | O(1) | O(1) | O(n) |
* amortized average, worst case O(n) · ** push/pop at the top only
Where the foundations crack
deque.popleft().
if stack: before calling pop() or reading stack[-1].
RuntimeError. Iterate over a copy: for k in list(d):.
tuple first
(see the Group Anagrams key).
# WRONG for "find two distinct indices":
seen[num] = i
if complement in seen: ... # might find itself
# RIGHT:
if complement in seen: ... # check first
seen[num] = i # store after
"((" never triggers a mismatch — it just leaves
items behind. Always finish with len(stack) == 0.
Practice ladder
In this order — easiest first. Tick each one as you solve it, and re-solve anything that took more than 25 minutes two days later.