Algorithms — TypeScript Implementations

Data Structures & Algorithms in TypeScript

Full documented implementations of every core data structure and algorithm — ready for interviews. Every method includes Big-O complexity and step-by-step explanation.

Linked List

Singly & Doubly Linked List — Full Implementation

Fundamental
/** * Singly Linked List Node * Each node holds a value and a pointer to the next node. * Memory: O(n) — n nodes, each O(1) space */ class ListNode<T> { constructor( public val: T, public next: ListNode<T> | null = null ) {} } /** * Singly Linked List * - prepend O(1) — add to head * - append O(n) — add to tail (O(1) with tail pointer) * - delete O(n) — must find predecessor * - search O(n) — sequential scan */ class LinkedList<T> { private head: ListNode<T> | null = null; private tail: ListNode<T> | null = null; private _size = 0; get size() { return this._size; } /** Add to tail — O(1) with tail pointer */ append(val: T): void { const node = new ListNode(val); if (!this.tail) { this.head = this.tail = node; } else { this.tail.next = node; this.tail = node; } this._size++; } /** Add to head — O(1) */ prepend(val: T): void { const node = new ListNode(val, this.head); this.head = node; if (!this.tail) this.tail = node; this._size++; } /** Remove first occurrence of value — O(n) */ delete(val: T): boolean { if (!this.head) return false; // Special case: removing head if (this.head.val === val) { this.head = this.head.next; if (!this.head) this.tail = null; this._size--; return true; } // Find predecessor of target node let curr = this.head; while (curr.next) { if (curr.next.val === val) { if (curr.next === this.tail) this.tail = curr; curr.next = curr.next.next; this._size--; return true; } curr = curr.next; } return false; } /** Reverse the list in-place — O(n) time, O(1) space */ reverse(): void { let prev: ListNode<T> | null = null; let curr = this.head; this.tail = this.head; // old head becomes new tail while (curr) { const next = curr.next; curr.next = prev; // flip pointer prev = curr; curr = next; } this.head = prev; // prev is now the last seen node = new head } /** Detect cycle using Floyd's two-pointer — O(n) time, O(1) space */ hasCycle(): boolean { let slow = this.head, fast = this.head; while (fast?.next) { slow = slow!.next; // moves 1 step fast = fast.next.next; // moves 2 steps if (slow === fast) return true; // they meet → cycle } return false; } /** Get middle node (slow/fast pointer) — O(n) */ middle(): ListNode<T> | null { let slow = this.head, fast = this.head; while (fast?.next) { slow = slow!.next; fast = fast.next.next; } return slow; // when fast reaches end, slow is at middle } toArray(): T[] { const res: T[] = []; let curr = this.head; while (curr) { res.push(curr.val); curr = curr.next; } return res; } }
Stack & Queue

Stack (LIFO) + Queue (FIFO) + Deque — Full Implementation

Fundamental
/** * Stack — Last In First Out * Uses an array internally. All operations O(1) amortized. * Use cases: undo/redo, DFS, expression parsing, call stack */ class Stack<T> { private data: T[] = []; push(val: T): void { this.data.push(val); } pop(): T | undefined { return this.data.pop(); } peek(): T | undefined { return this.data[this.data.length - 1]; } isEmpty(): boolean { return this.data.length === 0; } get size(): number { return this.data.length; } } /** * Queue — First In First Out (using two stacks — O(1) amortized) * Naive array approach: enqueue O(1), dequeue O(n) [shift is O(n)] * Two-stack approach: both operations O(1) amortized */ class Queue<T> { private inbox: T[] = []; // push into this private outbox: T[] = []; // pop from this enqueue(val: T): void { this.inbox.push(val); } dequeue(): T | undefined { if (this.outbox.length === 0) { // Transfer all from inbox to outbox — reverses order (FIFO preserved) while (this.inbox.length) this.outbox.push(this.inbox.pop()!); } return this.outbox.pop(); } peek(): T | undefined { if (!this.outbox.length) while (this.inbox.length) this.outbox.push(this.inbox.pop()!); return this.outbox[this.outbox.length - 1]; } get size(): number { return this.inbox.length + this.outbox.length; } isEmpty(): boolean { return this.size === 0; } } /** * Monotonic Stack — maintains increasing or decreasing order * Use case: Next Greater Element, Largest Rectangle in Histogram */ function nextGreaterElement(nums: number[]): number[] { const result = new Array(nums.length).fill(-1); const stack: number[] = []; // stores INDICES of elements waiting for answer for (let i = 0; i < nums.length; i++) { // Pop all elements smaller than current — current is their "next greater" while (stack.length && nums[stack[stack.length - 1]] < nums[i]) { result[stack.pop()!] = nums[i]; } stack.push(i); // push index of current element } return result; // remaining in stack have no greater element → stay -1 }
Binary Tree — Traversals & Operations

Binary Tree — DFS (Pre/In/Post), BFS, Height, Diameter, LCA

Critical
class TreeNode<T> { constructor( public val: T, public left: TreeNode<T> | null = null, public right: TreeNode<T> | null = null ) {} } // ─── DFS Traversals ─────────────────────────────────────── // Pre-order: Root → Left → Right (serialize tree, copy tree) function preOrder<T>(root: TreeNode<T> | null): T[] { if (!root) return []; return [root.val, ...preOrder(root.left), ...preOrder(root.right)]; } // In-order: Left → Root → Right (BST sorted order!) function inOrder<T>(root: TreeNode<T> | null): T[] { if (!root) return []; return [...inOrder(root.left), root.val, ...inOrder(root.right)]; } // Post-order: Left → Right → Root (delete tree, evaluate expression) function postOrder<T>(root: TreeNode<T> | null): T[] { if (!root) return []; return [...postOrder(root.left), ...postOrder(root.right), root.val]; } // Iterative in-order (avoids stack overflow for deep trees) function inOrderIterative<T>(root: TreeNode<T> | null): T[] { const result: T[] = [], stack: TreeNode<T>[] = []; let curr = root; while (curr || stack.length) { while (curr) { stack.push(curr); curr = curr.left; } // go left curr = stack.pop()!; result.push(curr.val); // visit curr = curr.right; // go right } return result; } // ─── BFS / Level-order ───────────────────────────────────── function levelOrder<T>(root: TreeNode<T> | null): T[][] { if (!root) return []; const result: T[][] = [], queue: TreeNode<T>[] = [root]; while (queue.length) { const level: T[] = []; const size = queue.length; // snapshot size — process ONE level for (let i = 0; i < size; i++) { const node = queue.shift()!; level.push(node.val); if (node.left) queue.push(node.left); if (node.right) queue.push(node.right); } result.push(level); } return result; } // ─── Height of tree — O(n) ──────────────────────────────── function height<T>(root: TreeNode<T> | null): number { if (!root) return 0; return 1 + Math.max(height(root.left), height(root.right)); } // ─── Diameter (longest path between any two nodes) ───────── function diameter<T>(root: TreeNode<T> | null): number { let maxDia = 0; function dfs(node: TreeNode<T> | null): number { if (!node) return 0; const L = dfs(node.left), R = dfs(node.right); maxDia = Math.max(maxDia, L + R); // path through this node return 1 + Math.max(L, R); // height contribution upward } dfs(root); return maxDia; } // ─── Lowest Common Ancestor — O(n) ──────────────────────── function lca<T>( root: TreeNode<T> | null, p: T, q: T ): TreeNode<T> | null { if (!root || root.val === p || root.val === q) return root; const left = lca(root.left, p, q); const right = lca(root.right, p, q); // If both sides return non-null → root is LCA if (left && right) return root; return left ?? right; // one side found both nodes }
Binary Search Tree

BST — Insert, Search, Delete, Validate, kth Smallest

Critical
/** * BST Property: left.val < node.val < right.val for ALL nodes * Average case: O(log n) for insert/search/delete * Worst case: O(n) when tree is skewed (like a linked list) * Use self-balancing (AVL, Red-Black) to guarantee O(log n) */ class BST { root: TreeNode<number> | null = null; /** Insert — avg O(log n), worst O(n) */ insert(val: number): void { const insertNode = (node: TreeNode<number> | null): TreeNode<number> => { if (!node) return new TreeNode(val); if (val < node.val) node.left = insertNode(node.left); else if (val > node.val) node.right = insertNode(node.right); // Equal: duplicates ignored (or add count field) return node; }; this.root = insertNode(this.root); } /** Search — avg O(log n) */ search(val: number): boolean { let curr = this.root; while (curr) { if (val === curr.val) return true; curr = val < curr.val ? curr.left : curr.right; } return false; } /** Delete — avg O(log n). 3 cases: * 1. Leaf: simply remove * 2. One child: replace node with child * 3. Two children: replace with in-order successor (min of right subtree) */ delete(val: number): void { const del = (node: TreeNode<number> | null): TreeNode<number> | null => { if (!node) return null; if (val < node.val) { node.left = del(node.left); return node; } if (val > node.val) { node.right = del(node.right); return node; } // Found the node to delete: if (!node.left) return node.right; // case 1 or 2 if (!node.right) return node.left; // case 2 // Case 3: find in-order successor (min of right subtree) let successor = node.right; while (successor.left) successor = successor.left; node.val = successor.val; // copy successor value node.right = del(node.right); // delete successor return node; }; this.root = del(this.root); } /** Validate BST — pass valid range down — O(n) */ isValid(): boolean { const check = ( node: TreeNode<number> | null, min = -Infinity, max = Infinity ): boolean => { if (!node) return true; if (node.val <= min || node.val >= max) return false; return check(node.left, min, node.val) && check(node.right, node.val, max); }; return check(this.root); } /** kth Smallest via in-order (BST sorted) — O(k) */ kthSmallest(k: number): number | null { let count = 0, result: number | null = null; const inOrder = (node: TreeNode<number> | null): void => { if (!node || result !== null) return; inOrder(node.left); if (++count === k) { result = node.val; return; } inOrder(node.right); }; inOrder(this.root); return result; } }
Heap / Priority Queue

Min-Heap (Binary Heap) — Full Implementation with heapify

Critical
/** * Binary Min-Heap stored in array. * Parent of i = Math.floor((i-1)/2) * Left child = 2*i + 1 * Right child = 2*i + 2 * * insert O(log n) — add to end, bubble up * extractMin O(log n) — swap root with last, remove, bubble down * peek O(1) * buildHeap O(n) — heapify from bottom */ class MinHeap { private heap: number[] = []; get size() { return this.heap.length; } peek(): number | undefined { return this.heap[0]; } /** Add element and restore heap property upward */ insert(val: number): void { this.heap.push(val); this.bubbleUp(this.heap.length - 1); } /** Remove and return minimum element — O(log n) */ extractMin(): number | undefined { if (!this.heap.length) return undefined; const min = this.heap[0]; const last = this.heap.pop()!; if (this.heap.length) { this.heap[0] = last; // move last to root this.bubbleDown(0); // restore heap property } return min; } private bubbleUp(i: number): void { while (i > 0) { const parent = Math.floor((i - 1) / 2); if (this.heap[parent] <= this.heap[i]) break; [this.heap[parent], this.heap[i]] = [this.heap[i], this.heap[parent]]; i = parent; } } private bubbleDown(i: number): void { const n = this.heap.length; while (true) { let smallest = i; const L = 2 * i + 1, R = 2 * i + 2; if (L < n && this.heap[L] < this.heap[smallest]) smallest = L; if (R < n && this.heap[R] < this.heap[smallest]) smallest = R; if (smallest === i) break; [this.heap[i], this.heap[smallest]] = [this.heap[smallest], this.heap[i]]; i = smallest; } } /** Build heap from array in O(n) using sift-down from bottom-up */ buildHeap(arr: number[]): void { this.heap = [...arr]; // Start from last internal node and sift down each for (let i = Math.floor(this.heap.length / 2) - 1; i >= 0; i--) { this.bubbleDown(i); } } } // ─── K Largest Elements using MinHeap ───────────────────── function kLargest(nums: number[], k: number): number[] { const heap = new MinHeap(); for (const n of nums) { heap.insert(n); if (heap.size > k) heap.extractMin(); // keep only k largest } const result: number[] = []; while (heap.size) result.unshift(heap.extractMin()!); return result; }
Trie (Prefix Tree)

Trie — Insert, Search, StartsWith, Autocomplete

String
/** * Trie (Prefix Tree) * - Each node has up to 26 children (for lowercase English) * - insert O(m) where m = word length * - search O(m) * - startsWith O(m) * - Space: O(n * m) in worst case (n words, m avg length) * * Use cases: autocomplete, spell check, IP routing, word search grid */ class TrieNode { children: Map<string, TrieNode> = new Map(); isEnd = false; count = 0; // how many words pass through this node } class Trie { private root = new TrieNode(); /** Insert word — O(m) */ insert(word: string): void { let node = this.root; for (const ch of word) { if (!node.children.has(ch)) node.children.set(ch, new TrieNode()); node = node.children.get(ch)!; node.count++; } node.isEnd = true; } /** Exact match search — O(m) */ search(word: string): boolean { const node = this.traverse(word); return !!node?.isEnd; } /** Prefix check — O(m) */ startsWith(prefix: string): boolean { return this.traverse(prefix) !== null; } private traverse(str: string): TrieNode | null { let node: TrieNode | undefined = this.root; for (const ch of str) { node = node.children.get(ch); if (!node) return null; } return node; } /** Autocomplete: get all words with given prefix — O(m + k) */ autocomplete(prefix: string): string[] { const node = this.traverse(prefix); if (!node) return []; const results: string[] = []; const dfs = (curr: TrieNode, path: string): void => { if (curr.isEnd) results.push(prefix + path); for (const [ch, child] of curr.children) dfs(child, path + ch); }; dfs(node, ""); return results; } }
Graph — BFS, DFS, Dijkstra, Topological Sort

Graph Representations + BFS + DFS + Dijkstra + Topological Sort + Union-Find

Critical
/** * Graph Representations: * Adjacency List — O(V+E) space, O(1) add edge, O(degree) neighbors * Adjacency Matrix — O(V²) space, O(1) edge check, good for dense graphs */ class Graph { private adj: Map<number, { node: number; weight: number }[]> = new Map(); addEdge(u: number, v: number, weight = 1, directed = false): void { if (!this.adj.has(u)) this.adj.set(u, []); if (!this.adj.has(v)) this.adj.set(v, []); this.adj.get(u)!.push({ node: v, weight }); if (!directed) this.adj.get(v)!.push({ node: u, weight }); } /** * BFS — O(V+E) * Uses a queue. Explores level by level. * Best for: shortest path in UNWEIGHTED graph, level-order, nearest neighbors */ bfs(start: number): number[] { const visited = new Set<number>([start]); const queue = [start], order: number[] = []; while (queue.length) { const node = queue.shift()!; order.push(node); for (const { node: nb } of this.adj.get(node) ?? []) { if (!visited.has(nb)) { visited.add(nb); queue.push(nb); } } } return order; } /** * DFS — O(V+E) * Explores as deep as possible before backtracking. * Best for: cycle detection, topological sort, connected components, maze */ dfs(start: number): number[] { const visited = new Set<number>(); const order: number[] = []; const explore = (node: number): void => { visited.add(node); order.push(node); for (const { node: nb } of this.adj.get(node) ?? []) { if (!visited.has(nb)) explore(nb); } }; explore(start); return order; } /** * Dijkstra — O((V+E) log V) with min-heap * Shortest path in WEIGHTED graph (non-negative weights only). * Negative weights → use Bellman-Ford instead. */ dijkstra(start: number): Map<number, number> { const dist = new Map<number, number>(); for (const node of this.adj.keys()) dist.set(node, Infinity); dist.set(start, 0); // Min-heap: [distance, node] — use sorted array for simplicity // In production use a proper priority queue for O(log n) extract-min const pq: [number, number][] = [[0, start]]; while (pq.length) { pq.sort((a, b) => a[0] - b[0]); // O(n log n) — replace with heap const [d, u] = pq.shift()!; if (d > dist.get(u)!) continue; // stale entry for (const { node: v, weight: w } of this.adj.get(u) ?? []) { const nd = d + w; if (nd < dist.get(v)!) { dist.set(v, nd); pq.push([nd, v]); } } } return dist; } /** * Topological Sort — Kahn's Algorithm (BFS-based) * O(V+E). Only works on DAG (Directed Acyclic Graph). * Use case: build systems, course prerequisites, task scheduling. */ topologicalSort(nodes: number[]): number[] { const inDegree = new Map<number, number>(); for (const n of nodes) inDegree.set(n, 0); for (const [, edges] of this.adj) { for (const { node: v } of edges) inDegree.set(v, (inDegree.get(v) ?? 0) + 1); } const queue = nodes.filter(n => inDegree.get(n) === 0); const order: number[] = []; while (queue.length) { const u = queue.shift()!; order.push(u); for (const { node: v } of this.adj.get(u) ?? []) { inDegree.set(v, inDegree.get(v)! - 1); if (inDegree.get(v) === 0) queue.push(v); } } return order.length === nodes.length ? order : []; // empty if cycle } }
Union-Find (Disjoint Set Union)

Union-Find — Path Compression + Union by Rank

Graph
/** * Union-Find (Disjoint Set Union) * Operations: O(α(n)) ≈ O(1) amortized with path compression + union by rank * α = inverse Ackermann function — effectively constant for all practical n * * Use cases: * - Kruskal's MST algorithm * - Number of connected components * - Detect cycle in undirected graph * - Dynamic connectivity problems */ class UnionFind { private parent: number[]; private rank: number[]; private _components: number; constructor(n: number) { this.parent = Array.from({ length: n }, (_, i) => i); // each is own parent this.rank = new Array(n).fill(0); this._components = n; } /** Find root with PATH COMPRESSION — O(α(n)) */ find(x: number): number { if (this.parent[x] !== x) { this.parent[x] = this.find(this.parent[x]); // flatten path to root } return this.parent[x]; } /** Union two sets by RANK — O(α(n)) */ union(x: number, y: number): boolean { const rx = this.find(x), ry = this.find(y); if (rx === ry) return false; // already connected — would form cycle! // Attach smaller tree under larger tree to keep depth minimal 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]++; } this._components--; return true; } connected(x: number, y: number): boolean { return this.find(x) === this.find(y); } get components(): number { return this._components; } } // Number of islands using Union-Find function numIslands(grid: string[][]): number { const R = grid.length, C = grid[0].length; const uf = new UnionFind(R * C); let water = 0; for (let r = 0; r < R; r++) for (let c = 0; c < C; c++) { if (grid[r][c] === '0') { water++; continue; } for (const [dr, dc] of [[1,0],[0,1]]) { // check down & right only const nr = r+dr, nc = c+dc; if (nr < R && nc < C && grid[nr][nc] === '1') uf.union(r*C+c, nr*C+nc); } } return uf.components - water; }
Sorting Algorithms

QuickSort, MergeSort, HeapSort, CountingSort — All Implemented

Must Know
/** * Quick Sort — avg O(n log n), worst O(n²), space O(log n) (call stack) * In-place. Not stable. Best for large arrays in practice. * Partition: choose pivot, rearrange so left < pivot < right, recurse. */ function quickSort(arr: number[], lo = 0, hi = arr.length - 1): number[] { if (lo < hi) { const p = partition(arr, lo, hi); quickSort(arr, lo, p - 1); quickSort(arr, p + 1, hi); } return arr; } function partition(arr: number[], lo: number, hi: number): number { const pivot = arr[hi]; let i = lo - 1; for (let j = lo; j < hi; j++) { if (arr[j] <= pivot) { i++; [arr[i], arr[j]] = [arr[j], arr[i]]; } } [arr[i + 1], arr[hi]] = [arr[hi], arr[i + 1]]; return i + 1; } /** * Merge Sort — O(n log n) always, space O(n), stable * Divide array in half, sort each half, merge. * Preferred when stability matters or for linked lists. */ function mergeSort(arr: number[]): number[] { if (arr.length <= 1) return arr; const mid = Math.floor(arr.length / 2); return merge(mergeSort(arr.slice(0, mid)), mergeSort(arr.slice(mid))); } function merge(L: number[], R: number[]): number[] { const res: number[] = []; let i = 0, j = 0; while (i < L.length && j < R.length) { res.push(L[i] <= R[j] ? L[i++] : R[j++]); } return [...res, ...L.slice(i), ...R.slice(j)]; } /** * Counting Sort — O(n + k) where k = value range * Only for integers in known range. Not comparison-based. */ function countingSort(arr: number[], max: number): number[] { const count = new Array(max + 1).fill(0); for (const n of arr) count[n]++; const result: number[] = []; count.forEach((c, v) => result.push(...new Array(c).fill(v))); return result; }
Binary Search Patterns

Binary Search — Template + Left/Right Boundary + Rotated Array

Must Know
/** * Binary Search — O(log n) * TEMPLATE: Always use [lo, hi] inclusive. * Condition check determines which half to go to. */ // Exact search function binarySearch(arr: number[], target: number): number { let lo = 0, hi = arr.length - 1; while (lo <= hi) { const mid = lo + Math.floor((hi - lo) / 2); // avoids overflow vs (lo+hi)/2 if (arr[mid] === target) return mid; else if (arr[mid] < target) lo = mid + 1; else hi = mid - 1; } return -1; } // Left boundary (first occurrence / leftmost valid position) function lowerBound(arr: number[], target: number): number { let lo = 0, hi = arr.length; // hi = length (exclusive) while (lo < hi) { const mid = lo + Math.floor((hi - lo) / 2); if (arr[mid] < target) lo = mid + 1; else hi = mid; // keep going left } return lo; // insertion point = first index where arr[i] >= target } // Search in rotated sorted array — O(log n) function searchRotated(arr: number[], target: number): number { let lo = 0, hi = arr.length - 1; while (lo <= hi) { const mid = lo + Math.floor((hi - lo) / 2); if (arr[mid] === target) return mid; // Determine which half is sorted if (arr[lo] <= arr[mid]) { // left half is sorted if (arr[lo] <= target && target < arr[mid]) hi = mid - 1; else lo = mid + 1; } else { // right half is sorted if (arr[mid] < target && target <= arr[hi]) lo = mid + 1; else hi = mid - 1; } } return -1; } // Binary search on answer (minimize/maximize a value) function minEatingSpeed(piles: number[], h: number): number { let lo = 1, hi = Math.max(...piles); while (lo < hi) { const mid = lo + Math.floor((hi - lo) / 2); const hoursNeeded = piles.reduce((s, p) => s + Math.ceil(p / mid), 0); if (hoursNeeded <= h) hi = mid; // mid might work, try smaller else lo = mid + 1; // too slow } return lo; }
Dynamic Programming Patterns

DP — Memoization, Tabulation, LCS, Knapsack, Coin Change, LIS

Critical
/** * DP Framework: * 1. Define subproblem (what does dp[i] mean?) * 2. Identify recurrence relation * 3. Base case * 4. Iteration order (ensure subproblems are solved before needed) */ // ─── Fibonacci (memoization vs tabulation) ───────────────── function fib(n: number, memo = new Map<number,number>()): number { if (n <= 1) return n; if (memo.has(n)) return memo.get(n)!; const res = fib(n - 1, memo) + fib(n - 2, memo); memo.set(n, res); return res; } // ─── Coin Change (min coins) ────────────────────────────── // dp[i] = min coins to make amount i function coinChange(coins: number[], amount: number): number { const dp = new Array(amount + 1).fill(Infinity); dp[0] = 0; // base: 0 coins to make 0 for (let i = 1; i <= amount; i++) { for (const coin of coins) { if (coin <= i) dp[i] = Math.min(dp[i], dp[i - coin] + 1); } } return dp[amount] === Infinity ? -1 : dp[amount]; } // ─── Longest Common Subsequence ──────────────────────────── // dp[i][j] = LCS of s1[0..i-1] and s2[0..j-1] function lcs(s1: string, s2: string): number { const m = s1.length, n = s2.length; const dp = Array.from({ length: m+1 }, () => new Array(n+1).fill(0)); for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { dp[i][j] = s1[i-1] === s2[j-1] ? dp[i-1][j-1] + 1 // chars match — extend LCS : Math.max(dp[i-1][j], dp[i][j-1]); // skip one char } } return dp[m][n]; } // ─── Longest Increasing Subsequence — O(n log n) ────────── function lis(nums: number[]): number { const tails: number[] = []; // tails[i] = smallest tail of all IS of length i+1 for (const n of nums) { let lo = 0, hi = tails.length; while (lo < hi) { // binary search for insertion position const mid = (lo + hi) >> 1; tails[mid] < n ? lo = mid + 1 : hi = mid; } tails[lo] = n; // extend or replace tail } return tails.length; } // ─── 0/1 Knapsack ───────────────────────────────────────── // dp[i][w] = max value using first i items with capacity w function knapsack(weights: number[], values: number[], W: number): number { const n = weights.length; const dp = new Array(W + 1).fill(0); // space-optimized 1D for (let i = 0; i < n; i++) { for (let w = W; w >= weights[i]; w--) { // reverse to avoid reuse dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]); } } return dp[W]; }
Sliding Window & Two Pointers

Sliding Window + Two Pointers — Templates & Classic Problems

Pattern
// ─── Fixed-size sliding window (max sum of k elements) ───── function maxSumK(arr: number[], k: number): number { let sum = arr.slice(0, k).reduce((a, b) => a + b, 0); let max = sum; for (let i = k; i < arr.length; i++) { sum += arr[i] - arr[i - k]; // slide: add new, remove old max = Math.max(max, sum); } return max; } // ─── Variable-size window (longest substring without repeat) function lengthOfLongestSubstring(s: string): number { const last = new Map<string, number>(); let left = 0, max = 0; for (let right = 0; right < s.length; right++) { const ch = s[right]; // Shrink window if char was seen inside current window if (last.has(ch) && last.get(ch)! >= left) left = last.get(ch)! + 1; last.set(ch, right); max = Math.max(max, right - left + 1); } return max; } // ─── Two Pointers: Container with Most Water ─────────────── function maxWater(height: number[]): number { let lo = 0, hi = height.length - 1, max = 0; while (lo < hi) { max = Math.max(max, Math.min(height[lo], height[hi]) * (hi - lo)); // Move the shorter side inward — moving taller can't improve area height[lo] < height[hi] ? lo++ : hi--; } return max; } // ─── Two Pointers: 3Sum ─────────────────────────────────── function threeSum(nums: number[]): number[][] { nums.sort((a, b) => a - b); const result: number[][] = []; for (let i = 0; i < nums.length - 2; i++) { if (i > 0 && nums[i] === nums[i-1]) continue; // skip duplicates let lo = i + 1, hi = nums.length - 1; while (lo < hi) { const sum = nums[i] + nums[lo] + nums[hi]; if (sum === 0) { result.push([nums[i], nums[lo], nums[hi]]); lo++; hi--; } else if (sum < 0) lo++; else hi--; while (lo < hi && nums[lo] === nums[lo-1]) lo++; // skip dups while (lo < hi && nums[hi] === nums[hi+1]) hi--; } } return result; }
Big-O Complexity Reference

Complete Big-O Cheatsheet — Data Structures & Algorithms

Reference
Data StructureAccessSearchInsertDeleteSpace
ArrayO(1)O(n)O(n)O(n)O(n)
Linked ListO(n)O(n)O(1)O(1)*O(n)
Stack / QueueO(n)O(n)O(1)O(1)O(n)
Hash TableO(1)*O(1)*O(1)*O(n)
BST (avg)O(log n)O(log n)O(log n)O(log n)O(n)
BST (worst)O(n)O(n)O(n)O(n)O(n)
AVL / Red-BlackO(log n)O(log n)O(log n)O(log n)O(n)
Binary HeapO(n)O(n)O(log n)O(log n)O(n)
TrieO(m)O(m)O(m)O(m)O(n·m)
Graph (adj list)O(V+E)O(1)O(V+E)O(V+E)
AlgorithmBestAverageWorstSpaceStable?
Bubble SortO(n)O(n²)O(n²)O(1)
Selection SortO(n²)O(n²)O(n²)O(1)
Insertion SortO(n)O(n²)O(n²)O(1)
Merge SortO(n log n)O(n log n)O(n log n)O(n)
Quick SortO(n log n)O(n log n)O(n²)O(log n)
Heap SortO(n log n)O(n log n)O(n log n)O(1)
Counting SortO(n+k)O(n+k)O(n+k)O(k)
Binary SearchO(1)O(log n)O(log n)O(1)
BFS / DFSO(V+E)O(V)
DijkstraO((V+E) log V)O(V)