JavaScript Algorithms

JavaScript Algorithms: Sorting, Searching, and Problem Solving

Master essential algorithms in JavaScript. Learn sorting, searching, graph algorithms, dynamic programming, and optimize your problem-solving skills.

By JavaScript Document Team
algorithmssortingsearchinggraphdynamic-programmingoptimization

Algorithms are step-by-step procedures for solving computational problems. This comprehensive guide covers essential algorithms implemented in JavaScript, from basic sorting and searching to advanced graph algorithms and dynamic programming.

Sorting Algorithms

Sorting algorithms arrange elements in a specific order, typically ascending or descending.

Bubble Sort

// Bubble Sort - Simple but inefficient O(n²)
class BubbleSort {
  static sort(arr, compareFunction = (a, b) => a - b) {
    const result = [...arr];
    const n = result.length;
    let swapped;

    for (let i = 0; i < n - 1; i++) {
      swapped = false;

      for (let j = 0; j < n - i - 1; j++) {
        if (compareFunction(result[j], result[j + 1]) > 0) {
          // Swap elements
          [result[j], result[j + 1]] = [result[j + 1], result[j]];
          swapped = true;
        }
      }

      // If no swapping occurred, array is already sorted
      if (!swapped) break;
    }

    return result;
  }

  // Animated version that yields intermediate steps
  static *sortAnimated(arr, compareFunction = (a, b) => a - b) {
    const result = [...arr];
    const n = result.length;

    for (let i = 0; i < n - 1; i++) {
      let swapped = false;

      for (let j = 0; j < n - i - 1; j++) {
        // Yield current state for visualization
        yield {
          array: [...result],
          comparing: [j, j + 1],
          sorted: Array.from({ length: i }, (_, k) => n - 1 - k),
        };

        if (compareFunction(result[j], result[j + 1]) > 0) {
          [result[j], result[j + 1]] = [result[j + 1], result[j]];
          swapped = true;

          yield {
            array: [...result],
            swapped: [j, j + 1],
            sorted: Array.from({ length: i }, (_, k) => n - 1 - k),
          };
        }
      }

      if (!swapped) break;
    }

    yield {
      array: [...result],
      comparing: [],
      sorted: Array.from({ length: n }, (_, i) => i),
      completed: true,
    };

    return result;
  }
}

// Usage
console.log(BubbleSort.sort([64, 34, 25, 12, 22, 11, 90]));
// [11, 12, 22, 25, 34, 64, 90]

Quick Sort

// Quick Sort - Efficient divide-and-conquer O(n log n) average
class QuickSort {
  static sort(arr, compareFunction = (a, b) => a - b) {
    if (arr.length <= 1) return [...arr];

    const result = [...arr];
    this._quickSort(result, 0, result.length - 1, compareFunction);
    return result;
  }

  static _quickSort(arr, low, high, compareFunction) {
    if (low < high) {
      const pivotIndex = this._partition(arr, low, high, compareFunction);

      this._quickSort(arr, low, pivotIndex - 1, compareFunction);
      this._quickSort(arr, pivotIndex + 1, high, compareFunction);
    }
  }

