DSA Cheat Sheet: Deep Notes
Every block: when to use → key properties → sub-topics → Python template → diagram. Skim before an interview; jump via the tabs above.
⏱️ Complexity & Data Structures
Input size → feasible complexity
| n | Approach |
|---|---|
| ≤ 12 | O(n!) permutations |
| ≤ 20 | O(2ⁿ) subsets / bitmask |
| ≤ 100 | O(n³) Floyd-Warshall |
| ≤ 5·10³ | O(n²) DP |
| ≤ 10⁶ | O(n log n) sort / heap |
| ≥ 10⁷ | O(n) / O(log n) |
Operation costs
| DS | Access | Ins/Del | Search |
|---|---|---|---|
| Dynamic array | O(1) | O(n) / O(1)* | O(n) |
| Hash map/set | n/a | O(1) | O(1) |
| Balanced BST | O(log n) | O(log n) | O(log n) |
| Heap | O(1) top | O(log n) | O(n) |
| Trie | n/a | O(L) | O(L) |
*append amortized O(1). Recursion adds O(depth) stack space. Set
sys.setrecursionlimit(10**6) for deep trees.🔢 Arrays & Two Pointers
When: sorted data, pair/triplet sums, in-place partition, palindrome, dedupe, container problems.
PROPERTY Sorting unlocks two-pointers & binary search (turns O(n²)→O(n log n)). In-place tricks use the array itself as storage (index-as-hash).
Sub-topics
- Opposite ends: 2-sum sorted, 3-sum (fix i, 2-ptr rest), container water, trapping rain.
- Same direction: remove dupes, move zeroes (slow=write).
- Dutch flag: 3-way partition (sort colors) with low/mid/high.
- Boyer–Moore voting: majority element (n/2) in O(1) space.
- Next Permutation: find first a[i]<a[i+1] from right, swap w/ next larger, reverse suffix.
- Kadane: max subarray, reset running sum at 0.
def three_sum(a):
a.sort(); res=[]
for i in range(len(a)-2):
if i and a[i]==a[i-1]: continue # skip dup
l, r = i+1, len(a)-1
while l < r:
s = a[i]+a[l]+a[r]
if s==0:
res.append([a[i],a[l],a[r]])
l+=1; r-=1
while l
Kadane: cur = max(x, cur+x); best = max(best, cur)
Dutch flag: [0..low) 0s | [low..mid) 1s | mid..high ? | (high..] 2s
🪟 Sliding Window
When: longest/shortest/count of a contiguous subarray/substring meeting a constraint.
TRICK "Exactly K" =
atMost(K) − atMost(K−1). Window max/min → monotonic deque.- Fixed size k: add a[r], remove a[r-k].
- Variable: grow right; while invalid shrink left; record best.
- Track state: running sum or a frequency map.
def longest_no_repeat(s):
last, left, best = {}, 0, 0
for r, ch in enumerate(s):
if ch in last and last[ch] >= left:
left = last[ch] + 1 # jump left
last[ch] = r
best = max(best, r - left + 1)
return best
Monotonic deque (window max): keep indices, values DECREASING pop_back while a[back] <= a[r] pop_front if front <= r-k front = current window max
🔍 Binary Search
When: sorted search space, OR a monotonic predicate (feasible→infeasible). "min/max X such that check(X)".
PROPERTY
bisect_left = first index ≥ x (lower bound); bisect_right = first index > x. On rotated arrays, one half is always sorted.Sub-topics
- On index: classic, first/last occurrence, rotated search, peak element.
- On answer: Koko bananas, ship in D days, split array, allocate books, aggressive cows.
- On floats: nth root, iterate ~100× or until precision.
- 2D matrix: treat as flattened sorted array, or staircase from top-right.
def lower_bound(a, x):
lo, hi = 0, len(a)
while lo < hi:
mid = (lo+hi)//2
if a[mid] < x: lo = mid+1
else: hi = mid
return lo # first idx with a[idx] >= x
def min_feasible(lo, hi, ok):
while lo < hi:
mid = (lo+hi)//2
if ok(mid): hi = mid
else: lo = mid+1
return lo
➕ Prefix Sum, Difference & Hashing
Prefix / Difference
- Prefix sum: range =
pre[r]-pre[l-1]. - #subarrays sum=k: store freq of prefix;
ans+=seen[pre-k]. - Prefix XOR: subarray XOR = pre[r]^pre[l-1].
- 2D prefix: submatrix sum O(1).
- Difference array: range update +v at l, −v at r+1; prefix to apply. Great for many range adds.
seen={0:1}; pre=ans=0
for x in nums:
pre+=x
ans+=seen.get(pre-k,0)
seen[pre]=seen.get(pre,0)+1
Hashing
- "seen / count / group / pair" → dict or set (O(1)).
- Anagram key = sorted string OR 26-length count tuple.
- Longest consecutive: only start counting at nums without n-1 present → O(n).
- Index-as-hash: put value v at index v (cyclic sort) for 1..n problems.
Counter, defaultdict(list), set: memorize these.📚 Stack, Monotonic Stack & Queue
When: matching, "next greater/smaller", histogram, spans, expression eval; queues for BFS/level order.
RULE Next Greater → decreasing stack. Next Smaller → increasing stack. Store indices to compute widths/spans.
Sub-topics
- NGE/NSE, daily temperatures, stock span.
- Largest rectangle in histogram (nearest smaller both sides).
- Sum of subarray minimums (contribution technique).
- Min-stack (store (val,curMin)); valid parentheses; RPN eval; basic calculator.
- LRU = hashmap + doubly linked list; LFU adds freq buckets.
- Deque = sliding window max/min.
def largest_rectangle(h):
h.append(0); st=[]; best=0 # sentinel
for i, x in enumerate(h):
while st and h[st[-1]] >= x:
height = h[st.pop()]
width = i if not st else i-st[-1]-1
best = max(best, height*width)
st.append(i)
return best
NGE: [2,1,3] -> decreasing stack of idx
push 0; push1; 3 pops 1&0 -> ans[1]=3,ans[0]=3
🔗 Linked List
When: pointer surgery. Tools: dummy head, fast/slow, iterative reverse.
PROPERTY Floyd's cycle: slow+1/fast+2 meet inside a cycle; reset one ptr to head, move both +1 → meet at cycle start.
Sub-topics
- Reverse (full / between / k-group).
- Cycle detect + find start; find middle; palindrome.
- Merge two / merge k (heap); add two numbers.
- Remove Nth from end (two ptr gap = n).
- Copy list with random pointer; reorder list.
def reverse(head):
prev=None
while head:
head.next, prev, head = prev, head, head.next
return prev
dummy = ListNode(0, head) # simplifies edge cases
# remove nth from end: advance fast n+1 steps, then move both
reverse: None<-1 2->3 (flip each next to prev)
🌳 Binary Tree
When: hierarchical data / divide-and-conquer. Solve children first, combine at node.
PROPERTIES Height of node = 1+max(childHeights). #nodes in complete tree ≈ 2^h. DFS uses stack/recursion; BFS uses a queue (level by level).
Sub-topics
- Traversals: pre/in/post (rec + iterative), Morris (O(1) space, thread to predecessor).
- Views: left/right (last per level), top/bottom (by horizontal distance).
- Metrics: height, diameter (max L+R depth), balanced, count nodes.
- Paths: root-to-leaf sum, max path sum (return one side, update global).
- LCA: return node if it equals p/q or splits.
- Construct from pre+in / in+post; serialize (preorder + null markers).
- Distance-K (convert to graph via parent links, BFS).
def diameter(root):
best = 0
def depth(n):
nonlocal best
if not n: return 0
L, R = depth(n.left), depth(n.right)
best = max(best, L + R) # path through n
return 1 + max(L, R)
depth(root); return best
1
/ \
2 3 Pre 1 2 4 5 3 | In 4 2 5 1 3
/ \ Post 4 5 2 3 1 | BFS 1|2 3|4 5
4 5
🌲 Binary Search Tree (BST)
When: ordered data with fast search/insert/delete in O(height).
KEY PROPERTY Inorder traversal of a BST is sorted (ascending). So: print sorted = inorder; Kth smallest = Kth node in inorder; validate = inorder must be strictly increasing; Two-Sum in BST = inorder + two pointers.
Sub-topics & properties
- Search/Insert: go left if target < node else right (O(h)).
- Delete: replace with inorder successor (smallest in right subtree).
- Validate: pass (low, high) bounds down, or check inorder increasing.
- LCA in BST: walk down; the first node between p and q is the LCA.
- Floor/Ceil: track candidate while walking down.
- Kth smallest/largest: inorder (or reverse-inorder) with a counter.
- Predecessor/Successor via inorder neighbors.
def kth_smallest(root, k):
st, node = [], root
while st or node:
while node: # go left
st.append(node); node = node.left
node = st.pop(); k -= 1 # inorder visit
if k == 0: return node.val
node = node.right
def lca_bst(root, p, q):
while root:
if proot.val and q>root.val: root=root.right
else: return root # split point
⛰️ Heap / Priority Queue
When: "Top-K", "Kth", "median of stream", "merge k sorted", scheduling by priority.
PROPERTIES Array heap: parent(i)=(i-1)//2, children=2i+1,2i+2. heapify a list in O(n) (not n log n). Python
heapq is a MIN-heap; push -x for max.- Kth largest: min-heap of size k → root = answer.
- Top-K frequent: Counter + heap (or bucket sort).
- Median of stream: two heaps, keep sizes balanced.
- Merge k lists/arrays: heap of (val, listIdx, elemIdx).
- Greedy + heap: task scheduler, IPO, connect ropes.
Two-heap median:
maxHeap(low half) minHeap(high half)
tops give median; rebalance if sizes differ by >1
heapify([...]) -> O(n) (sift-down from last parent up)
🌿 Recursion & Backtracking
When: enumerate all subsets/permutations/combinations, grid paths, constraint puzzles.
TEMPLATE choose → explore → un-choose. Combos use a
start index (order doesn't matter); permutations use used[]. Sort input to skip duplicates.- Subsets 2ⁿ; permutations n!; combination sum (reuse vs not).
- N-Queens/Sudoku: track cols & diagonals in sets; prune.
- Word search: DFS + mark visited, unmark on return.
- Palindrome partitioning: try every prefix that is a palindrome.
def subsets(nums):
res=[]
def bt(start, path):
res.append(path[:])
for i in range(start, len(nums)):
path.append(nums[i])
bt(i+1, path) # i+1: no reuse
path.pop()
bt(0, []); return res
decision tree (include/exclude):
[]
/ \
[1] []
/ \ / \
[1,2] [1] [2] []
🕸️ Graphs
When: connectivity, paths, ordering, cycles. Model as adjacency list. Always track visited.
CHOOSE Unweighted shortest path → BFS. 0/1 weights → 0-1 BFS (deque). Non-negative → Dijkstra. Negative edges → Bellman-Ford. All-pairs → Floyd-Warshall. DAG order → Topological sort. Directed & undirected: all work on both (an undirected edge = two directed edges u→w, w→u); but negative weights only apply to directed graphs, since a negative undirected edge is itself a negative cycle.
Traversal / Topo / Cycle
from collections import deque, defaultdict
def topo_kahn(n, edges):
adj=defaultdict(list); indeg=[0]*n
for u,v in edges: adj[u].append(v); indeg[v]+=1
q=deque([i for i in range(n) if indeg[i]==0]); order=[]
while q:
u=q.popleft(); order.append(u)
for v in adj[u]:
indeg[v]-=1
if indeg[v]==0: q.append(v)
return order if len(order)==n else [] # [] => cycle
- Cycle (directed): DFS 3-color (white/gray/black) or Kahn fails.
- Cycle (undirected): DSU union, or DFS with parent.
- Bipartite: 2-color via BFS; conflict → not bipartite.
Shortest path / MST
import heapq
def dijkstra(src, adj, n):
dist=[float('inf')]*n; dist[src]=0; pq=[(0,src)]
while pq:
d,u=heapq.heappop(pq)
if d>dist[u]: continue
for v,w in adj[u]:
if d+w
- Bellman-Ford: relax all edges V−1 times; extra relax → negative cycle.
- Floyd-Warshall: dp[i][j]=min(dp[i][j], dp[i][k]+dp[k][j]).
- MST: Kruskal (sort edges + DSU) or Prim (heap).
Grid = graph: cell neighbors = 4/8 directions. "Spread/rot/nearest" → multi-source BFS (push all sources first).
🔗 Union-Find (DSU)
When: dynamic connectivity, count components, cycle in undirected graph, Kruskal MST.
PROPERTY With path compression + union by rank/size, operations are ~O(α(n)) ≈ O(1) amortized.
parent=list(range(n)); rank=[0]*n
def find(x):
while parent[x]!=x:
parent[x]=parent[parent[x]] # path compression
x=parent[x]
return x
def union(a,b):
ra,rb=find(a),find(b)
if ra==rb: return False # already connected (cycle!)
if rank[ra]
🔤 Trie (Prefix Tree)
When: many prefix/word lookups, autocomplete, dictionary matching, and max-XOR (bitwise trie).
PROPERTY Insert/search/prefix are O(word length), independent of #words. Bitwise trie stores numbers MSB→LSB; to maximize XOR, greedily take the opposite bit.
class Trie:
def __init__(self): self.root={}
def insert(self, w):
node=self.root
for c in w: node=node.setdefault(c, {})
node['$']=True
def starts_with(self, pre):
node=self.root
for c in pre:
if c not in node: return False
node=node[c]
return True
🧮 Dynamic Programming
When: overlapping subproblems + optimal substructure. "count ways / min-max / can we reach".
METHOD 1) define state (what varies) 2) transition 3) base case 4) order (memo top-down first, then tabulate). If greedy is hard to justify → it's usually DP.
Pattern → state
| Pattern | State / transition |
|---|---|
| 1D (rob/stairs) | dp[i] from dp[i-1], dp[i-2] |
| 0/1 Knapsack | dp[i][cap] take/skip |
| Unbounded | reuse item (coin change) |
| LCS / Edit | dp[i][j] over 2 strings |
| LIS | dp[i]=1+max(dp[j]); n log n patience |
| Grid | dp[i][j]=dp[i-1][j]+dp[i][j-1] |
| Partition/MCM | try split k in [i,j] |
| Stocks | (day, holding, txns left) |
| Tree DP | return (incl, excl) per node |
| Bitmask (TSP) | dp[mask][i] visited set |
from functools import lru_cache
@lru_cache(None)
def knap(i, cap):
if i==n or cap==0: return 0
best = knap(i+1, cap) # skip
if wt[i] <= cap:
best = max(best, val[i]+knap(i+1, cap-wt[i]))
return best
# LIS in O(n log n)
import bisect
def lis(a):
tails=[]
for x in a:
i=bisect.bisect_left(tails, x)
if i==len(tails): tails.append(x)
else: tails[i]=x
return len(tails)
🎯 Greedy
When: a locally-optimal choice provably leads to a global optimum. Usually sort first.
PATTERNS Intervals → sort by end, pick earliest finish. Fractional knapsack → sort by value/weight. Huffman → repeatedly merge two smallest (heap). Jump game → track farthest reach.
- Activity selection / non-overlapping intervals / min arrows.
- Gas station: if total gas ≥ cost, the answer is the index after the last deficit.
- Job sequencing by deadline; assign cookies; partition labels.
🔟 Bit Manipulation
IDENTITIES
x&(x-1) clears lowest set bit · x&-x isolates it · a^a=0, a^0=a · x&(x-1)==0 ⇒ power of two.- Single number: XOR all → unpaired survives.
- Count set bits: loop
x&=x-1, orbin(x).count('1'). - Subsets: iterate
mask in range(1<<n). - Check/set/clear bit i:
x>>i&1,x|1<<i,x&~(1<<i).
Iterate submasks of mask:
sub = mask
while sub:
use(sub); sub = (sub-1) & mask
⭐ Must-Know Algorithms
One-line recall of the classic named algorithms interviewers expect.
| Algorithm | Idea | Complexity |
|---|---|---|
| Kadane | max subarray: cur=max(x,cur+x) | O(n) |
| Boyer–Moore | majority vote (count++/--) | O(n), O(1) |
| Dutch National Flag | 3-way partition low/mid/high | O(n) |
| Floyd's Cycle | tortoise & hare; reset to find start | O(n), O(1) |
| Quickselect | partition around pivot for Kth | O(n) avg |
| Merge Sort | divide, sort, merge; count inversions | O(n log n) |
| KMP | LPS array skips re-matching | O(n+m) |
| Rabin–Karp | rolling hash of windows | O(n+m) avg |
| Dijkstra | greedy + min-heap, non-neg weights | O(E log V) |
| Bellman–Ford | relax edges V−1×; detect neg cycle | O(V·E) |
| Floyd–Warshall | all-pairs via intermediate k | O(V³) |
| Kahn's Topo | BFS on in-degree = 0 | O(V+E) |
| Kruskal / Prim | MST via DSU / heap | O(E log E) |
| Tarjan / Kosaraju | bridges, articulation, SCC | O(V+E) |
| Union-Find | path compression + rank | ~O(α)≈O(1) |
| Morris Traversal | threaded inorder, O(1) space | O(n) |
| Sieve of Eratosthenes | mark multiples of each prime | O(n log log n) |
| Fast Exponentiation | square & multiply on bits of n | O(log n) |
🔢 Math & Number Theory
- GCD (Euclid):
gcd(a,b)=gcd(b,a%b); LCM = a*b/gcd. - Modular: (a+b)%m, (a*b)%m; use
pow(a,b,m)for modular power. - Primes: sieve up to n; a number n is prime if no divisor ≤ √n.
- Combinatorics: nCr = n!/(r!(n−r)!); Pascal for small n.
- Overflow: Python ints are arbitrary precision (no overflow), but state it in other languages.
def sieve(n):
is_p=[True]*(n+1); is_p[0]=is_p[1]=False
for i in range(2, int(n**0.5)+1):
if is_p[i]:
for j in range(i*i, n+1, i): is_p[j]=False
return [i for i,v in enumerate(is_p) if v]
def power(a, b, mod):
res=1; a%=mod
while b:
if b&1: res=res*a%mod
a=a*a%mod; b>>=1
return res
🔃 Sorting
| Algorithm | Time | Space | Stable | Note |
|---|---|---|---|---|
| Merge | O(n log n) | O(n) | Yes | linked lists, inversions |
| Quick | O(n log n) avg | O(log n) | No | in-place; worst O(n²) |
| Heap | O(n log n) | O(1) | No | no recursion |
| Counting | O(n+k) | O(k) | Yes | small int range |
| Radix | O(d·(n+b)) | O(n+b) | Yes | fixed-width keys |
| Cyclic sort | O(n) | O(1) | No | values in 1..n |
Quickselect finds the Kth element in avg O(n), partition, recurse into one side only.
✅ Interview Playbook
- Clarify: ranges, dups, sorted?, empties, negative numbers, expected complexity.
- Brute force → optimize: state it with complexity, then improve.
- Dry-run a tiny example; check edges: empty, 1 element, all same, duplicates, overflow.
- State final complexity (time & space) and trade-offs.
Trigger → tool: sorted/pair → Two Pointers · contiguous subarray → Sliding Window / Prefix · next greater/spans → Monotonic Stack · shortest unweighted → BFS · weighted → Dijkstra · all combinations → Backtracking · min/max/count ways → DP · Top-K/Kth → Heap · prefix words → Trie · connectivity → Union-Find · ordering with deps → Topological Sort.