XplormityXplormity
HomeHandbooks
Browse
Xplormity

TLDR developer handbooks for
seasoned developers.

Handbooks

RustNestJSNext.jsGitDockerTypeScriptReactNode.jsDSASQLSystem DesignTailwind CSS

Site

HomeHandbooksAboutPrivacyTerms

Connect

GitHubTwitterLinkedIn

© 2026 Xplormity. All rights reserved.

HandbooksDSASorting, Searching & Recursion

Sorting, Searching & Recursion

sortingbinary-searchrecursionbacktrackinghandbook

TL;DR

  • Merge Sort O(n log n): Divide-and-conquer, stable, guaranteed performance.
  • Quick Sort O(n log n) avg: In-place, fast in practice, partition-based.
  • Binary Search: Halve search space each step → O(log n). Learn the template.
  • Backtracking: Systematic exploration with undo — subsets, permutations, combinations.

Step 1: Sorting Algorithms

Sorting is the most-studied problem in computer science because so many other algorithms depend on sorted data — binary search, merge operations, duplicate detection, and interval scheduling all require sorting as a prerequisite. Understanding merge sort and quick sort isn't just academic: JavaScript's Array.sort() uses TimSort (a merge sort variant), and databases use external merge sort for large datasets. Interviewers test sorting knowledge to assess whether you understand divide-and-conquer, stability (preserving order of equal elements), and time-space tradeoffs.

Merge Sort — O(n log n) time, O(n) space

function mergeSort(arr: number[]): number[] {
  if (arr.length <= 1) return arr;

  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));

  return merge(left, right);
}

function merge(left: number[], right: number[]): number[] {
  const result: number[] = [];
  let i = 0, j = 0;

  while (i < left.length && j < right.length) {
    if (left[i] <= right[j]) {
      result.push(left[i++]);
    } else {
      result.push(right[j++]);
    }
  }

  return [...result, ...left.slice(i), ...right.slice(j)];
}

Quick Sort — O(n log n) avg, O(n²) worst, O(log n) space

function quickSort(arr: number[], low = 0, high = arr.length - 1): number[] {
  if (low < high) {
    const pivotIndex = partition(arr, low, high);
    quickSort(arr, low, pivotIndex - 1);
    quickSort(arr, pivotIndex + 1, high);
  }
  return arr;
}

function partition(arr: number[], low: number, high: number): number {
  const pivot = arr[high]; // Choose last element as pivot
  let i = low - 1;        // Pointer for smaller elements

  for (let j = low; j < high; j++) {
    if (arr[j] < pivot) {
      i++;
      [arr[i], arr[j]] = [arr[j], arr[i]]; // Swap
    }
  }

  [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]]; // Place pivot
  return i + 1;
}

Sorting Comparison

Algorithm Best Average Worst Space Stable
Merge Sort O(n log n) O(n log n) O(n log n) O(n) Yes
Quick Sort O(n log n) O(n log n) O(n²) O(log n) No
Heap Sort O(n log n) O(n log n) O(n log n) O(1) No
Insertion Sort O(n) O(n²) O(n²) O(1) Yes
Counting Sort O(n+k) O(n+k) O(n+k) O(k) Yes

Step 2: Binary Search Templates

Binary search templates exist because despite being a "simple" algorithm, it has notoriously tricky edge cases — off-by-one errors, infinite loops from wrong mid calculation, and boundary conditions. Jon Bentley found that 90% of professional programmers couldn't write a correct binary search in 2 hours. These three templates (find exact, find boundary, search on answer) cover every binary search variant you'll encounter. Memorize them and you'll never get stuck on the boundary conditions again.

Template 1: Find exact target

function binarySearch(nums: number[], target: number): number {
  let left = 0, right = nums.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (nums[mid] === target) return mid;
    if (nums[mid] < target) left = mid + 1;
    else right = mid - 1;
  }

  return -1;
}

Template 2: Find boundary (first true)

// Find first index where condition(nums[i]) is true
function findFirst(nums: number[], condition: (val: number) => boolean): number {
  let left = 0, right = nums.length;

  while (left < right) {
    const mid = Math.floor((left + right) / 2);
    if (condition(nums[mid])) {
      right = mid;    // Mid might be the answer, search left
    } else {
      left = mid + 1; // Mid is not valid, search right
    }
  }

  return left; // First index where condition is true
}