  static _partition(arr, low, high, compareFunction) {
    // Choose the rightmost element as pivot
    const pivot = arr[high];
    let i = low - 1; // Index of smaller element

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

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

  // Three-way partitioning for handling duplicates efficiently
  static sortThreeWay(arr, compareFunction = (a, b) => a - b) {
    if (arr.length <= 1) return [...arr];

    const result = [...arr];
    this._quickSortThreeWay(result, 0, result.length - 1, compareFunction);
    return result;
  }

  static _quickSortThreeWay(arr, low, high, compareFunction) {
    if (low >= high) return;

    const [lt, gt] = this._partitionThreeWay(arr, low, high, compareFunction);

    this._quickSortThreeWay(arr, low, lt - 1, compareFunction);
    this._quickSortThreeWay(arr, gt + 1, high, compareFunction);
  }

  static _partitionThreeWay(arr, low, high, compareFunction) {
    const pivot = arr[low];
    let i = low;
    let lt = low;
    let gt = high;

    while (i <= gt) {
      const cmp = compareFunction(arr[i], pivot);

      if (cmp < 0) {
        [arr[lt++], arr[i++]] = [arr[i], arr[lt]];
      } else if (cmp > 0) {
        [arr[i], arr[gt--]] = [arr[gt], arr[i]];
      } else {
        i++;
      }
    }

    return [lt, gt];
  }
}

// Usage
const numbers = [3, 6, 8, 10, 1, 2, 1];
console.log(QuickSort.sort(numbers)); // [1, 1, 2, 3, 6, 8, 10]
console.log(QuickSort.sortThreeWay(numbers)); // [1, 1, 2, 3, 6, 8, 10]

Merge Sort

// Merge Sort - Stable divide-and-conquer O(n log n)
class MergeSort {
  static sort(arr, compareFunction = (a, b) => a - b) {
    if (arr.length <= 1) return [...arr];

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

    return this._merge(left, right, compareFunction);
  }

  static _merge(left, right, compareFunction) {
    const result = [];
    let leftIndex = 0;
    let rightIndex = 0;

    while (leftIndex < left.length && rightIndex < right.length) {
      if (compareFunction(left[leftIndex], right[rightIndex]) <= 0) {
        result.push(left[leftIndex]);
        leftIndex++;
      } else {
        result.push(right[rightIndex]);
        rightIndex++;
      }
    }

    // Add remaining elements
    while (leftIndex < left.length) {
      result.push(left[leftIndex]);
      leftIndex++;
    }

    while (rightIndex < right.length) {
      result.push(right[rightIndex]);
      rightIndex++;
    }

    return result;
  }

  // In-place merge sort to save memory
  static sortInPlace(arr, compareFunction = (a, b) => a - b) {
    const result = [...arr];
    this._mergeSortInPlace(result, 0, result.length - 1, compareFunction);
    return result;
  }

  static _mergeSortInPlace(arr, left, right, compareFunction) {
    if (left >= right) return;

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

    this._mergeSortInPlace(arr, left, mid, compareFunction);
    this._mergeSortInPlace(arr, mid + 1, right, compareFunction);
    this._mergeInPlace(arr, left, mid, right, compareFunction);
  }

  static _mergeInPlace(arr, left, mid, right, compareFunction) {
    const leftArray = arr.slice(left, mid + 1);
    const rightArray = arr.slice(mid + 1, right + 1);

    let i = 0,
      j = 0,
      k = left;

    while (i < leftArray.length && j < rightArray.length) {
      if (compareFunction(leftArray[i], rightArray[j]) <= 0) {
        arr[k] = leftArray[i];
        i++;
      } else {
        arr[k] = rightArray[j];
        j++;
      }
      k++;
    }

    while (i < leftArray.length) {
      arr[k] = leftArray[i];
      i++;
      k++;
    }

    while (j < rightArray.length) {
      arr[k] = rightArray[j];
      j++;
      k++;
    }
  }
}

// Custom object sorting
const people = [
  { name: 'Alice', age: 30 },
  { name: 'Bob', age: 25 },
  { name: 'Charlie', age: 35 },
];

const sortedByAge = MergeSort.sort(people, (a, b) => a.age - b.age);
console.log(sortedByAge);

Heap Sort

// Heap Sort - In-place O(n log n) algorithm
class HeapSort {
  static sort(arr, compareFunction = (a, b) => a - b) {
    const result = [...arr];
    const n = result.length;

    // Build max heap
    for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
      this._heapify(result, n, i, compareFunction);
    }

    // Extract elements from heap one by one
    for (let i = n - 1; i > 0; i--) {
      // Move current root to end
      [result[0], result[i]] = [result[i], result[0]];

      // Call heapify on the reduced heap
      this._heapify(result, i, 0, compareFunction);
    }

    return result;
  }

