Source: if you pass this file around, link LinhTruong.com so the canonical copy keeps the author line.
Data Structures and Algorithms (DSA) means picking the right representation for data and a procedure that matches your constraints. I keep tripping over it—latency budgets, interview loops, and code nobody profiled. The wrong structure or an overly loose bound is how something “fine locally” turns painful under load.
HashMap vs TreeMap, designing caches, optimizing queries, debugging slow code — all DSA.Before the structures themselves, it helps to agree on Big-O notation: an asymptotic upper bound on how work grows with input size n.
| Notation | Name | Example | n = 1,000,000 |
|---|---|---|---|
| O(1) | Constant | Array index, hash lookup | 1 op |
| O(log n) | Logarithmic | Binary search, balanced BST | ~20 ops |
| O(n) | Linear | Scan, single loop | 1,000,000 ops |
| O(n log n) | Linearithmic | Merge sort, heapsort | ~20,000,000 ops |
| O(n²) | Quadratic | Bubble sort, nested loops | 10¹² ops (infeasible) |
| O(2ⁿ) | Exponential | Naive subset/Fibonacci | cosmic — infeasible above n≈30 |
| O(n!) | Factorial | Brute-force permutations | infeasible above n≈12 |
The average cost per operation across a sequence. A dynamic array's push is O(1) amortized even though occasional resizes are O(n) — the cost is spread over many cheap operations.
std::sort, Python's Timsort, and Java's Dual-Pivot Quicksort use hybrid strategies.
Every data structure represents a tradeoff. There is no "best" structure — only the best for a specific access pattern.
An array is a contiguous block of memory holding fixed-size elements indexed by integer offsets. Because indexing is computed as base + i × sizeof(T), access is O(1). This locality also produces extreme cache efficiency — arrays are usually the fastest structure for sequential workloads despite their humble interface.
| Operation | Static Array | Dynamic Array |
|---|---|---|
| Access by index | O(1) | O(1) |
| Search (unsorted) | O(n) | O(n) |
| Search (sorted, binary) | O(log n) | O(log n) |
| Insert at end | — | O(1) amortized |
| Insert at index | O(n) | O(n) |
| Delete at index | O(n) | O(n) |
| Space | O(n) | O(n) |
Dynamic arrays (Python list, Java ArrayList, C++ std::vector) double their capacity when full, giving amortized O(1) append:
Strings are arrays of characters but with their own algorithmic universe. Most languages make strings immutable — operations that "modify" return new strings. Use a string builder (StringBuilder / list-then-join) when concatenating in loops to avoid O(n²) behavior.
| Algorithm | Purpose | Complexity |
|---|---|---|
| KMP (Knuth–Morris–Pratt) | Pattern matching with prefix function | O(n + m) |
| Rabin–Karp | Rolling hash; finds patterns or duplicates | O(n + m) average |
| Z-Algorithm | String matching via Z-array | O(n + m) |
| Boyer–Moore | Skip characters via bad-char + good-suffix heuristics | Sublinear in practice |
| Manacher's Algorithm | All palindromes | O(n) |
| Suffix Array / Tree | Substring queries | O(n log n) build |
| Aho–Corasick | Multi-pattern matching (e.g. malware scan) | O(n + Σm + matches) |
A linked list trades O(1) random access for O(1) insertion/deletion anywhere, given a reference. Each node holds data plus a pointer to the next node (and previous, for doubly linked).
# Reverse a singly linked list
def reverse(head):
prev, curr = None, head
while curr:
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
return prev
Last-In-First-Out. Imagine a stack of plates: you push onto and pop from the top. Used in: function call stacks, undo systems, expression parsing, DFS, backtracking.
First-In-First-Out. Used in: BFS, task scheduling, message buffers, OS kernels.
A deque (double-ended queue) allows push/pop from both ends in O(1). A priority queue orders by priority instead of arrival time (usually backed by a heap).
| Variant | Push | Pop | Use Case |
|---|---|---|---|
| Queue (FIFO) | O(1) | O(1) | BFS, level-order, scheduling |
| Deque | O(1) both ends | O(1) both ends | Sliding window maximum |
| Priority Queue | O(log n) | O(log n) | Dijkstra, A*, top-K |
| Circular Queue | O(1) | O(1) | Ring buffers, audio I/O |
Hash tables map keys to values in O(1) average time by transforming keys into array indices through a hash function. They are arguably the most important data structure in everyday engineering — they underpin caches, sets, dictionaries, symbol tables, deduplication, and indexing.
| Strategy | How it works | Pros / Cons |
|---|---|---|
| Separate Chaining | Each bucket = list/tree of entries | Simple; degrades to O(n) with bad hash |
| Linear Probing | On collision, scan i+1, i+2, … | Cache-friendly; clustering |
| Quadratic Probing | Scan i+1², i+2², … | Reduces clustering |
| Double Hashing | Step = second hash function | Excellent distribution |
| Robin Hood Hashing | Minimize displacement variance | Used in Rust's HashMap |
| Cuckoo Hashing | Each key has 2 candidate slots | O(1) worst-case lookup |
When load factor (n / capacity) exceeds a threshold (typically 0.75), the table doubles and re-hashes everything. This is O(n) but amortized O(1) per insert.
equals() AND hashCode() consistently.A tree is a connected, acyclic graph with a designated root. Each node has 0 or more children. Trees model hierarchies (file systems, DOM), enable fast search (BSTs), and back storage engines (B-Trees).
| Traversal | Order | Use |
|---|---|---|
| Pre-order | Root → Left → Right | Clone tree, prefix expressions |
| In-order | Left → Root → Right | Sorted output on BST |
| Post-order | Left → Right → Root | Delete tree, postfix expressions |
| Level-order (BFS) | Layer by layer | Shortest path in unweighted tree |
| Tree | Balance Invariant | Used By |
|---|---|---|
| AVL Tree | Heights of subtrees differ ≤ 1 | Memory-database indexes |
| Red-Black Tree | Black-height invariants; ≤ 2× imbalance | Java TreeMap, Linux CFS scheduler |
| Splay Tree | Recently-accessed nodes near root | Caching workloads |
| B-Tree / B+Tree | Many keys per node (high fan-out) | Postgres, MySQL, file systems |
| Treap | BST on keys + heap on random priorities | Randomized balance |
A binary heap is a complete binary tree satisfying the heap property: in a min-heap, every parent ≤ its children; in a max-heap, parent ≥ children. Stored as an array; for index i, children are 2i+1, 2i+2; parent is (i-1)/2.
| Operation | Complexity |
|---|---|
| Peek (min/max) | O(1) |
| Insert | O(log n) |
| Extract | O(log n) |
| Build heap from array | O(n) |
| Heapsort | O(n log n) |
A trie stores strings character-by-character in a tree, sharing common prefixes. Lookup, insert, and prefix queries run in O(L), independent of the number of stored strings.
Use Tries For: autocomplete, spell-check, IP routing (radix trie), DNA k-mers, Aho–Corasick multi-pattern matching.
A graph G = (V, E) is a set of vertices V connected by edges E. Graphs are the most expressive structure in CS — almost any real-world entity-relationship problem reduces to one. Examples: social networks, road maps, dependency resolution, web crawl, neural networks, blockchain.
0: [1, 2]
1: [0, 3]
2: [0, 3]
3: [1, 2]
Space O(V+E). Best for sparse graphs.
0 1 2 3
0 [0 1 1 0]
1 [1 0 0 1]
2 [1 0 0 1]
3 [0 1 1 0]
Space O(V²). O(1) edge lookup. Best for dense graphs.
A Union–Find structure maintains a collection of disjoint sets supporting find(x) (which set?) and union(x,y) (merge sets). With path compression + union by rank, every operation runs in nearly O(1) — formally O(α(n)), the inverse Ackermann function.
Used For: Kruskal's MST, network connectivity, dynamic graph problems, "is this a cycle?" checks.
class DSU:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0]*n
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]] # path compression
x = self.parent[x]
return x
def union(self, a, b):
ra, rb = self.find(a), self.find(b)
if ra == rb: return False
if self.rank[ra] < self.rank[rb]: ra, rb = rb, ra
self.parent[rb] = ra
if self.rank[ra] == self.rank[rb]: self.rank[ra] += 1
return True
Range query structures. A segment tree answers range-sum, range-min, range-gcd, etc. in O(log n) with O(log n) point or range updates. A Fenwick tree (BIT) handles prefix sums with simpler code but only invertible operations.
| Operation | Segment Tree | Fenwick Tree |
|---|---|---|
| Build | O(n) | O(n) |
| Point Update | O(log n) | O(log n) |
| Range Query | O(log n) | O(log n) |
| Range Update (lazy) | O(log n) | O(log n) for additive only |
| Implementation | ~80 LOC | ~15 LOC |
| Algorithm | Best | Average | Worst | Space | Stable | In-place |
|---|---|---|---|---|---|---|
| Bubble | Ω(n) | Θ(n²) | O(n²) | O(1) | ✓ | ✓ |
| Selection | Ω(n²) | Θ(n²) | O(n²) | O(1) | ✗ | ✓ |
| Insertion | Ω(n) | Θ(n²) | O(n²) | O(1) | ✓ | ✓ |
| Merge | Ω(n log n) | Θ(n log n) | O(n log n) | O(n) | ✓ | ✗ |
| Quicksort | Ω(n log n) | Θ(n log n) | O(n²) | O(log n) | ✗ | ✓ |
| Heapsort | Ω(n log n) | Θ(n log n) | O(n log n) | O(1) | ✗ | ✓ |
| Counting | Ω(n+k) | Θ(n+k) | O(n+k) | O(k) | ✓ | ✗ |
| Radix | Ω(nk) | Θ(nk) | O(nk) | O(n+k) | ✓ | ✗ |
| Timsort (Python/Java) | Ω(n) | Θ(n log n) | O(n log n) | O(n) | ✓ | ✗ |
| Introsort (C++) | Ω(n log n) | Θ(n log n) | O(n log n) | O(log n) | ✗ | ✓ |
This is why counting/radix/bucket sort can beat n log n — they exploit the structure of the keys (integers in a bounded range), not arbitrary comparison.
def quicksort(arr, lo, hi):
if lo < hi:
pivot = partition(arr, lo, hi)
quicksort(arr, lo, pivot - 1)
quicksort(arr, pivot + 1, hi)
def partition(arr, lo, hi):
p = arr[hi]; i = lo - 1
for j in range(lo, hi):
if arr[j] <= p:
i += 1; arr[i], arr[j] = arr[j], arr[i]
arr[i+1], arr[hi] = arr[hi], arr[i+1]
return i + 1
Linear search: O(n). Binary search: O(log n) but requires sorted data.
# Canonical binary search — find target index, else -1
def bsearch(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = (lo + hi) // 2 # (lo + (hi - lo)//2) avoids overflow in fixed-width langs
if arr[mid] == target: return mid
elif arr[mid] < target: lo = mid + 1
else: hi = mid - 1
return -1
When you can phrase a problem as "what's the minimum/maximum X such that condition(X) is true?" and the predicate is monotonic, you can binary search the answer space — even if it's not an array.
Examples: minimum capacity to ship in D days, smallest pages to allocate, Koko eating bananas, find peak element.
Recursion is a function that calls itself on smaller instances. Every recursive solution needs:
def backtrack(state):
if is_goal(state):
results.append(copy(state))
return
for choice in candidates(state):
if is_valid(state, choice):
apply(state, choice) # make
backtrack(state)
undo(state, choice) # unmake — the heart of backtracking
Classic backtracking problems: N-Queens, Sudoku, permutations, combinations, subset sum, word search, graph coloring.
Three steps:
For T(n) = a·T(n/b) + f(n):
Classic D&C algorithms: Merge sort, Quicksort, Karatsuba multiplication, Strassen matrix multiply, FFT, closest pair of points.
DP = recursion + memoization. It solves problems with two properties:
Write the natural recursion, then cache results.
def fib(n, memo={}):
if n < 2: return n
if n not in memo:
memo[n] = fib(n-1) + fib(n-2)
return memo[n]
Build a table from base cases upward.
def fib(n):
if n < 2: return n
a, b = 0, 1
for _ in range(n - 1):
a, b = b, a + b
return b
| Category | Example Problem | Recurrence Shape |
|---|---|---|
| 1-D Linear | Climbing stairs, House Robber | dp[i] = f(dp[i-1], dp[i-2]) |
| 2-D Grid | Unique paths, Edit distance, LCS | dp[i][j] = f(dp[i-1][j], dp[i][j-1], …) |
| Knapsack | 0/1 Knapsack, Coin change, Subset sum | dp[i][w] = max(skip, take) |
| Interval | Matrix chain, Burst balloons, Palindrome partition | dp[i][j] = min/max over k |
| Tree / Bitmask | Tree DP, TSP via bitmask | dp[node][state] or dp[mask][i] |
dp[i][j] mean in plain English?A greedy algorithm makes the locally optimal choice at each step, hoping it leads to a globally optimal solution. It works only when the problem has the greedy-choice property + optimal substructure.
When greedy works, it's usually faster and simpler than DP. When it doesn't, it produces wrong answers silently — always prove correctness (exchange argument, matroid theory, or induction).
| Problem | Greedy Choice |
|---|---|
| Activity Selection | Earliest finishing time |
| Huffman Coding | Merge two smallest-frequency nodes |
| Dijkstra's SSSP | Closest unvisited vertex |
| Prim's MST | Lightest crossing edge |
| Kruskal's MST | Lightest edge that doesn't form a cycle |
| Fractional Knapsack | Highest value/weight ratio |
| Jump Game II | Furthest reachable within current step |
Queue-based. Visits all vertices at distance k before k+1.
Uses: shortest path in unweighted graph, level/grid problems, web crawl.
from collections import deque
def bfs(g, src):
seen, q = {src}, deque([src])
while q:
u = q.popleft()
for v in g[u]:
if v not in seen:
seen.add(v); q.append(v)
Stack/recursion. Goes deep before backtracking.
Uses: cycle detection, topological sort, SCCs (Tarjan/Kosaraju), bridges & articulation points.
def dfs(g, u, seen):
seen.add(u)
for v in g[u]:
if v not in seen:
dfs(g, v, seen)
| Algorithm | Handles | Complexity | Use Case |
|---|---|---|---|
| BFS | Unweighted | O(V+E) | Hops in a social network |
| Dijkstra | Non-negative weights | O((V+E) log V) | GPS routing, network latency |
| Bellman–Ford | Negative weights, detects negative cycles | O(V·E) | Currency arbitrage |
| Floyd–Warshall | All-pairs shortest paths | O(V³) | Small dense graphs |
| A* search | Heuristic-guided | O(E) with good heuristic | Pathfinding in games, robotics |
| Johnson's | All-pairs with negatives (no negative cycle) | O(V·E + V² log V) | Sparse graphs |
Linear ordering of a DAG such that every edge u→v has u before v. Used in: build systems (Make, Bazel), course prerequisites, dependency resolution (npm, pip).
Most interview (and plenty of contest) problems still snap to one of these shapes. Naming the shape early beats rewriting from scratch.
| Step | Action |
|---|---|
| Understand | Restate the problem. Confirm input ranges, edge cases, output format. |
| Match | Match to a known pattern (sliding window? graph? DP?). |
| Plan | Outline the algorithm in plain English before coding. |
| Implement | Code cleanly. Use helper functions. |
| Review | Trace through with a small example. |
| Evaluate | Analyze time/space complexity. Discuss tradeoffs and follow-ups. |
| Constraint on n | Likely Complexity | Suggested Approach |
|---|---|---|
| n ≤ 12 | O(n!) | Permutations / brute force |
| n ≤ 25 | O(2ⁿ) | Subsets, bitmask DP, meet-in-the-middle |
| n ≤ 100 | O(n⁴) | DP, Floyd–Warshall |
| n ≤ 500 | O(n³) | DP, matrix chain |
| n ≤ 5,000 | O(n²) | 2-D DP, all pairs |
| n ≤ 10⁶ | O(n log n) | Sort, heap, segment tree, binary search |
| n ≤ 10⁸ | O(n) or O(log n) | Linear scan, two pointers, math formula |
| n > 10⁹ | O(log n) or O(1) | Mathematical insight, closed form |
left, right, count beats a, b, c.| Language | Pros for DSA | Cons |
|---|---|---|
| Python | Concise; built-in sort, heap, dict, set | Slower; recursion limit; no real arrays of primitives |
| C++ | Fastest; STL is unmatched; bit operations | More verbose; manual memory considerations |
| Java | Strong typing; rich collections; common at FAANG | Verbose; int overflow gotchas |
| Go | Simple; great concurrency primitives | Smaller standard collection library |
| Rust | Memory-safe; modern | Borrow checker slows competitive coding |
Narrative and diagrams in §1–§26 are my own synthesis unless noted inline; §27 is where I point people for primary texts and courses. Author: Linh Truong · LinhTruong.com.