// Example: First Bad Version
function firstBadVersion(n: number, isBad: (v: number) => boolean): number {
  let left = 1, right = n;
  while (left < right) {
    const mid = Math.floor((left + right) / 2);
    if (isBad(mid)) right = mid;
    else left = mid + 1;
  }
  return left;
}

Binary Search on Answer

// Koko eating bananas: minimum eating speed to finish in h hours
function minEatingSpeed(piles: number[], h: number): number {
  let left = 1;
  let right = Math.max(...piles);

  // Binary search on the answer (speed)
  while (left < right) {
    const mid = Math.floor((left + right) / 2);
    const hoursNeeded = piles.reduce((sum, pile) => sum + Math.ceil(pile / mid), 0);

    if (hoursNeeded <= h) {
      right = mid;    // Can eat slower
    } else {
      left = mid + 1; // Need to eat faster
    }
  }

  return left;
}

// Split array largest sum: minimize the largest subarray sum with k splits
function splitArray(nums: number[], k: number): number {
  let left = Math.max(...nums);
  let right = nums.reduce((a, b) => a + b);

  while (left < right) {
    const mid = Math.floor((left + right) / 2);

    // Can we split into ≤ k groups where each sum ≤ mid?
    let groups = 1, currentSum = 0;
    for (const num of nums) {
      if (currentSum + num > mid) {
        groups++;
        currentSum = num;
      } else {
        currentSum += num;
      }
    }

    if (groups <= k) right = mid;
    else left = mid + 1;
  }

  return left;
}

Step 3: Recursion & Backtracking

Backtracking was formalized for constraint-satisfaction problems — placing N queens on a chessboard, solving Sudoku, generating all valid parentheses combinations. The key insight: instead of generating all 2^n or n! possibilities and filtering, you build the solution incrementally and abandon a path the moment it violates a constraint ("pruning"). This transforms problems from "generate everything" to "explore intelligently". Backtracking is behind every "generate all combinations/permutations/subsets" problem and appears in ~20% of medium/hard LeetCode problems.

Backtracking Template

function backtrack(
  result: any[],
  current: any[],
  choices: any[],
  start: number
) {
  // 1. Base case: found a valid solution
  if (/* condition */) {
    result.push([...current]); // Add copy of current state
    return;
  }

  // 2. Explore each choice
  for (let i = start; i < choices.length; i++) {
    // 3. Make choice
    current.push(choices[i]);

    // 4. Recurse (explore further)
    backtrack(result, current, choices, i + 1);

    // 5. Undo choice (backtrack)
    current.pop();
  }
}

Subsets (Power Set)

function subsets(nums: number[]): number[][] {
  const result: number[][] = [];

  function backtrack(start: number, current: number[]) {
    result.push([...current]); // Every state is a valid subset

    for (let i = start; i < nums.length; i++) {
      current.push(nums[i]);
      backtrack(i + 1, current);
      current.pop(); // Undo
    }
  }

  backtrack(0, []);
  return result;
}
// [1, 2, 3] → [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]

Permutations

function permute(nums: number[]): number[][] {
  const result: number[][] = [];

  function backtrack(current: number[], remaining: number[]) {
    if (remaining.length === 0) {
      result.push([...current]);
      return;
    }

    for (let i = 0; i < remaining.length; i++) {
      current.push(remaining[i]);
      backtrack(current, [...remaining.slice(0, i), ...remaining.slice(i + 1)]);
      current.pop();
    }
  }

  backtrack([], nums);
  return result;
}
// [1, 2, 3] → [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]

Combinations

function combine(n: number, k: number): number[][] {
  const result: number[][] = [];

  function backtrack(start: number, current: number[]) {
    if (current.length === k) {
      result.push([...current]);
      return;
    }

    // Pruning: need (k - current.length) more, so stop early if not enough left
    for (let i = start; i <= n - (k - current.length) + 1; i++) {
      current.push(i);
      backtrack(i + 1, current);
      current.pop();
    }
  }

  backtrack(1, []);
  return result;
}
// combine(4, 2) → [[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]

N-Queens