  static _heapify(arr, n, i, compareFunction) {
    let largest = i;
    const left = 2 * i + 1;
    const right = 2 * i + 2;

    // If left child is larger than root
    if (left < n && compareFunction(arr[left], arr[largest]) > 0) {
      largest = left;
    }

    // If right child is larger than largest so far
    if (right < n && compareFunction(arr[right], arr[largest]) > 0) {
      largest = right;
    }

    // If largest is not root
    if (largest !== i) {
      [arr[i], arr[largest]] = [arr[largest], arr[i]];

      // Recursively heapify the affected sub-tree
      this._heapify(arr, n, largest, compareFunction);
    }
  }
}

// Heap data structure for priority queues
class Heap {
  constructor(compareFunction = (a, b) => a - b) {
    this.items = [];
    this.compare = compareFunction;
  }

  insert(item) {
    this.items.push(item);
    this._heapifyUp(this.items.length - 1);
    return this;
  }

  extractMax() {
    if (this.items.length === 0) return null;
    if (this.items.length === 1) return this.items.pop();

    const max = this.items[0];
    this.items[0] = this.items.pop();
    this._heapifyDown(0);

    return max;
  }

  peek() {
    return this.items.length > 0 ? this.items[0] : null;
  }

  size() {
    return this.items.length;
  }

  isEmpty() {
    return this.items.length === 0;
  }

  _heapifyUp(index) {
    if (index === 0) return;

    const parentIndex = Math.floor((index - 1) / 2);

    if (this.compare(this.items[index], this.items[parentIndex]) > 0) {
      [this.items[index], this.items[parentIndex]] = [
        this.items[parentIndex],
        this.items[index],
      ];
      this._heapifyUp(parentIndex);
    }
  }

  _heapifyDown(index) {
    const leftChild = 2 * index + 1;
    const rightChild = 2 * index + 2;
    let largest = index;

    if (
      leftChild < this.items.length &&
      this.compare(this.items[leftChild], this.items[largest]) > 0
    ) {
      largest = leftChild;
    }

    if (
      rightChild < this.items.length &&
      this.compare(this.items[rightChild], this.items[largest]) > 0
    ) {
      largest = rightChild;
    }

    if (largest !== index) {
      [this.items[index], this.items[largest]] = [
        this.items[largest],
        this.items[index],
      ];
      this._heapifyDown(largest);
    }
  }
}

// Usage
console.log(HeapSort.sort([12, 11, 13, 5, 6, 7])); // [5, 6, 7, 11, 12, 13]

const maxHeap = new Heap();
[3, 1, 4, 1, 5, 9, 2, 6].forEach((n) => maxHeap.insert(n));
console.log(maxHeap.extractMax()); // 9

Searching Algorithms

Binary Search

// Binary Search - O(log n) for sorted arrays
class BinarySearch {
  static search(arr, target, compareFunction = (a, b) => a - b) {
    let left = 0;
    let right = arr.length - 1;

    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      const comparison = compareFunction(arr[mid], target);

      if (comparison === 0) {
        return mid; // Found target
      } else if (comparison < 0) {
        left = mid + 1; // Target is in right half
      } else {
        right = mid - 1; // Target is in left half
      }
    }

    return -1; // Target not found
  }

  // Find first occurrence of target
  static searchFirst(arr, target, compareFunction = (a, b) => a - b) {
    let left = 0;
    let right = arr.length - 1;
    let result = -1;

    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      const comparison = compareFunction(arr[mid], target);

      if (comparison === 0) {
        result = mid;
        right = mid - 1; // Continue searching in left half
      } else if (comparison < 0) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }

    return result;
  }

  // Find last occurrence of target
  static searchLast(arr, target, compareFunction = (a, b) => a - b) {
    let left = 0;
    let right = arr.length - 1;
    let result = -1;

    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      const comparison = compareFunction(arr[mid], target);

      if (comparison === 0) {
        result = mid;
        left = mid + 1; // Continue searching in right half
      } else if (comparison < 0) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }

    return result;
  }

  // Find insertion point for target
  static searchInsertionPoint(arr, target, compareFunction = (a, b) => a - b) {
    let left = 0;
    let right = arr.length;

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

      if (compareFunction(arr[mid], target) < 0) {
        left = mid + 1;
      } else {
        right = mid;
      }
    }

    return left;
  }

  // Binary search on answer (for optimization problems)
  static searchAnswer(predicate, low, high, precision = 1e-9) {
    while (high - low > precision) {
      const mid = (low + high) / 2;

      if (predicate(mid)) {
        high = mid;
      } else {
        low = mid;
      }
    }

    return (low + high) / 2;
  }
}

