61DSA Fundamentals (deep teach)
What you will master here
- Complexity analysis (Big-O, Big-Θ, Big-Ω, amortised)
- Arrays, strings, hashmaps, sets — when to pick each
- Stacks, queues, deques, priority queues
- Linked lists, doubly-linked lists
- Trees: binary, BST, balanced (AVL/Red-Black), trie
- Graphs: BFS, DFS, Dijkstra, topological sort
- Sorting: quicksort, mergesort, heapsort, counting sort
- Recursion vs iteration; memoisation; DP
61.1 Complexity — the alphabet
| Notation | Meaning |
| Big-O — O(f) | Upper bound — "grows no faster than f" |
| Big-Ω — Ω(f) | Lower bound — "grows at least as fast as f" |
| Big-Θ — Θ(f) | Tight bound — both upper and lower |
| Amortised | Average over a sequence — array push is O(1) amortised even though resize is O(n) |
| Complexity | Examples | Feels like |
| O(1) | Hash lookup, array index | Instant |
| O(log n) | Binary search, balanced tree ops | 1B items in ~30 steps |
| O(n) | Linear scan | Scales linearly |
| O(n log n) | Mergesort, heapsort | Best general-purpose sort |
| O(n²) | Nested loop, bubble sort | Slow on big input |
| O(2ⁿ) | Naive subset/Fibonacci | Exponential — usually wrong |
| O(n!) | All permutations | Brute force; need pruning |
61.2 Arrays & Strings
- Contiguous memory; O(1) index access; O(n) insert/delete in middle
- Dynamic arrays (JS Array, Python list) — amortised O(1) push, resize doubles capacity
- Strings are usually immutable in JS — every "modification" creates a new string
- Two-pointer — sorted array tricks (pair sum, palindrome)
- Sliding window — substring/subarray with constraints
- Prefix sum — range queries in O(1) after O(n) preprocessing
// Prefix sum
function prefix(nums: number[]): number[] {
const p = new Array(nums.length + 1).fill(0);
for (let i = 0; i < nums.length; i++) p[i + 1] = p[i] + nums[i];
return p;
}
// rangeSum(l, r) = p[r+1] - p[l]
61.3 Hashmaps & Sets
- Average O(1) insert/lookup; worst case O(n) with adversarial keys
- Use Set when you only need membership; Map when you need a value per key
- JS
Map preserves insertion order; {} coerces keys to string
- Hash collisions in practice are extremely rare for non-adversarial workloads
61.4 Stacks & Queues
| Structure | Order | Operations |
| Stack | LIFO | push, pop, peek (top) — all O(1) |
| Queue | FIFO | enqueue, dequeue — O(1) with linked list |
| Deque | Both ends | pushFront, pushBack, popFront, popBack — O(1) |
| Priority Queue (min-heap) | By priority | insert, extract-min — O(log n) |
JS arrays approximate all four — push/pop for stack, push/shift for queue (but shift is O(n)!). For real queue use a linked list or a JS class.
61.5 Linked Lists
class Node { val: number; next: Node | null = null; constructor(v: number) { this.val = v; } }
// Insert at head — O(1)
function prepend(head: Node | null, v: number): Node {
const n = new Node(v); n.next = head; return n;
}
// Reverse iteratively — O(n) time, O(1) space
function reverse(head: Node | null): Node | null {
let prev: Node | null = null, curr = head;
while (curr) { const next = curr.next; curr.next = prev; prev = curr; curr = next; }
return prev;
}
When to use a linked list: O(1) insert/delete at known positions; no random access required. Most real code uses arrays — linked lists are interview territory.
61.6 Trees
| Tree type | Property | Used for |
| Binary tree | Each node ≤ 2 children | Generic hierarchical data |
| Binary search tree (BST) | Left < root < right | Sorted lookup O(log n) when balanced |
| Balanced BST (AVL, Red-Black) | Auto-balancing | Java TreeMap, C++ std::map |
| Heap (binary) | Parent ≤ children (min-heap) | Priority queue |
| Trie (prefix tree) | Path = string | Autocomplete, prefix queries |
| B-tree | Many children per node | DB indexes, filesystems |
Tree traversals
- Inorder (left, root, right) — gives sorted order on a BST
- Preorder (root, left, right) — useful for cloning
- Postorder (left, right, root) — useful for deleting
- BFS / level-order — queue-based, useful for "shortest path"
// DFS preorder
function preorder(n: TreeNode | null, out: number[] = []): number[] {
if (!n) return out;
out.push(n.val);
preorder(n.left, out);
preorder(n.right, out);
return out;
}
// BFS
function bfs(root: TreeNode | null): number[] {
const out: number[] = [], q: TreeNode[] = root ? [root] : [];
while (q.length) {
const n = q.shift()!;
out.push(n.val);
if (n.left) q.push(n.left);
if (n.right) q.push(n.right);
}
return out;
}
61.7 Graphs
A graph = nodes + edges. Directed or undirected, weighted or not, cyclic or acyclic.
Representations
- Adjacency list —
Map<node, node[]>. Best for sparse graphs.
- Adjacency matrix — 2D array; O(1) edge check, O(n²) space.
Algorithms
| Algorithm | Purpose | Complexity |
| BFS | Shortest path (unweighted) | O(V + E) |
| DFS | Connectivity, topological sort | O(V + E) |
| Dijkstra | Shortest path (positive weights) | O((V+E) log V) |
| Bellman-Ford | Shortest path (negative weights) | O(V·E) |
| Floyd-Warshall | All-pairs shortest path | O(V³) |
| Topological sort | DAG ordering | O(V + E) |
| Union-Find | Connected components | Nearly O(1) per op |
61.8 Sorting
| Algorithm | Time avg | Time worst | Stable? |
| Bubble / Insertion | O(n²) | O(n²) | Yes |
| Mergesort | O(n log n) | O(n log n) | Yes |
| Quicksort | O(n log n) | O(n²) | No (typically) |
| Heapsort | O(n log n) | O(n log n) | No |
| Counting / Radix | O(n + k) | O(n + k) | Yes |
| JS Array.sort (V8) | O(n log n) | O(n log n) | Yes (Timsort since 2019) |
61.9 Recursion & DP
Recursion basics
- Base case (when to stop)
- Recursive case (smaller subproblem)
- Combine results
- Watch the call stack — JS doesn't tail-call optimise
Memoisation (top-down DP)
// Fibonacci with memo
const memo = new Map<number, number>();
function fib(n: number): number {
if (n < 2) return n;
if (memo.has(n)) return memo.get(n)!;
const r = fib(n - 1) + fib(n - 2);
memo.set(n, r);
return r;
}
// O(n) with memo vs O(2^n) without
Tabulation (bottom-up DP)
function fib2(n: number): number {
if (n < 2) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) [a, b] = [b, a + b];
return b;
}
// O(n) time, O(1) space
61.10 When to use which
| Need | Pick |
| Random access by index | Array |
| Key → value lookup | HashMap |
| Membership check | Set |
| LIFO order | Stack (array) |
| FIFO order | Queue (linked list / deque) |
| Priority access | Heap / PriorityQueue |
| Sorted lookup | Balanced BST (TreeMap) |
| Prefix search | Trie |
| Connected components / cycle detection | Union-Find / DFS |
| Shortest path unweighted | BFS |
| Shortest path weighted | Dijkstra |
Module 37 — DSA Q&A
What's the difference between O, Θ, Ω?
Big-O: upper bound (grows no faster than). Big-Ω: lower bound (grows at least as fast as). Big-Θ: both — tight bound. In practice O is what's usually meant.
What is amortised complexity?
Average cost per operation over a sequence. Array push is O(1) amortised: most pushes are O(1), occasional resizes are O(n), but spread over many pushes the average stays constant.
Hashmap vs balanced BST?
Hashmap: O(1) avg lookup, no order. BST (balanced): O(log n) lookup, maintains sorted order, supports range queries. Use BST when you need to iterate in order or query ranges.
When would you use a stack?
Anywhere you need LIFO: function call tracking, undo, expression evaluation, valid-parentheses, DFS without recursion. push/pop on a JS array works.
What's a priority queue?
Queue where each element has a priority; extract always returns highest priority. Usually implemented as a binary heap — O(log n) insert and extract. Used in Dijkstra, top-K, scheduling.
BFS or DFS — when?
BFS for shortest path on unweighted graphs / level-order on trees. DFS for full exploration, topological sort, cycle detection, path existence. BFS = queue; DFS = recursion or stack.
What's a trie?
Tree where each node represents a character; path from root to node spells a prefix. O(L) lookup where L = word length. Used in autocomplete, prefix queries, spell-checkers.
How does mergesort work?
Recursively split array in half, sort each half, merge. O(n log n) guaranteed, stable, but O(n) extra space. Quicksort is similar but in-place and faster in practice (cache-friendly).
What's the difference between memoisation and tabulation?
Memoisation: top-down recursion + cache. Tabulation: bottom-up iteration, fill a table. Same time complexity; tabulation often uses less memory (can drop earlier rows), no recursion stack.
When is DP the right tool?
Problems with overlapping subproblems AND optimal substructure (optimal answer composed of optimal sub-answers). Climbing stairs, coin change, edit distance, LCS, knapsack. If subproblems don't repeat, plain recursion / divide-and-conquer is fine.
What's a binary heap?
Array-backed tree where parent < children (min-heap) or parent > children (max-heap). Insert and extract-min in O(log n). Index math: parent of i is (i-1)/2; children are 2i+1 and 2i+2.
Sliding window vs two-pointer?
Both move pointers forward. Two-pointer often starts at opposite ends (sorted pair sum, palindrome). Sliding window keeps a contiguous range, growing/shrinking to satisfy a constraint (longest substring without repeats).
What's the space complexity of a recursive DFS?
O(depth) for the call stack — up to O(n) for a skewed tree, O(log n) for a balanced tree. Iterative DFS with an explicit stack has the same.
Why might a hashmap be O(n) in the worst case?
Adversarial keys causing every entry to land in the same bucket — degenerates to a linked list. Real hash functions + load-factor-triggered resizing make this practically impossible for non-adversarial workloads.
When is recursion preferred over iteration?
When the natural definition is recursive — tree/graph traversal, divide-and-conquer (mergesort), backtracking (permutations). For linear iteration, plain loops are clearer and have no stack risk.
61.11 Advanced data structures (dev-level)
Fenwick tree (Binary Indexed Tree)
O(log n) prefix sums with point updates. Compact array implementation.
class Fenwick {
private bit: number[];
constructor(n: number) { this.bit = new Array(n + 1).fill(0); }
update(i: number, delta: number) {
for (i++; i < this.bit.length; i += i & -i) this.bit[i] += delta;
}
query(i: number): number { // prefix sum [0..i]
let s = 0;
for (i++; i > 0; i -= i & -i) s += this.bit[i];
return s;
}
}
Segment tree
Range queries (sum/min/max) + range updates with lazy propagation. O(log n) per op.
Union-Find (Disjoint Set Union)
class DSU {
parent: number[]; rank: number[];
constructor(n: number) {
this.parent = Array.from({ length: n }, (_, i) => i);
this.rank = new Array(n).fill(0);
}
find(x: number): number {
if (this.parent[x] !== x) this.parent[x] = this.find(this.parent[x]); // path compression
return this.parent[x];
}
union(x: number, y: number): boolean {
const rx = this.find(x), ry = this.find(y);
if (rx === ry) return false;
if (this.rank[rx] < this.rank[ry]) this.parent[rx] = ry;
else if (this.rank[rx] > this.rank[ry]) this.parent[ry] = rx;
else { this.parent[ry] = rx; this.rank[rx]++; }
return true;
}
}
// Nearly O(1) per op with both optimisations
Trie + Aho-Corasick
class Trie {
children = new Map<string, Trie>();
end = false;
insert(word: string) {
let n: Trie = this;
for (const c of word) {
if (!n.children.has(c)) n.children.set(c, new Trie());
n = n.children.get(c)!;
}
n.end = true;
}
search(word: string): boolean {
let n: Trie = this;
for (const c of word) {
if (!n.children.has(c)) return false;
n = n.children.get(c)!;
}
return n.end;
}
}
// Aho-Corasick = Trie + failure links → multi-pattern search in O(text + patterns + matches)
Bloom filter
Probabilistic membership: false positives possible, false negatives impossible. Tiny memory. K hash functions set K bits per insert; lookup checks all K bits.
Skip list
Probabilistic alternative to balanced trees. Multiple levels of forward pointers. Average O(log n) ops. Used in Redis sorted sets, LevelDB.
LRU cache (O(1) all ops)
Hashmap + doubly-linked list. Map keys → list nodes. Move to head on access; evict tail on overflow. (See dev ebook m41 for full implementation.)
61.12 Graph algorithms (advanced)
| Algorithm | Purpose | Complexity |
| Dijkstra | Single-source shortest path (non-negative weights) | O((V+E) log V) with heap |
| Bellman-Ford | Same but handles negative weights; detects negative cycles | O(V·E) |
| Floyd-Warshall | All-pairs shortest path | O(V³) |
| A* | Heuristic-guided Dijkstra (maps, games) | Depends on heuristic |
| Kruskal / Prim | Minimum spanning tree | O(E log V) |
| Topological sort | DAG linearisation; dependency order | O(V + E) |
| Tarjan / Kosaraju | Strongly connected components | O(V + E) |
| Max-flow (Dinic's) | Network capacity | O(V²·E) |
61.13 Dynamic programming patterns (complete catalogue)
| Pattern | Example problems |
| 1D state | Fibonacci, climb stairs, house robber, max subarray (Kadane) |
| 2D string | Edit distance, LCS, regex match, distinct subsequences |
| Knapsack (0/1) | Subset sum, partition equal sum, target sum |
| Knapsack (unbounded) | Coin change ways, combinations |
| Interval DP | Matrix chain multiplication, palindrome partition, burst balloons |
| State machine | Best time to buy/sell stock (with cooldown / fees / k transactions) |
| Tree DP | House robber III, longest path, diameter |
| Bitmask DP | TSP for small N (N ≤ 20), assignment problem |
| Digit DP | Count numbers in [L,R] with property |
| DP on graphs | Longest path in DAG, count paths |
61.14 Bit manipulation
x & (x - 1) // clears lowest set bit (Brian Kernighan)
x & -x // isolates lowest set bit
x | (1 << k) // set bit k
x & ~(1 << k) // clear bit k
x ^ (1 << k) // toggle bit k
(x >> k) & 1 // read bit k
Number.isInteger(Math.log2(x)) // is x a power of 2
// Count set bits (popcount)
function popcount(x: number): number {
let c = 0;
while (x) { x &= x - 1; c++; }
return c;
}
// Swap without temp
a ^= b; b ^= a; a ^= b;
61.15 Math you'll use
- Modular arithmetic — large number ops mod prime; (a + b) % m, (a * b) % m, Fermat's little theorem for inverse
- GCD (Euclidean):
gcd(a,b) = b == 0 ? a : gcd(b, a % b)
- Sieve of Eratosthenes — primes up to N in O(N log log N)
- Combinatorics — nCr precomputation via Pascal's triangle
- Geometry — line intersection, convex hull (Graham scan)
- Reservoir sampling — pick K from a stream of unknown length, uniform probability
61.16 SDET-flavoured DSA scenarios (interview-real)
- Test selection algorithm — given a diff (set of changed files) + a coverage map (test → files covered), select minimum tests covering all changed files (set cover, greedy approximation)
- Flake detection — given a stream of test results (pass/fail per run), detect tests with failure rate > X% in sliding window of N (sliding window + counter)
- Dependency graph — given a Jenkins job DAG, find longest path (critical path), detect cycles (topological sort)
- Test ordering — schedule tests to minimise total wall-clock time across K parallel workers (LPT scheduling, NP-hard, greedy approx)
- Log-search — find requests matching a pattern in a 100 GB log file; can't load to memory (streaming + grep + parallel)
- Rate limiter — design a sliding-window counter (sorted set in Redis, or per-shard counters with aggregation)
Module 37 — Advanced DSA Q&A bank
What's a Fenwick tree useful for?
Range-sum (or other associative-op) queries on a mutable array. O(log n) for both point update and prefix query. Simpler than segment tree when you only need point updates and prefix queries.
Union-Find optimisations?
Path compression (flatten chains during find) + union by rank/size (attach smaller tree under larger). Combined: nearly O(1) amortised per op — inverse Ackermann α(n) which is < 5 for any realistic n.
When use a Trie?
Autocomplete, prefix queries, dictionary problems, IP routing tables (radix trie). O(L) lookup where L = word length. Memory cost: one node per char-position across all words.
What's Aho-Corasick?
Trie + failure links. Search for many patterns in text in one O(text + patterns + matches) pass. Used in grep, antivirus, intrusion detection.
Bloom filter trade-off?
Constant-size, very small. False positives possible (with tunable rate), no false negatives. Use to skip expensive lookups (DB, disk) when the answer is "definitely not here".
Dijkstra vs Bellman-Ford?
Dijkstra: faster, requires non-negative weights. Bellman-Ford: slower, handles negative weights, detects negative cycles. Both single-source.
How to detect a cycle in a directed graph?
DFS with 3-colour state (white = unvisited, grey = in current stack, black = done). If you reach a grey node, there's a cycle. O(V + E).
Topological sort algorithms?
Kahn's BFS (repeatedly pick nodes with indegree 0) or DFS-postorder reverse. Both O(V + E). Kahn's also detects cycles (if not all nodes processed).
When use A*?
Shortest path on a graph where you have a heuristic estimate of distance to goal (e.g. Euclidean for maps). A* explores fewer nodes than Dijkstra when the heuristic is good. Heuristic must be admissible (never overestimates).
Memoisation vs tabulation — practical difference?
Memoisation: top-down recursion + cache. Often more intuitive but stack-bound. Tabulation: bottom-up loop. Usually less memory (can compress dimensions). Same time complexity.
How would you compress DP state?
If dp[i] depends only on dp[i-1], use two variables instead of an array. If dp[i][j] depends only on dp[i-1][j-...], reduce 2D to 1D rolling.
0/1 knapsack vs unbounded knapsack?
0/1: each item once; loop items outer, weights inner DESCENDING. Unbounded: items reusable; loop items outer, weights inner ASCENDING. The loop direction prevents/enables re-use.
What's a state machine DP?
Define states (e.g. "holding stock", "not holding") + transitions. dp[i][state] = best by end of day i in state. Useful for buy/sell stock variations.
How to find SCCs?
Tarjan's (one DFS pass with low-link) or Kosaraju's (two DFS passes; second on transposed graph). O(V + E). Common in compiler analysis, social network communities.
Reservoir sampling — algorithm?
Pick K from a stream of unknown N, uniform probability. Fill reservoir with first K. For i >= K, generate r = rand(0..i); if r < K, replace reservoir[r] with item i. O(N) one pass, O(K) memory.
How to find Kth largest in an array?
Min-heap of size K → O(n log k). Or quickselect (Hoare partition) → O(n) average, O(n²) worst. Or sort → O(n log n) simplest.
Sliding-window vs two-pointer — same thing?
Sliding window is a specific two-pointer pattern where both move forward, expanding/shrinking to maintain a constraint. Generic two-pointer also covers opposite-end traversal (palindrome, sorted pair).
How to handle very deep recursion in JS?
JS doesn't optimise tail calls (despite spec). Convert to iteration with an explicit stack. Or increase stack via Node's --stack-size flag (rare).
What's the matroid property (greedy correctness)?
A condition under which a greedy choice always leads to optimal. If a problem is a matroid, greedy works. Example: minimum spanning tree (Kruskal). Hard to prove for new problems; often need DP fallback.
Why is union-find used in Kruskal?
Kruskal sorts edges by weight, picks each if it doesn't form a cycle. "Doesn't form a cycle" = endpoints in different components. Union-find answers + merges in nearly O(1).
How would you implement a thread-safe LRU cache?
Single lock around the map+list ops (simple, contention high). Or finer: lock-free using ConcurrentHashMap + AtomicReference for nodes (complex, paper-level). For most apps, simple lock is fine; if hot, shard the cache across N independent LRUs.
One key formula to remember for DP?
Define dp[state] precisely. Then write transition: dp[state] = function of dp[smaller_states]. Then base + iteration order. Most DP bugs are unclear state definition.
How would an SDET design a test selection algorithm?
Map files → tests (from coverage data). Given changed files = set S, find minimum tests covering S — Set Cover (NP-hard). Use greedy: repeatedly pick the test covering most uncovered files. Approximation ratio ln(n). Add nightly full run as safety net.
How to detect a sliding-window flake?
Per-test ring buffer of last N results. Compute failure rate = fails / N. Alert if > threshold. Implementation: hashmap test_id → ring buffer; update on each test result.