function solveNQueens(n: number): string[][] {
  const result: string[][] = [];
  const board: string[][] = Array.from({ length: n }, () => new Array(n).fill('.'));

  const cols = new Set<number>();
  const diag1 = new Set<number>(); // row - col
  const diag2 = new Set<number>(); // row + col

  function backtrack(row: number) {
    if (row === n) {
      result.push(board.map(r => r.join('')));
      return;
    }

    for (let col = 0; col < n; col++) {
      if (cols.has(col) || diag1.has(row - col) || diag2.has(row + col)) continue;

      board[row][col] = 'Q';
      cols.add(col);
      diag1.add(row - col);
      diag2.add(row + col);

      backtrack(row + 1);

      board[row][col] = '.'; // Undo
      cols.delete(col);
      diag1.delete(row - col);
      diag2.delete(row + col);
    }
  }

  backtrack(0);
  return result;
}

Step 4: Important Data Structures

These three data structures (heap, trie, union-find) appear constantly in interviews because they each solve a category of problems that basic arrays/maps can't handle efficiently. Heaps give you O(1) access to min/max with O(log n) insertion — essential for "top K", priority scheduling, and Dijkstra's algorithm. Tries store strings with shared prefixes for autocomplete and spell-checking. Union-Find tracks dynamic connectivity ("are these two nodes connected?") with near-O(1) operations. Knowing when to reach for each one is what separates senior from junior problem-solvers.

Heap / Priority Queue

// JavaScript doesn't have a built-in heap, but here's a min-heap implementation
class MinHeap {
  private heap: number[] = [];

  push(val: number) {
    this.heap.push(val);
    this.bubbleUp(this.heap.length - 1);
  }

  pop(): number | undefined {
    if (this.heap.length === 0) return undefined;
    const min = this.heap[0];
    const last = this.heap.pop()!;
    if (this.heap.length > 0) {
      this.heap[0] = last;
      this.bubbleDown(0);
    }
    return min;
  }

  peek(): number | undefined {
    return this.heap[0];
  }

  get size(): number {
    return this.heap.length;
  }

  private bubbleUp(i: number) {
    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) {
    while (true) {
      let smallest = i;
      const left = 2 * i + 1, right = 2 * i + 2;
      if (left < this.heap.length && this.heap[left] < this.heap[smallest]) smallest = left;
      if (right < this.heap.length && this.heap[right] < this.heap[smallest]) smallest = right;
      if (smallest === i) break;
      [this.heap[smallest], this.heap[i]] = [this.heap[i], this.heap[smallest]];
      i = smallest;
    }
  }
}

// K-th Largest Element using min-heap of size k
function findKthLargest(nums: number[], k: number): number {
  const heap = new MinHeap();
  for (const num of nums) {
    heap.push(num);
    if (heap.size > k) heap.pop(); // Remove smallest, keep k largest
  }
  return heap.peek()!;
}

Trie (Prefix Tree)

class TrieNode {
  children = new Map<string, TrieNode>();
  isEnd = false;
}

class Trie {
  private root = new TrieNode();

  insert(word: string) {
    let node = this.root;
    for (const char of word) {
      if (!node.children.has(char)) {
        node.children.set(char, new TrieNode());
      }
      node = node.children.get(char)!;
    }
    node.isEnd = true;
  }

  search(word: string): boolean {
    const node = this.traverse(word);
    return node !== null && node.isEnd;
  }

  startsWith(prefix: string): boolean {
    return this.traverse(prefix) !== null;
  }

  private traverse(str: string): TrieNode | null {
    let node = this.root;
    for (const char of str) {
      if (!node.children.has(char)) return null;
      node = node.children.get(char)!;
    }
    return node;
  }
}

Union-Find (Disjoint Set)

class UnionFind {
  private parent: number[];
  private rank: number[];
  count: number; // Number of connected components

  constructor(n: number) {
    this.parent = Array.from({ length: n }, (_, i) => i);
    this.rank = new Array(n).fill(0);
    this.count = n;
  }

  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 rootX = this.find(x), rootY = this.find(y);
    if (rootX === rootY) return false; // Already connected

    // Union by rank
    if (this.rank[rootX] < this.rank[rootY]) {
      this.parent[rootX] = rootY;
    } else if (this.rank[rootX] > this.rank[rootY]) {
      this.parent[rootY] = rootX;
    } else {
      this.parent[rootY] = rootX;
      this.rank[rootX]++;
    }

    this.count--;
    return true;
  }

  connected(x: number, y: number): boolean {
    return this.find(x) === this.find(y);
  }
}