// Usage examples
const sortedArray = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
console.log(BinarySearch.search(sortedArray, 7)); // 3
console.log(BinarySearch.search(sortedArray, 8)); // -1

const duplicateArray = [1, 2, 2, 2, 3, 4, 5];
console.log(BinarySearch.searchFirst(duplicateArray, 2)); // 1
console.log(BinarySearch.searchLast(duplicateArray, 2)); // 3

// Binary search on answer example: find square root
function findSquareRoot(x, precision = 1e-6) {
  return BinarySearch.searchAnswer((mid) => mid * mid >= x, 0, x, precision);
}

console.log(findSquareRoot(25)); // ~5.0
console.log(findSquareRoot(2)); // ~1.414

Depth-First Search (DFS)

// Graph representation and DFS implementation
class Graph {
  constructor() {
    this.adjacencyList = new Map();
  }

  addVertex(vertex) {
    if (!this.adjacencyList.has(vertex)) {
      this.adjacencyList.set(vertex, []);
    }
    return this;
  }

  addEdge(vertex1, vertex2, directed = false) {
    this.addVertex(vertex1);
    this.addVertex(vertex2);

    this.adjacencyList.get(vertex1).push(vertex2);

    if (!directed) {
      this.adjacencyList.get(vertex2).push(vertex1);
    }

    return this;
  }

  // Recursive DFS
  dfsRecursive(startVertex, callback) {
    const visited = new Set();
    const result = [];

    const dfs = (vertex) => {
      if (visited.has(vertex)) return;

      visited.add(vertex);
      result.push(vertex);

      if (callback) callback(vertex);

      const neighbors = this.adjacencyList.get(vertex) || [];
      for (const neighbor of neighbors) {
        dfs(neighbor);
      }
    };

    dfs(startVertex);
    return result;
  }

  // Iterative DFS using stack
  dfsIterative(startVertex, callback) {
    const visited = new Set();
    const stack = [startVertex];
    const result = [];

    while (stack.length > 0) {
      const vertex = stack.pop();

      if (visited.has(vertex)) continue;

      visited.add(vertex);
      result.push(vertex);

      if (callback) callback(vertex);

      const neighbors = this.adjacencyList.get(vertex) || [];
      // Add neighbors in reverse order to maintain left-to-right traversal
      for (let i = neighbors.length - 1; i >= 0; i--) {
        if (!visited.has(neighbors[i])) {
          stack.push(neighbors[i]);
        }
      }
    }

    return result;
  }

  // Find path between two vertices using DFS
  findPath(start, end) {
    const visited = new Set();
    const path = [];

    const dfs = (vertex) => {
      if (visited.has(vertex)) return false;

      visited.add(vertex);
      path.push(vertex);

      if (vertex === end) return true;

      const neighbors = this.adjacencyList.get(vertex) || [];
      for (const neighbor of neighbors) {
        if (dfs(neighbor)) return true;
      }

      path.pop(); // Backtrack
      return false;
    };

    return dfs(start) ? path : null;
  }

  // Check if graph has cycle (for directed graph)
  hasCycle() {
    const color = new Map(); // 0: white, 1: gray, 2: black

    // Initialize all vertices as white
    for (const vertex of this.adjacencyList.keys()) {
      color.set(vertex, 0);
    }

    const dfs = (vertex) => {
      color.set(vertex, 1); // Mark as gray (visiting)

      const neighbors = this.adjacencyList.get(vertex) || [];
      for (const neighbor of neighbors) {
        if (color.get(neighbor) === 1) {
          return true; // Back edge found - cycle detected
        }
        if (color.get(neighbor) === 0 && dfs(neighbor)) {
          return true;
        }
      }

      color.set(vertex, 2); // Mark as black (visited)
      return false;
    };

    for (const vertex of this.adjacencyList.keys()) {
      if (color.get(vertex) === 0 && dfs(vertex)) {
        return true;
      }
    }

    return false;
  }

  // Topological sort using DFS
  topologicalSort() {
    const visited = new Set();
    const stack = [];

    const dfs = (vertex) => {
      visited.add(vertex);

      const neighbors = this.adjacencyList.get(vertex) || [];
      for (const neighbor of neighbors) {
        if (!visited.has(neighbor)) {
          dfs(neighbor);
        }
      }

      stack.push(vertex);
    };

    for (const vertex of this.adjacencyList.keys()) {
      if (!visited.has(vertex)) {
        dfs(vertex);
      }
    }

    return stack.reverse();
  }
}

// Usage examples
const graph = new Graph();
graph.addEdge('A', 'B');
graph.addEdge('A', 'C');
graph.addEdge('B', 'D');
graph.addEdge('C', 'E');
graph.addEdge('D', 'E');
graph.addEdge('D', 'F');
graph.addEdge('E', 'F');

console.log(graph.dfsRecursive('A')); // ['A', 'B', 'D', 'E', 'C', 'F']
console.log(graph.findPath('A', 'F')); // ['A', 'B', 'D', 'F']

Breadth-First Search (BFS)

// BFS implementation for shortest path
class BFSGraph extends Graph {
  // Standard BFS traversal
  bfs(startVertex, callback) {
    const visited = new Set();
    const queue = [startVertex];
    const result = [];

    visited.add(startVertex);

    while (queue.length > 0) {
      const vertex = queue.shift();
      result.push(vertex);

      if (callback) callback(vertex);

      const neighbors = this.adjacencyList.get(vertex) || [];
      for (const neighbor of neighbors) {
        if (!visited.has(neighbor)) {
          visited.add(neighbor);
          queue.push(neighbor);
        }
      }
    }

    return result;
  }

  // Find shortest path using BFS
  shortestPath(start, end) {
    const visited = new Set();
    const queue = [{ vertex: start, path: [start] }];

    visited.add(start);

    while (queue.length > 0) {
      const { vertex, path } = queue.shift();

      if (vertex === end) {
        return path;
      }

      const neighbors = this.adjacencyList.get(vertex) || [];
      for (const neighbor of neighbors) {
        if (!visited.has(neighbor)) {
          visited.add(neighbor);
          queue.push({
            vertex: neighbor,
            path: [...path, neighbor],
          });
        }
      }
    }

    return null; // No path found
  }

  // Find shortest distance to all vertices
  shortestDistances(start) {
    const distances = new Map();
    const visited = new Set();
    const queue = [{ vertex: start, distance: 0 }];

    distances.set(start, 0);
    visited.add(start);

    while (queue.length > 0) {
      const { vertex, distance } = queue.shift();

      const neighbors = this.adjacencyList.get(vertex) || [];
      for (const neighbor of neighbors) {
        if (!visited.has(neighbor)) {
          visited.add(neighbor);
          distances.set(neighbor, distance + 1);
          queue.push({
            vertex: neighbor,
            distance: distance + 1,
          });
        }
      }
    }

    return distances;
  }

  // Check if graph is bipartite using BFS
  isBipartite() {
    const color = new Map();

    for (const startVertex of this.adjacencyList.keys()) {
      if (color.has(startVertex)) continue;

      const queue = [startVertex];
      color.set(startVertex, 0);

      while (queue.length > 0) {
        const vertex = queue.shift();
        const currentColor = color.get(vertex);

        const neighbors = this.adjacencyList.get(vertex) || [];
        for (const neighbor of neighbors) {
          if (!color.has(neighbor)) {
            color.set(neighbor, 1 - currentColor);
            queue.push(neighbor);
          } else if (color.get(neighbor) === currentColor) {
            return false; // Same color adjacent vertices
          }
        }
      }
    }

    return true;
  }