Step 5: Linked List Patterns

Linked lists were one of the first dynamic data structures (invented 1955–56 at RAND Corporation). They solve the problem of insertions/deletions in the middle of a sequence without shifting elements. In modern interviews, they test pointer manipulation skills — can you reverse pointers without losing references? Can you detect a cycle with O(1) space? The fast/slow pointer technique (Floyd's algorithm, 1967) is especially elegant: two pointers moving at different speeds will meet in a cycle, and the slow pointer finds the middle in one pass. These patterns also apply to any iterator/stream-based problem.

class ListNode {
  val: number;
  next: ListNode | null;
  constructor(val: number, next: ListNode | null = null) {
    this.val = val;
    this.next = next;
  }
}

// Reverse linked list — O(n)
function reverseList(head: ListNode | null): ListNode | null {
  let prev: ListNode | null = null;
  let current = head;

  while (current) {
    const next = current.next;
    current.next = prev;
    prev = current;
    current = next;
  }

  return prev;
}

// Detect cycle (Floyd's algorithm)
function hasCycle(head: ListNode | null): boolean {
  let slow = head, fast = head;

  while (fast && fast.next) {
    slow = slow!.next;
    fast = fast.next.next;
    if (slow === fast) return true;
  }

  return false;
}

// Find middle of linked list
function middleNode(head: ListNode | null): ListNode | null {
  let slow = head, fast = head;
  while (fast && fast.next) {
    slow = slow!.next;
    fast = fast.next.next;
  }
  return slow;
}

// Merge two sorted lists
function mergeTwoLists(l1: ListNode | null, l2: ListNode | null): ListNode | null {
  const dummy = new ListNode(0);
  let current = dummy;

  while (l1 && l2) {
    if (l1.val <= l2.val) {
      current.next = l1;
      l1 = l1.next;
    } else {
      current.next = l2;
      l2 = l2.next;
    }
    current = current.next;
  }

  current.next = l1 ?? l2;
  return dummy.next;
}

Complexity Cheat Sheet

Operation Array Linked List Hash Map BST (balanced) Heap
Access O(1) O(n) O(1) O(log n) —
Search O(n) O(n) O(1) O(log n) O(n)
Insert O(n) O(1)* O(1) O(log n) O(log n)
Delete O(n) O(1)* O(1) O(log n) O(log n)
Min/Max O(n) O(n) O(n) O(log n) O(1)/O(n)

*With pointer to node


Interview Questions

  1. When would you use Merge Sort over Quick Sort?

    • Merge Sort: guaranteed O(n log n), stable (preserves order of equal elements), good for linked lists (no random access needed). Quick Sort: faster in practice (cache-friendly, less overhead), O(1) extra space (in-place), but O(n²) worst case.
  2. Explain binary search on the answer.

    • When the answer is a number in a range and you can verify if a value works, binary search the answer space. Examples: minimum speed (Koko), maximum minimum distance, minimum days. Check: "Can I achieve this with value X?" If yes, try smaller. If no, try bigger.
  3. How does backtracking differ from brute force?

    • Backtracking prunes invalid paths early (if current state can't lead to solution, stop exploring). Brute force generates all possibilities then filters. Backtracking is still exponential but much faster in practice due to pruning.
  4. When to use Union-Find vs DFS for connectivity?

    • Union-Find: dynamic connectivity (edges added over time), quick connected queries. DFS: static graph, full traversal needed, path finding. Union-Find: near O(1) per operation with path compression. DFS: O(V+E) per traversal.
Dynamic Programming

On this page

  • TL;DR
  • Step 1: Sorting Algorithms
  • Merge Sort — O(n log n) time, O(n) space
  • Quick Sort — O(n log n) avg, O(n²) worst, O(log n) space
  • Sorting Comparison
  • Step 2: Binary Search Templates
  • Template 1: Find exact target
  • Template 2: Find boundary (first true)
  • Binary Search on Answer
  • Step 3: Recursion & Backtracking
  • Backtracking Template
  • Subsets (Power Set)
  • Permutations
  • Combinations
  • N-Queens
  • Step 4: Important Data Structures
  • Heap / Priority Queue
  • Trie (Prefix Tree)
  • Union-Find (Disjoint Set)
  • Step 5: Linked List Patterns
  • Complexity Cheat Sheet
  • Interview Questions