  // Find connected components
  connectedComponents() {
    const visited = new Set();
    const components = [];

    for (const vertex of this.adjacencyList.keys()) {
      if (!visited.has(vertex)) {
        const component = [];
        const queue = [vertex];
        visited.add(vertex);

        while (queue.length > 0) {
          const current = queue.shift();
          component.push(current);

          const neighbors = this.adjacencyList.get(current) || [];
          for (const neighbor of neighbors) {
            if (!visited.has(neighbor)) {
              visited.add(neighbor);
              queue.push(neighbor);
            }
          }
        }

        components.push(component);
      }
    }

    return components;
  }
}

// Usage examples
const bfsGraph = new BFSGraph();
bfsGraph.addEdge('A', 'B');
bfsGraph.addEdge('A', 'C');
bfsGraph.addEdge('B', 'D');
bfsGraph.addEdge('C', 'E');
bfsGraph.addEdge('D', 'F');
bfsGraph.addEdge('E', 'F');

console.log(bfsGraph.bfs('A')); // ['A', 'B', 'C', 'D', 'E', 'F']
console.log(bfsGraph.shortestPath('A', 'F')); // ['A', 'B', 'D', 'F']
console.log(bfsGraph.shortestDistances('A')); // Map of distances

Dynamic Programming

Common DP Problems

// Dynamic Programming utilities and common problems
class DynamicProgramming {
  // Memoization decorator
  static memoize(fn) {
    const cache = new Map();

    return function (...args) {
      const key = JSON.stringify(args);

      if (cache.has(key)) {
        return cache.get(key);
      }

      const result = fn.apply(this, args);
      cache.set(key, result);
      return result;
    };
  }

  // Fibonacci with memoization
  static fibonacci = this.memoize((n) => {
    if (n <= 1) return n;
    return (
      DynamicProgramming.fibonacci(n - 1) + DynamicProgramming.fibonacci(n - 2)
    );
  });

  // Fibonacci with tabulation (bottom-up)
  static fibonacciTabulation(n) {
    if (n <= 1) return n;

    const dp = [0, 1];

    for (let i = 2; i <= n; i++) {
      dp[i] = dp[i - 1] + dp[i - 2];
    }

    return dp[n];
  }

  // Fibonacci with space optimization
  static fibonacciOptimized(n) {
    if (n <= 1) return n;

    let prev2 = 0;
    let prev1 = 1;

    for (let i = 2; i <= n; i++) {
      const current = prev1 + prev2;
      prev2 = prev1;
      prev1 = current;
    }

    return prev1;
  }

  // Longest Common Subsequence
  static longestCommonSubsequence(str1, str2) {
    const m = str1.length;
    const n = str2.length;
    const dp = Array(m + 1)
      .fill()
      .map(() => Array(n + 1).fill(0));

    for (let i = 1; i <= m; i++) {
      for (let j = 1; j <= n; j++) {
        if (str1[i - 1] === str2[j - 1]) {
          dp[i][j] = dp[i - 1][j - 1] + 1;
        } else {
          dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
        }
      }
    }

    return dp[m][n];
  }

  // Knapsack Problem (0/1)
  static knapsack(weights, values, capacity) {
    const n = weights.length;
    const dp = Array(n + 1)
      .fill()
      .map(() => Array(capacity + 1).fill(0));

    for (let i = 1; i <= n; i++) {
      for (let w = 1; w <= capacity; w++) {
        if (weights[i - 1] <= w) {
          dp[i][w] = Math.max(
            values[i - 1] + dp[i - 1][w - weights[i - 1]],
            dp[i - 1][w]
          );
        } else {
          dp[i][w] = dp[i - 1][w];
        }
      }
    }

    return dp[n][capacity];
  }

  // Coin Change Problem
  static coinChange(coins, amount) {
    const dp = Array(amount + 1).fill(Infinity);
    dp[0] = 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];
  }

  // Edit Distance (Levenshtein Distance)
  static editDistance(str1, str2) {
    const m = str1.length;
    const n = str2.length;
    const dp = Array(m + 1)
      .fill()
      .map(() => Array(n + 1).fill(0));

    // Initialize base cases
    for (let i = 0; i <= m; i++) dp[i][0] = i;
    for (let j = 0; j <= n; j++) dp[0][j] = j;

    for (let i = 1; i <= m; i++) {
      for (let j = 1; j <= n; j++) {
        if (str1[i - 1] === str2[j - 1]) {
          dp[i][j] = dp[i - 1][j - 1];
        } else {
          dp[i][j] =
            1 +
            Math.min(
              dp[i - 1][j], // deletion
              dp[i][j - 1], // insertion
              dp[i - 1][j - 1] // substitution
            );
        }
      }
    }

    return dp[m][n];
  }

  // Maximum Subarray Sum (Kadane's Algorithm)
  static maxSubarraySum(arr) {
    let maxSoFar = arr[0];
    let maxEndingHere = arr[0];

    for (let i = 1; i < arr.length; i++) {
      maxEndingHere = Math.max(arr[i], maxEndingHere + arr[i]);
      maxSoFar = Math.max(maxSoFar, maxEndingHere);
    }

    return maxSoFar;
  }

  // Unique Paths in Grid
  static uniquePaths(m, n) {
    const dp = Array(m)
      .fill()
      .map(() => Array(n).fill(1));

    for (let i = 1; i < m; i++) {
      for (let j = 1; j < n; j++) {
        dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
      }
    }

    return dp[m - 1][n - 1];
  }

  // House Robber Problem
  static houseRobber(nums) {
    if (nums.length === 0) return 0;
    if (nums.length === 1) return nums[0];

    let prev2 = 0;
    let prev1 = nums[0];

    for (let i = 1; i < nums.length; i++) {
      const current = Math.max(prev1, prev2 + nums[i]);
      prev2 = prev1;
      prev1 = current;
    }

    return prev1;
  }
}

// Usage examples
console.log(DynamicProgramming.fibonacci(10)); // 55
console.log(DynamicProgramming.longestCommonSubsequence('ABCDGH', 'AEDFHR')); // 3
console.log(DynamicProgramming.knapsack([1, 3, 4, 5], [1, 4, 5, 7], 7)); // 9
console.log(DynamicProgramming.coinChange([1, 3, 4], 6)); // 2
console.log(DynamicProgramming.editDistance('kitten', 'sitting')); // 3
console.log(DynamicProgramming.maxSubarraySum([-2, 1, -3, 4, -1, 2, 1, -5, 4])); // 6

Algorithm Analysis

// Performance analysis and benchmarking tools
class AlgorithmAnalysis {
  // Time a function execution
  static timeFunction(fn, ...args) {
    const start = performance.now();
    const result = fn(...args);
    const end = performance.now();

    return {
      result,
      timeMs: end - start,
      timeFormatted: this.formatTime(end - start),
    };
  }

  // Memory usage estimation (approximate)
  static estimateMemoryUsage(data) {
    const seen = new WeakSet();

    function sizeOf(obj) {
      if (obj === null || typeof obj !== 'object') {
        return 8; // Approximate size for primitives
      }

      if (seen.has(obj)) {
        return 0; // Already counted
      }

      seen.add(obj);

      let size = 0;

      if (Array.isArray(obj)) {
        size += obj.length * 8; // Approximate pointer size
        for (const item of obj) {
          size += sizeOf(item);
        }
      } else {
        const keys = Object.keys(obj);
        size += keys.length * 16; // Key-value pair overhead

        for (const key of keys) {
          size += key.length * 2; // String key size
          size += sizeOf(obj[key]);
        }
      }

      return size;
    }

    return sizeOf(data);
  }

  // Compare algorithm performance
  static compareAlgorithms(algorithms, testCases) {
    const results = [];

    for (const testCase of testCases) {
      const caseResults = {
        input: testCase.name || `Input size: ${testCase.data.length}`,
        results: [],
      };

      for (const algorithm of algorithms) {
        const timing = this.timeFunction(algorithm.fn, ...testCase.data);

        caseResults.results.push({
          name: algorithm.name,
          time: timing.timeMs,
          timeFormatted: timing.timeFormatted,
          result: timing.result,
        });
      }

      results.push(caseResults);
    }

    return results;
  }

  // Generate test data
  static generateTestData(size, type = 'random') {
    switch (type) {
      case 'random':
        return Array.from({ length: size }, () =>
          Math.floor(Math.random() * 1000)
        );

      case 'sorted':
        return Array.from({ length: size }, (_, i) => i);

      case 'reverse':
        return Array.from({ length: size }, (_, i) => size - i - 1);

      case 'nearly_sorted':
        const arr = Array.from({ length: size }, (_, i) => i);
        // Swap a few random elements
        for (let i = 0; i < size * 0.1; i++) {
          const idx1 = Math.floor(Math.random() * size);
          const idx2 = Math.floor(Math.random() * size);
          [arr[idx1], arr[idx2]] = [arr[idx2], arr[idx1]];
        }
        return arr;

      default:
        throw new Error(`Unknown test data type: ${type}`);
    }
  }

  // Format time for display
  static formatTime(ms) {
    if (ms < 1) {
      return `${(ms * 1000).toFixed(2)}μs`;
    } else if (ms < 1000) {
      return `${ms.toFixed(2)}ms`;
    } else {
      return `${(ms / 1000).toFixed(2)}s`;
    }
  }

  // Big O complexity analyzer (simplified)
  static analyzeComplexity(fn, maxSize = 1000, step = 100) {
    const measurements = [];

    for (let size = step; size <= maxSize; size += step) {
      const data = this.generateTestData(size);
      const timing = this.timeFunction(fn, data);

      measurements.push({
        size,
        time: timing.timeMs,
      });
    }

    // Simple heuristic to guess complexity
    const ratios = [];
    for (let i = 1; i < measurements.length; i++) {
      const prev = measurements[i - 1];
      const curr = measurements[i];

      const sizeRatio = curr.size / prev.size;
      const timeRatio = curr.time / prev.time;

      ratios.push(timeRatio / sizeRatio);
    }

    const avgRatio = ratios.reduce((a, b) => a + b, 0) / ratios.length;

    let complexity;
    if (avgRatio < 1.5) {
      complexity = 'O(n)';
    } else if (avgRatio < 2.5) {
      complexity = 'O(n log n)';
    } else if (avgRatio < 4) {
      complexity = 'O(n²)';
    } else {
      complexity = 'O(n³) or higher';
    }

    return {
      measurements,
      estimatedComplexity: complexity,
      averageRatio: avgRatio,
    };
  }
}

// Usage examples
const sortingAlgorithms = [
  { name: 'Quick Sort', fn: QuickSort.sort },
  { name: 'Merge Sort', fn: MergeSort.sort },
  { name: 'Heap Sort', fn: HeapSort.sort },
  { name: 'Bubble Sort', fn: BubbleSort.sort },
];

const testCases = [
  { name: 'Small Random', data: [AlgorithmAnalysis.generateTestData(100)] },
  { name: 'Medium Random', data: [AlgorithmAnalysis.generateTestData(1000)] },
  {
    name: 'Sorted Data',
    data: [AlgorithmAnalysis.generateTestData(1000, 'sorted')],
  },
];

const comparison = AlgorithmAnalysis.compareAlgorithms(
  sortingAlgorithms,
  testCases
);
console.log('Algorithm Comparison:', comparison);

// Analyze complexity
const complexityAnalysis = AlgorithmAnalysis.analyzeComplexity(QuickSort.sort);
console.log('Quick Sort Complexity Analysis:', complexityAnalysis);

Conclusion

Mastering algorithms in JavaScript requires understanding both the theoretical concepts and practical implementations. Start with basic sorting and searching algorithms, then progress to more complex topics like graph algorithms and dynamic programming. Practice implementing these algorithms from scratch, analyze their time and space complexity, and learn when to apply each one. Remember that choosing the right algorithm for your specific use case is often more important than knowing every algorithm in detail.