重新学习前端之算法

1 阅读34分钟

算法

一、排序算法

1. 排序算法概述

定义: 排序算法是将一组数据按照某种特定顺序(升序或降序)重新排列的算法。

原理: 排序算法通过比较或分配元素的方式来确定元素之间的相对顺序,最终使整个序列满足有序性。

分类:

  • 比较排序:通过比较元素大小来决定顺序(冒泡、选择、插入、快排、归并、堆排序)
  • 非比较排序:不通过比较,利用数据特性直接确定位置(计数排序、桶排序、基数排序)

常见误区:

  • 排序算法的稳定性不等于性能,稳定排序不一定慢
  • 时间复杂度最优的不一定是最实用的,需要考虑常数因子和实际数据特征
  • "原地排序"指的是空间复杂度为 O(1),不代表不使用额外空间

2. 冒泡排序(Bubble Sort)

定义: 冒泡排序是一种简单的比较排序算法,通过重复遍历数组,比较相邻元素并交换位置,使较大的元素像气泡一样"浮"到数组末尾。

原理:

  1. 从数组第一个元素开始,比较相邻的两个元素
  2. 如果前一个大于后一个,则交换它们
  3. 对每一对相邻元素重复步骤1-2,直到最后一对,此时最大元素已到达末尾
  4. 重复上述过程,但每次排除已排序的末尾元素
  5. 直到没有交换发生,排序完成

代码实现:

// 基础版本
function bubbleSort(arr) {
  const n = arr.length;
  for (let i = 0; i < n - 1; i++) {
    for (let j = 0; j < n - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
}

// 优化版本(加入提前退出机制)
function bubbleSortOptimized(arr) {
  const n = arr.length;
  for (let i = 0; i < n - 1; i++) {
    let swapped = false;
    for (let j = 0; j < n - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        swapped = true;
      }
    }
    if (!swapped) break; // 数组已有序,提前退出
  }
  return arr;
}

// 示例
console.log(bubbleSortOptimized([64, 34, 25, 12, 22, 11, 90]));
// 输出: [11, 12, 22, 25, 34, 64, 90]

复杂度分析:

  • 时间复杂度:O(n^2)(最坏/平均),O(n)(最好,已有序)
  • 空间复杂度:O(1)(原地排序)
  • 稳定性:稳定

常见误区:

  • 认为冒泡排序完全无用,实际上对于小规模或接近有序的数据,优化后的冒泡排序表现不错
  • 忘记优化版本的提前退出机制

3. 选择排序(Selection Sort)

定义: 选择排序每次从未排序部分选择最小(或最大)元素,放到已排序部分的末尾。

原理:

  1. 在未排序序列中找到最小元素
  2. 将其与未排序序列的第一个元素交换
  3. 剩余未排序部分继续重复步骤1-2
  4. 直到所有元素排序完成

代码实现:

function selectionSort(arr) {
  const n = arr.length;
  for (let i = 0; i < n - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < n; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    if (minIndex !== i) {
      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
  }
  return arr;
}

// 示例
console.log(selectionSort([64, 25, 12, 22, 11]));
// 输出: [11, 12, 22, 25, 64]

复杂度分析:

  • 时间复杂度:O(n^2)(所有情况)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

常见误区:

  • 认为选择排序是稳定的,实际上交换操作可能破坏稳定性

4. 插入排序(Insertion Sort)

定义: 插入排序将数组分为已排序和未排序两部分,每次取未排序的第一个元素,插入到已排序部分的正确位置。

原理:

  1. 从第二个元素开始,将其与已排序部分比较
  2. 如果已排序元素大于当前元素,则将已排序元素后移
  3. 重复步骤2,直到找到正确位置
  4. 插入当前元素
  5. 继续处理下一个未排序元素

代码实现:

function insertionSort(arr) {
  const n = arr.length;
  for (let i = 1; i < n; i++) {
    let current = arr[i];
    let j = i - 1;
    while (j >= 0 && arr[j] > current) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = current;
  }
  return arr;
}

// 示例
console.log(insertionSort([12, 11, 13, 5, 6]));
// 输出: [5, 6, 11, 12, 13]

复杂度分析:

  • 时间复杂度:O(n^2)(最坏/平均),O(n)(最好)
  • 空间复杂度:O(1)
  • 稳定性:稳定

最佳实践:

  • 对于小规模数据(n < 50)或接近有序的数据,插入排序非常高效
  • 常用于优化快速排序(当子数组较小时切换为插入排序)

5. 快速排序(Quick Sort)

定义: 快速排序采用分治策略,选择一个"基准"元素,将数组分为小于基准和大于基准的两部分,然后递归排序这两部分。

原理:

  1. 选择基准元素(通常选第一个、最后一个或中间元素)
  2. 分区操作:将小于基准的放左边,大于基准的放右边
  3. 递归对左右两部分进行快速排序
  4. 基准选择策略影响性能

代码实现:

// 基础版本
function quickSort(arr) {
  if (arr.length <= 1) return arr;
  
  const pivot = arr[arr.length - 1];
  const left = [];
  const right = [];
  
  for (let i = 0; i < arr.length - 1; i++) {
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  
  return [...quickSort(left), pivot, ...quickSort(right)];
}

// 原地排序优化版本
function quickSortInPlace(arr, left = 0, right = arr.length - 1) {
  if (left < right) {
    const pivotIndex = partition(arr, left, right);
    quickSortInPlace(arr, left, pivotIndex - 1);
    quickSortInPlace(arr, pivotIndex + 1, right);
  }
  return arr;
}

function partition(arr, left, right) {
  const pivot = arr[right];
  let i = left - 1;
  
  for (let j = left; j < right; j++) {
    if (arr[j] <= pivot) {
      i++;
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
  }
  [arr[i + 1], arr[right]] = [arr[right], arr[i + 1]];
  return i + 1;
}

// 示例
console.log(quickSort([3, 6, 8, 10, 1, 2, 1]));
// 输出: [1, 1, 2, 3, 6, 8, 10]

复杂度分析:

  • 时间复杂度:O(n log n)(平均),O(n^2)(最坏)
  • 空间复杂度:O(log n)(递归栈)
  • 稳定性:不稳定

最佳实践:

  • 基准选择:使用三数取中法或随机选择避免最坏情况
  • 小数组切换:当子数组小于阈值(如10)时切换为插入排序
  • 尾递归优化:先递归较小子数组

6. 归并排序(Merge Sort)

定义: 归并排序采用分治策略,将数组递归地分成两半,分别排序后再合并。

原理:

  1. 分解:将数组分成两个子数组
  2. 解决:递归排序两个子数组
  3. 合并:将两个已排序的子数组合并为一个有序数组

代码实现:

function mergeSort(arr) {
  if (arr.length <= 1) return arr;
  
  const mid = Math.floor(arr.length / 2);
  const left = arr.slice(0, mid);
  const right = arr.slice(mid);
  
  return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
  const result = [];
  let i = 0, j = 0;
  
  while (i < left.length && j < right.length) {
    if (left[i] <= right[j]) {
      result.push(left[i]);
      i++;
    } else {
      result.push(right[j]);
      j++;
    }
  }
  
  return [...result, ...left.slice(i), ...right.slice(j)];
}

// 示例
console.log(mergeSort([38, 27, 43, 3, 9, 82, 10]));
// 输出: [3, 9, 10, 27, 38, 43, 82]

复杂度分析:

  • 时间复杂度:O(n log n)(所有情况)
  • 空间复杂度:O(n)
  • 稳定性:稳定

最佳实践:

  • 归并排序适合外部排序(大数据量)
  • 对于链表排序,归并排序是首选(不需要额外空间)
  • 常用于需要稳定排序的场景

7. 堆排序(Heap Sort)

定义: 堆排序利用堆数据结构的特性,先将数组构造成最大堆,然后反复取出堆顶元素并调整堆。

原理:

  1. 构建最大堆(父节点大于子节点)
  2. 将堆顶元素(最大值)与末尾元素交换
  3. 缩小堆的范围,重新调整堆
  4. 重复步骤2-3直到堆为空

代码实现:

function heapSort(arr) {
  const n = arr.length;
  
  // 构建最大堆
  for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
    heapify(arr, n, i);
  }
  
  // 逐个提取元素
  for (let i = n - 1; i > 0; i--) {
    [arr[0], arr[i]] = [arr[i], arr[0]];
    heapify(arr, i, 0);
  }
  
  return arr;
}

function heapify(arr, n, i) {
  let largest = i;
  const left = 2 * i + 1;
  const right = 2 * i + 2;
  
  if (left < n && arr[left] > arr[largest]) {
    largest = left;
  }
  
  if (right < n && arr[right] > arr[largest]) {
    largest = right;
  }
  
  if (largest !== i) {
    [arr[i], arr[largest]] = [arr[largest], arr[i]];
    heapify(arr, n, largest);
  }
}

// 示例
console.log(heapSort([12, 11, 13, 5, 6, 7]));
// 输出: [5, 6, 7, 11, 12, 13]

复杂度分析:

  • 时间复杂度:O(n log n)(所有情况)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

8. 希尔排序(Shell Sort)

定义: 希尔排序是插入排序的改进版本,通过比较相距一定间隔的元素来减少数据移动次数。

原理:

  1. 选择一个增量序列(通常为 n/2, n/4, ..., 1)
  2. 按增量将数组分组,对每组进行插入排序
  3. 缩小增量,重复步骤2
  4. 当增量为1时,进行最后一次插入排序

代码实现:

function shellSort(arr) {
  const n = arr.length;
  let gap = Math.floor(n / 2);
  
  while (gap > 0) {
    for (let i = gap; i < n; i++) {
      let current = arr[i];
      let j = i;
      while (j >= gap && arr[j - gap] > current) {
        arr[j] = arr[j - gap];
        j -= gap;
      }
      arr[j] = current;
    }
    gap = Math.floor(gap / 2);
  }
  
  return arr;
}

// 示例
console.log(shellSort([12, 34, 54, 2, 3]));
// 输出: [2, 3, 12, 34, 54]

复杂度分析:

  • 时间复杂度:O(n log n) ~ O(n^2)(取决于增量序列)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

9. 计数排序(Counting Sort)

定义: 计数排序是一种非比较排序,通过统计每个元素出现的次数来确定其位置。

代码实现:

function countingSort(arr) {
  if (arr.length === 0) return arr;
  
  const max = Math.max(...arr);
  const min = Math.min(...arr);
  const range = max - min + 1;
  const count = new Array(range).fill(0);
  const output = new Array(arr.length);
  
  // 统计频率
  for (let i = 0; i < arr.length; i++) {
    count[arr[i] - min]++;
  }
  
  // 累加计数
  for (let i = 1; i < count.length; i++) {
    count[i] += count[i - 1];
  }
  
  // 构建输出数组
  for (let i = arr.length - 1; i >= 0; i--) {
    output[count[arr[i] - min] - 1] = arr[i];
    count[arr[i] - min]--;
  }
  
  return output;
}

复杂度分析:

  • 时间复杂度:O(n + k),k为数据范围
  • 空间复杂度:O(k)
  • 稳定性:稳定

10. 桶排序(Bucket Sort)

定义: 桶排序将数据分配到有限数量的桶中,然后对每个桶内的数据单独排序。

代码实现:

function bucketSort(arr, bucketSize = 5) {
  if (arr.length === 0) return arr;
  
  const min = Math.min(...arr);
  const max = Math.max(...arr);
  const bucketCount = Math.floor((max - min) / bucketSize) + 1;
  const buckets = new Array(bucketCount).fill(null).map(() => []);
  
  // 分配到桶中
  for (let i = 0; i < arr.length; i++) {
    buckets[Math.floor((arr[i] - min) / bucketSize)].push(arr[i]);
  }
  
  // 对每个桶排序并合并
  const result = [];
  for (let i = 0; i < buckets.length; i++) {
    insertionSort(buckets[i]);
    result.push(...buckets[i]);
  }
  
  return result;
}

11. 基数排序(Radix Sort)

定义: 基数排序按位排序,从最低位到最高位依次进行稳定排序。

代码实现:

function radixSort(arr) {
  const max = Math.max(...arr);
  let exp = 1;
  
  while (Math.floor(max / exp) > 0) {
    countingSortByDigit(arr, exp);
    exp *= 10;
  }
  
  return arr;
}

function countingSortByDigit(arr, exp) {
  const n = arr.length;
  const output = new Array(n);
  const count = new Array(10).fill(0);
  
  for (let i = 0; i < n; i++) {
    count[Math.floor(arr[i] / exp) % 10]++;
  }
  
  for (let i = 1; i < 10; i++) {
    count[i] += count[i - 1];
  }
  
  for (let i = n - 1; i >= 0; i--) {
    output[count[Math.floor(arr[i] / exp) % 10] - 1] = arr[i];
    count[Math.floor(arr[i] / exp) % 10]--;
  }
  
  for (let i = 0; i < n; i++) {
    arr[i] = output[i];
  }
}

12. 排序算法比较

算法最好时间平均时间最坏时间空间复杂度稳定性
冒泡排序O(n)O(n^2)O(n^2)O(1)稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
插入排序O(n)O(n^2)O(n^2)O(1)稳定
快速排序O(n log n)O(n log n)O(n^2)O(log n)不稳定
归并排序O(n log n)O(n log n)O(n log n)O(n)稳定
堆排序O(n log n)O(n log n)O(n log n)O(1)不稳定
希尔排序O(n log n)O(n log n)O(n^2)O(1)不稳定
计数排序O(n + k)O(n + k)O(n + k)O(k)稳定
桶排序O(n + k)O(n + k)O(n^2)O(n + k)稳定
基数排序O(n * d)O(n * d)O(n * d)O(n + k)稳定

13. 排序算法稳定性

定义: 排序算法的稳定性是指相等元素的相对顺序在排序后是否保持不变。

稳定排序算法: 冒泡排序、插入排序、归并排序、计数排序、桶排序、基数排序

不稳定排序算法: 选择排序、快速排序、堆排序、希尔排序

选择策略:

  • 如果需要稳定排序,优先选择归并排序
  • 如果内存有限,选择冒泡排序或插入排序(小数据量)
  • 一般场景选择快速排序(性能最优)

二、查找算法

1. 查找算法概述

定义: 查找算法是在数据集合中寻找满足特定条件的目标元素的过程。

分类:

  • 静态查找:数据集合不变(顺序查找、二分查找)
  • 动态查找:数据集合可能变化(二叉搜索树、哈希表)

2. 二分查找(Binary Search)

定义: 二分查找是一种在有序数组中查找特定元素的高效算法,通过不断将查找范围缩小一半来实现。

原理:

  1. 设定左右指针指向数组两端
  2. 计算中间位置,比较中间元素与目标值
  3. 如果中间元素等于目标值,返回索引
  4. 如果中间元素小于目标值,在右半部分继续查找
  5. 如果中间元素大于目标值,在左半部分继续查找
  6. 重复直到找到或查找范围为空

代码实现:

// 基础版本
function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    
    if (arr[mid] === target) {
      return mid;
    } else if (arr[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
  
  return -1;
}

// 递归版本
function binarySearchRecursive(arr, target, left = 0, right = arr.length - 1) {
  if (left > right) return -1;
  
  const mid = Math.floor((left + right) / 2);
  
  if (arr[mid] === target) return mid;
  if (arr[mid] < target) {
    return binarySearchRecursive(arr, target, mid + 1, right);
  }
  return binarySearchRecursive(arr, target, left, mid - 1);
}

// 示例
console.log(binarySearch([1, 3, 5, 7, 9, 11], 7));
// 输出: 3

复杂度分析:

  • 时间复杂度:O(log n)
  • 空间复杂度:O(1)(迭代),O(log n)(递归)
  • 前提条件:数组必须有序

常见误区:

  • 忘记处理边界条件(left <= right 而非 left < right)
  • 中间位置计算可能溢出(在大数据量时应使用 left + (right - left) / 2)
  • 不适用于链表结构

3. 顺序查找(Sequential Search)

定义: 顺序查找是最简单的查找算法,从数据结构的第一个元素开始,逐个比较直到找到目标或遍历完所有元素。

代码实现:

function sequentialSearch(arr, target) {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === target) {
      return i;
    }
  }
  return -1;
}

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 适用场景:无序数组、小规模数据

4. 插值查找(Interpolation Search)

定义: 插值查找是二分查找的改进版本,根据目标值与数组端点值的比例来估算目标位置。

代码实现:

function interpolationSearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  
  while (left <= right && target >= arr[left] && target <= arr[right]) {
    if (left === right) {
      return arr[left] === target ? left : -1;
    }
    
    const pos = left + Math.floor(
      ((target - arr[left]) * (right - left)) / (arr[right] - arr[left])
    );
    
    if (arr[pos] === target) return pos;
    if (arr[pos] < target) left = pos + 1;
    else right = pos - 1;
  }
  
  return -1;
}

复杂度分析:

  • 时间复杂度:O(log log n)(平均),O(n)(最坏)
  • 适用场景:均匀分布的有序数组

5. 哈希查找(Hash Search)

定义: 哈希查找通过哈希函数将键映射到数组索引,实现 O(1) 时间复杂度的查找。

原理:

  1. 使用哈希函数计算键的哈希值
  2. 将哈希值映射到数组索引
  3. 直接访问该索引获取数据
  4. 处理哈希冲突(链地址法、开放地址法)

代码实现:

class HashSearch {
  constructor() {
    this.table = {};
  }
  
  insert(key, value) {
    this.table[key] = value;
  }
  
  search(key) {
    return this.table[key] !== undefined ? this.table[key] : -1;
  }
}

// 示例
const hash = new HashSearch();
hash.insert('name', 'Alice');
hash.insert('age', 25);
console.log(hash.search('name')); // 输出: Alice

6. 二叉搜索树查找

定义: 在二叉搜索树中,左子树所有节点值小于根节点,右子树所有节点值大于根节点。

代码实现:

class TreeNode {
  constructor(val) {
    this.val = val;
    this.left = null;
    this.right = null;
  }
}

function bstSearch(root, target) {
  if (!root) return null;
  
  if (root.val === target) return root;
  if (target < root.val) {
    return bstSearch(root.left, target);
  }
  return bstSearch(root.right, target);
}

// 迭代版本
function bstSearchIterative(root, target) {
  let current = root;
  while (current) {
    if (current.val === target) return current;
    current = target < current.val ? current.left : current.right;
  }
  return null;
}

三、数据结构

1. 数组(Array)

定义: 数组是一种线性数据结构,元素在内存中连续存储,通过索引随机访问。

特点:

  • 随机访问:O(1) 时间复杂度
  • 插入/删除:O(n) 时间复杂度(需要移动元素)
  • 内存连续:缓存友好

代码实现:

// JavaScript 数组基本操作
const arr = [1, 2, 3, 4, 5];

// 访问
console.log(arr[0]); // O(1)

// 插入(末尾)
arr.push(6); // O(1) 均摊

// 插入(中间)
arr.splice(2, 0, 99); // O(n)

// 删除
arr.pop(); // O(1)
arr.shift(); // O(n)

常见误区:

  • 数组的 push 操作均摊时间复杂度为 O(1),但某些情况需要扩容
  • JavaScript 数组实际上是哈希表实现,不是真正的连续内存数组

2. 链表(Linked List)

定义: 链表是一种线性数据结构,元素通过指针链接,不要求内存连续。

2.1 单向链表

代码实现:

class ListNode {
  constructor(val) {
    this.val = val;
    this.next = null;
  }
}

class SinglyLinkedList {
  constructor() {
    this.head = null;
    this.size = 0;
  }
  
  // 头部插入
  addAtHead(val) {
    const node = new ListNode(val);
    node.next = this.head;
    this.head = node;
    this.size++;
  }
  
  // 尾部插入
  addAtTail(val) {
    const node = new ListNode(val);
    if (!this.head) {
      this.head = node;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      current.next = node;
    }
    this.size++;
  }
  
  // 删除节点
  deleteNode(val) {
    if (!this.head) return;
    if (this.head.val === val) {
      this.head = this.head.next;
      this.size--;
      return;
    }
    
    let current = this.head;
    while (current.next && current.next.val !== val) {
      current = current.next;
    }
    if (current.next) {
      current.next = current.next.next;
      this.size--;
    }
  }
  
  // 查找
  find(val) {
    let current = this.head;
    while (current) {
      if (current.val === val) return current;
      current = current.next;
    }
    return null;
  }
}
2.2 双向链表

代码实现:

class DoublyListNode {
  constructor(val) {
    this.val = val;
    this.prev = null;
    this.next = null;
  }
}

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }
  
  addAtTail(val) {
    const node = new DoublyListNode(val);
    if (!this.tail) {
      this.head = this.tail = node;
    } else {
      node.prev = this.tail;
      this.tail.next = node;
      this.tail = node;
    }
    this.size++;
  }
  
  deleteNode(val) {
    let current = this.head;
    while (current) {
      if (current.val === val) {
        if (current.prev) current.prev.next = current.next;
        else this.head = current.next;
        
        if (current.next) current.next.prev = current.prev;
        else this.tail = current.prev;
        
        this.size--;
        return true;
      }
      current = current.next;
    }
    return false;
  }
}

链表操作对比:

操作单向链表双向链表
头部插入O(1)O(1)
尾部插入O(n)O(1)
删除节点O(n)O(n)
反向遍历不支持支持

3. 栈(Stack)

定义: 栈是后进先出(LIFO)的线性数据结构,只允许在一端(栈顶)进行插入和删除操作。

代码实现:

class Stack {
  constructor() {
    this.items = [];
  }
  
  push(element) {
    this.items.push(element);
  }
  
  pop() {
    if (this.isEmpty()) return null;
    return this.items.pop();
  }
  
  peek() {
    if (this.isEmpty()) return null;
    return this.items[this.items.length - 1];
  }
  
  isEmpty() {
    return this.items.length === 0;
  }
  
  size() {
    return this.items.length;
  }
}

// 示例:有效的括号
function isValidParentheses(s) {
  const stack = new Stack();
  const map = { ')': '(', '}': '{', ']': '[' };
  
  for (const char of s) {
    if (map[char]) {
      if (stack.pop() !== map[char]) return false;
    } else {
      stack.push(char);
    }
  }
  
  return stack.isEmpty();
}

应用场景:

  • 函数调用栈
  • 括号匹配
  • 浏览器前进/后退
  • 表达式求值

4. 队列(Queue)

定义: 队列是先进先出(FIFO)的线性数据结构,在一端(队尾)插入,在另一端(队头)删除。

代码实现:

class Queue {
  constructor() {
    this.items = [];
    this.front = 0;
  }
  
  enqueue(element) {
    this.items.push(element);
  }
  
  dequeue() {
    if (this.isEmpty()) return null;
    const item = this.items[this.front];
    delete this.items[this.front];
    this.front++;
    return item;
  }
  
  peek() {
    if (this.isEmpty()) return null;
    return this.items[this.front];
  }
  
  isEmpty() {
    return this.front === this.items.length;
  }
  
  size() {
    return this.items.length - this.front;
  }
}
4.1 循环队列
class CircularQueue {
  constructor(capacity) {
    this.items = new Array(capacity);
    this.capacity = capacity;
    this.front = 0;
    this.rear = 0;
    this.count = 0;
  }
  
  enqueue(element) {
    if (this.count === this.capacity) return false;
    this.items[this.rear] = element;
    this.rear = (this.rear + 1) % this.capacity;
    this.count++;
    return true;
  }
  
  dequeue() {
    if (this.count === 0) return null;
    const item = this.items[this.front];
    this.front = (this.front + 1) % this.capacity;
    this.count--;
    return item;
  }
}
4.2 双端队列(Deque)
class Deque {
  constructor() {
    this.items = {};
    this.front = 0;
    this.rear = 0;
  }
  
  addFront(element) {
    this.front--;
    this.items[this.front] = element;
  }
  
  addRear(element) {
    this.items[this.rear] = element;
    this.rear++;
  }
  
  removeFront() {
    if (this.isEmpty()) return null;
    const item = this.items[this.front];
    delete this.items[this.front];
    this.front++;
    return item;
  }
  
  removeRear() {
    if (this.isEmpty()) return null;
    this.rear--;
    const item = this.items[this.rear];
    delete this.items[this.rear];
    return item;
  }
  
  isEmpty() {
    return this.rear - this.front === 0;
  }
}

5. 搜索框历史记录存储实战

问题: 如何利用合适的数据结构实现搜索框历史记录存储?

解决方案:使用队列 + 哈希表

class SearchHistory {
  constructor(maxSize = 10) {
    this.queue = [];
    this.set = new Set();
    this.maxSize = maxSize;
  }
  
  add(query) {
    // 如果已存在,先删除
    if (this.set.has(query)) {
      this.queue = this.queue.filter(item => item !== query);
    }
    
    // 如果超出容量,删除最旧的
    if (this.queue.length >= this.maxSize) {
      const removed = this.queue.shift();
      this.set.delete(removed);
    }
    
    // 添加新记录
    this.queue.push(query);
    this.set.add(query);
  }
  
  getHistory() {
    return [...this.queue];
  }
  
  clear() {
    this.queue = [];
    this.set.clear();
  }
}

// 示例
const history = new SearchHistory(5);
history.add('JavaScript');
history.add('算法');
history.add('JavaScript'); // 重复,移到最新
console.log(history.getHistory()); // ['算法', 'JavaScript']

选择策略:

  • 数组/队列:适合顺序存储
  • Set:快速去重和查找
  • localStorage:持久化存储

6. 树(Tree)

6.1 二叉树

定义: 二叉树是每个节点最多有两个子节点的树结构,分别称为左子节点和右子节点。

遍历方式:

class TreeNode {
  constructor(val) {
    this.val = val;
    this.left = null;
    this.right = null;
  }
}

// 前序遍历(根-左-右)
function preorderTraversal(root) {
  const result = [];
  function traverse(node) {
    if (!node) return;
    result.push(node.val);
    traverse(node.left);
    traverse(node.right);
  }
  traverse(root);
  return result;
}

// 中序遍历(左-根-右)
function inorderTraversal(root) {
  const result = [];
  function traverse(node) {
    if (!node) return;
    traverse(node.left);
    result.push(node.val);
    traverse(node.right);
  }
  traverse(root);
  return result;
}

// 后序遍历(左-右-根)
function postorderTraversal(root) {
  const result = [];
  function traverse(node) {
    if (!node) return;
    traverse(node.left);
    traverse(node.right);
    result.push(node.val);
  }
  traverse(root);
  return result;
}

// 层序遍历(BFS)
function levelOrderTraversal(root) {
  if (!root) return [];
  
  const result = [];
  const queue = [root];
  
  while (queue.length > 0) {
    const levelSize = queue.length;
    const currentLevel = [];
    
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift();
      currentLevel.push(node.val);
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    
    result.push(currentLevel);
  }
  
  return result;
}
6.2 二叉搜索树(BST)

定义: 二叉搜索树满足:左子树所有节点值 < 根节点值 < 右子树所有节点值。

class BinarySearchTree {
  constructor() {
    this.root = null;
  }
  
  insert(val) {
    const newNode = new TreeNode(val);
    if (!this.root) {
      this.root = newNode;
      return;
    }
    
    let current = this.root;
    while (true) {
      if (val < current.val) {
        if (!current.left) {
          current.left = newNode;
          break;
        }
        current = current.left;
      } else {
        if (!current.right) {
          current.right = newNode;
          break;
        }
        current = current.right;
      }
    }
  }
  
  search(val) {
    let current = this.root;
    while (current) {
      if (val === current.val) return true;
      current = val < current.val ? current.left : current.right;
    }
    return false;
  }
}
6.3 平衡二叉树(AVL 树)

定义: AVL 树是自平衡二叉搜索树,任意节点的左右子树高度差不超过1。

旋转操作:

class AVLNode extends TreeNode {
  constructor(val) {
    super(val);
    this.height = 1;
  }
}

function getHeight(node) {
  return node ? node.height : 0;
}

function getBalance(node) {
  return node ? getHeight(node.left) - getHeight(node.right) : 0;
}

function rightRotate(y) {
  const x = y.left;
  const T2 = x.right;
  
  x.right = y;
  y.left = T2;
  
  y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
  x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
  
  return x;
}

function leftRotate(x) {
  const y = x.right;
  const T2 = y.left;
  
  y.left = x;
  x.right = T2;
  
  x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
  y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
  
  return y;
}
6.4 红黑树

特性:

  1. 每个节点是红色或黑色
  2. 根节点是黑色
  3. 每个叶节点(NIL)是黑色
  4. 红色节点的子节点必须是黑色
  5. 从任一节点到其叶节点的路径上,黑色节点数量相同

应用:

  • Java TreeMap/HashMap
  • C++ STL map/set
  • Linux 内核调度器

7. 堆(Heap)

定义: 堆是一种完全二叉树,满足堆属性:父节点的值总是大于(最大堆)或小于(最小堆)子节点。

7.1 最大堆
class MaxHeap {
  constructor() {
    this.heap = [];
  }
  
  push(val) {
    this.heap.push(val);
    this.bubbleUp(this.heap.length - 1);
  }
  
  pop() {
    if (this.heap.length === 0) return null;
    if (this.heap.length === 1) return this.heap.pop();
    
    const max = this.heap[0];
    this.heap[0] = this.heap.pop();
    this.bubbleDown(0);
    return max;
  }
  
  bubbleUp(index) {
    while (index > 0) {
      const parent = Math.floor((index - 1) / 2);
      if (this.heap[parent] >= this.heap[index]) break;
      [this.heap[parent], this.heap[index]] = [this.heap[index], this.heap[parent]];
      index = parent;
    }
  }
  
  bubbleDown(index) {
    const length = this.heap.length;
    while (true) {
      let largest = index;
      const left = 2 * index + 1;
      const right = 2 * index + 2;
      
      if (left < length && this.heap[left] > this.heap[largest]) {
        largest = left;
      }
      if (right < length && this.heap[right] > this.heap[largest]) {
        largest = right;
      }
      if (largest === index) break;
      
      [this.heap[index], this.heap[largest]] = [this.heap[largest], this.heap[index]];
      index = largest;
    }
  }
}

8. 哈希表(Hash Table)

定义: 哈希表通过哈希函数将键映射到数组索引,实现 O(1) 时间复杂度的插入、删除和查找。

代码实现:

class HashTable {
  constructor(size = 53) {
    this.keyMap = new Array(size);
  }
  
  _hash(key) {
    let total = 0;
    const WEIRD_PRIME = 31;
    for (let i = 0; i < Math.min(key.length, 100); i++) {
      const char = key[i];
      const value = char.charCodeAt(0) - 96;
      total = (total * WEIRD_PRIME + value) % this.keyMap.length;
    }
    return total;
  }
  
  set(key, value) {
    const index = this._hash(key);
    if (!this.keyMap[index]) {
      this.keyMap[index] = [];
    }
    this.keyMap[index].push([key, value]);
  }
  
  get(key) {
    const index = this._hash(key);
    if (this.keyMap[index]) {
      for (let i = 0; i < this.keyMap[index].length; i++) {
        if (this.keyMap[index][i][0] === key) {
          return this.keyMap[index][i][1];
        }
      }
    }
    return undefined;
  }
  
  delete(key) {
    const index = this._hash(key);
    if (this.keyMap[index]) {
      const idx = this.keyMap[index].findIndex(pair => pair[0] === key);
      if (idx !== -1) {
        this.keyMap[index].splice(idx, 1);
        return true;
      }
    }
    return false;
  }
}
8.1 哈希冲突解决方案

对比表:

方法原理优点缺点
链地址法每个桶存储链表简单,适合高频冲突需要额外空间
开放地址法寻找下一个空位无需额外空间聚集问题
双重哈希使用第二个哈希函数减少聚集计算复杂

9. 图(Graph)

定义: 图是由顶点集合和边集合组成的数据结构,分为有向图和无向图。

表示方法:

// 邻接矩阵
const adjacencyMatrix = [
  [0, 1, 0, 1],
  [1, 0, 1, 0],
  [0, 1, 0, 1],
  [1, 0, 1, 0]
];

// 邻接表
const adjacencyList = {
  'A': ['B', 'D'],
  'B': ['A', 'C'],
  'C': ['B', 'D'],
  'D': ['A', 'C']
};

// 图类实现
class Graph {
  constructor() {
    this.adjacencyList = {};
  }
  
  addVertex(vertex) {
    if (!this.adjacencyList[vertex]) {
      this.adjacencyList[vertex] = [];
    }
  }
  
  addEdge(v1, v2) {
    this.adjacencyList[v1].push(v2);
    this.adjacencyList[v2].push(v1);
  }
  
  removeEdge(v1, v2) {
    this.adjacencyList[v1] = this.adjacencyList[v1].filter(v => v !== v2);
    this.adjacencyList[v2] = this.adjacencyList[v2].filter(v => v !== v1);
  }
  
  removeVertex(vertex) {
    while (this.adjacencyList[vertex].length) {
      const adjacent = this.adjacencyList[vertex].pop();
      this.removeEdge(vertex, adjacent);
    }
    delete this.adjacencyList[vertex];
  }
}

10. 哈希表与二叉搜索树对比

特性哈希表二叉搜索树
查找时间复杂度O(1) 平均O(log n) 平均
最坏情况O(n)O(n)
有序性无序有序
空间复杂度O(n)O(n)
范围查询不支持支持
实现复杂度简单复杂

选择策略:

  • 需要快速查找且不需要有序:选择哈希表
  • 需要有序数据或范围查询:选择二叉搜索树
  • 需要稳定的 O(log n) 性能:选择平衡二叉搜索树

四、递归与分治

1. 递归

定义: 递归是函数调用自身来解决问题的方法,包含基准情形和递归情形。

原理:

  1. 基准情形:最简单的情况,直接返回结果
  2. 递归情形:将问题分解为更小的子问题

代码实现:

// 斐波那契数列(基础递归)
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// 优化版本(记忆化)
function fibonacciMemo(n, memo = {}) {
  if (n in memo) return memo[n];
  if (n <= 1) return n;
  memo[n] = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo);
  return memo[n];
}

常见误区:

  • 忘记设置基准情形导致无限递归
  • 重复计算子问题(使用记忆化优化)
  • 递归深度过大导致栈溢出

2. 递归与迭代对比

特性递归迭代
实现方式函数自调用循环
可读性更简洁较冗长
性能函数调用开销更高效
内存栈空间消耗通常更优
适用场景树形结构、分治线性结构

3. 尾递归优化

定义: 尾递归是递归调用在函数最后一步的特殊形式,可被编译器优化为循环。

// 普通递归
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

// 尾递归
function factorialTail(n, acc = 1) {
  if (n <= 1) return acc;
  return factorialTail(n - 1, n * acc);
}

4. 分治算法

定义: 分治算法将大问题分解为相似的子问题,递归求解后合并结果。

步骤:

  1. 分解:将问题分成若干子问题
  2. 解决:递归求解子问题
  3. 合并:合并子问题的解

经典案例:归并排序

function mergeSort(arr) {
  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, right) {
  const result = [];
  while (left.length && right.length) {
    result.push(left[0] < right[0] ? left.shift() : right.shift());
  }
  return [...result, ...left, ...right];
}

分治与递归的关系:

  • 分治必然使用递归,但递归不一定是分治
  • 分治强调分解和合并,递归更通用

五、动态规划

1. 动态规划概述

定义: 动态规划是一种通过将复杂问题分解为更简单的子问题来求解的方法,适用于具有重叠子问题和最优子结构性质的问题。

核心概念:

  • 状态转移方程:描述子问题之间关系的公式
  • 最优子结构:问题的最优解包含子问题的最优解
  • 重叠子问题:在求解过程中,相同的子问题被多次计算

解题步骤:

  1. 定义状态
  2. 找出状态转移方程
  3. 确定边界条件
  4. 选择计算顺序(自顶向下或自底向上)

2. 动态规划与递归的区别

特性动态规划普通递归
子问题处理存储并复用重复计算
性能高效可能指数级
实现方式自底向上为主自顶向下
空间复杂度通常较高较低
适用问题重叠子问题无重叠子问题

3. 动态规划与贪心的区别

特性动态规划贪心算法
决策方式考虑所有可能局部最优
正确性保证全局最优不一定全局最优
复杂度较高较低
适用场景一般优化问题特殊结构问题

4. 斐波那契数列

问题: 求斐波那契数列的第 n 项。

代码实现:

// 基础递归 - O(2^n)
function fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}

// 记忆化递归 - O(n)
function fibMemo(n, memo = {}) {
  if (n in memo) return memo[n];
  if (n <= 1) return n;
  memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
  return memo[n];
}

// 动态规划 - O(n)
function fibDP(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];
}

// 空间优化 - O(n) 时间, O(1) 空间
function fibOptimized(n) {
  if (n <= 1) return n;
  let prev2 = 0, prev1 = 1;
  for (let i = 2; i <= n; i++) {
    const current = prev1 + prev2;
    prev2 = prev1;
    prev1 = current;
  }
  return prev1;
}

5. 爬楼梯问题

问题: 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。有多少种不同的方法可以爬到楼顶?

代码实现:

function climbStairs(n) {
  if (n <= 2) return n;
  
  let prev2 = 1, prev1 = 2;
  for (let i = 3; i <= n; i++) {
    const current = prev1 + prev2;
    prev2 = prev1;
    prev1 = current;
  }
  return prev1;
}

// 示例
console.log(climbStairs(5)); // 输出: 8

6. 背包问题(0-1 Knapsack)

问题: 给定 n 个物品,每个物品有重量和价值,在不超过背包容量的前提下,求最大价值。

代码实现:

function knapsack(weights, values, capacity) {
  const n = weights.length;
  const dp = Array(n + 1).fill(null).map(() => Array(capacity + 1).fill(0));
  
  for (let i = 1; i <= n; i++) {
    for (let w = 0; w <= capacity; w++) {
      if (weights[i - 1] <= w) {
        dp[i][w] = Math.max(
          dp[i - 1][w],
          dp[i - 1][w - weights[i - 1]] + values[i - 1]
        );
      } else {
        dp[i][w] = dp[i - 1][w];
      }
    }
  }
  
  return dp[n][capacity];
}

// 空间优化版本
function knapsackOptimized(weights, values, capacity) {
  const dp = Array(capacity + 1).fill(0);
  
  for (let i = 0; i < weights.length; i++) {
    for (let w = capacity; w >= weights[i]; w--) {
      dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
    }
  }
  
  return dp[capacity];
}

// 示例
console.log(knapsack([2, 3, 4, 5], [3, 4, 5, 6], 5)); // 输出: 7

7. 最长公共子序列(LCS)

问题: 给定两个字符串,求它们的最长公共子序列长度。

代码实现:

function longestCommonSubsequence(text1, text2) {
  const m = text1.length;
  const n = text2.length;
  const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
  
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (text1[i - 1] === text2[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];
}

// 示例
console.log(longestCommonSubsequence("abcde", "ace")); // 输出: 3

8. 最长递增子序列(LIS)

问题: 给定一个整数数组,求最长严格递增子序列的长度。

代码实现:

// 动态规划 - O(n^2)
function lengthOfLIS(nums) {
  if (nums.length === 0) return 0;
  
  const dp = Array(nums.length).fill(1);
  
  for (let i = 1; i < nums.length; i++) {
    for (let j = 0; j < i; j++) {
      if (nums[i] > nums[j]) {
        dp[i] = Math.max(dp[i], dp[j] + 1);
      }
    }
  }
  
  return Math.max(...dp);
}

// 二分查找优化 - O(n log n)
function lengthOfLISOptimized(nums) {
  const tails = [];
  
  for (const num of nums) {
    let left = 0, right = tails.length;
    
    while (left < right) {
      const mid = Math.floor((left + right) / 2);
      if (tails[mid] < num) {
        left = mid + 1;
      } else {
        right = mid;
      }
    }
    
    tails[left] = num;
  }
  
  return tails.length;
}

// 示例
console.log(lengthOfLISOptimized([10, 9, 2, 5, 3, 7, 101, 18])); // 输出: 4

9. 编辑距离(Levenshtein Distance)

问题: 给定两个单词,求将一个转换成另一个所需的最少操作数(插入、删除、替换)。

代码实现:

function minDistance(word1, word2) {
  const m = word1.length;
  const n = word2.length;
  const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
  
  // 初始化边界
  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 (word1[i - 1] === word2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1];
      } else {
        dp[i][j] = Math.min(
          dp[i - 1][j] + 1,     // 删除
          dp[i][j - 1] + 1,     // 插入
          dp[i - 1][j - 1] + 1  // 替换
        );
      }
    }
  }
  
  return dp[m][n];
}

// 示例
console.log(minDistance("horse", "ros")); // 输出: 3

10. 最小路径和

问题: 给定一个包含非负整数的 m x n 网格,求从左上角到右下角的最小路径和。

代码实现:

function minPathSum(grid) {
  const m = grid.length;
  const n = grid[0].length;
  const dp = Array(m).fill(null).map(() => Array(n));
  
  dp[0][0] = grid[0][0];
  
  // 初始化第一行
  for (let j = 1; j < n; j++) {
    dp[0][j] = dp[0][j - 1] + grid[0][j];
  }
  
  // 初始化第一列
  for (let i = 1; i < m; i++) {
    dp[i][0] = dp[i - 1][0] + grid[i][0];
  }
  
  // 填充其余
  for (let i = 1; i < m; i++) {
    for (let j = 1; j < n; j++) {
      dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
    }
  }
  
  return dp[m - 1][n - 1];
}

11. 零钱兑换

问题: 给定不同面额的硬币和总金额,求凑成总金额所需的最少硬币数。

代码实现:

function 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 (i >= coin) {
        dp[i] = Math.min(dp[i], dp[i - coin] + 1);
      }
    }
  }
  
  return dp[amount] === Infinity ? -1 : dp[amount];
}

// 示例
console.log(coinChange([1, 2, 5], 11)); // 输出: 3

12. 打家劫舍

问题: 不能抢劫相邻房屋,求最大收益。

代码实现:

function rob(nums) {
  if (nums.length === 0) return 0;
  if (nums.length === 1) return nums[0];
  
  let prev2 = 0, prev1 = 0;
  
  for (const num of nums) {
    const current = Math.max(prev1, prev2 + num);
    prev2 = prev1;
    prev1 = current;
  }
  
  return prev1;
}

// 示例
console.log(rob([2, 7, 9, 3, 1])); // 输出: 12

六、贪心算法

1. 贪心算法概述

定义: 贪心算法在每一步选择中都采取当前状态下最优的选择,希望通过局部最优达到全局最优。

原理:

  • 每步选择当前最优解
  • 不回溯、不撤销
  • 需要证明贪心选择的正确性

适用条件:

  • 贪心选择性质:局部最优解能导致全局最优解
  • 最优子结构:问题的最优解包含子问题的最优解

2. 活动选择问题

问题: 给定一组活动,每个活动有开始和结束时间,求最多能安排多少个互不冲突的活动。

代码实现:

function activitySelection(activities) {
  // 按结束时间排序
  activities.sort((a, b) => a.end - b.end);
  
  let count = 1;
  let lastEnd = activities[0].end;
  
  for (let i = 1; i < activities.length; i++) {
    if (activities[i].start >= lastEnd) {
      count++;
      lastEnd = activities[i].end;
    }
  }
  
  return count;
}

// 示例
const activities = [
  { start: 1, end: 3 },
  { start: 2, end: 5 },
  { start: 4, end: 6 },
  { start: 6, end: 8 },
  { start: 5, end: 7 }
];
console.log(activitySelection(activities)); // 输出: 3

3. 霍夫曼编码

原理: 通过构建霍夫曼树来实现数据压缩,出现频率高的字符使用较短编码。

class HuffmanNode {
  constructor(char, freq) {
    this.char = char;
    this.freq = freq;
    this.left = null;
    this.right = null;
  }
}

function buildHuffmanTree(chars, freqs) {
  const nodes = chars.map((char, i) => new HuffmanNode(char, freqs[i]));
  
  while (nodes.length > 1) {
    nodes.sort((a, b) => a.freq - b.freq);
    const left = nodes.shift();
    const right = nodes.shift();
    const parent = new HuffmanNode(null, left.freq + right.freq);
    parent.left = left;
    parent.right = right;
    nodes.push(parent);
  }
  
  return nodes[0];
}

4. 最小生成树 - Prim 算法

function prim(graph, start) {
  const n = graph.length;
  const visited = Array(n).fill(false);
  const minCost = Array(n).fill(Infinity);
  minCost[start] = 0;
  
  let totalCost = 0;
  
  for (let i = 0; i < n; i++) {
    let u = -1;
    for (let v = 0; v < n; v++) {
      if (!visited[v] && (u === -1 || minCost[v] < minCost[u])) {
        u = v;
      }
    }
    
    visited[u] = true;
    totalCost += minCost[u];
    
    for (let v = 0; v < n; v++) {
      if (graph[u][v] < minCost[v]) {
        minCost[v] = graph[u][v];
      }
    }
  }
  
  return totalCost;
}

5. Dijkstra 最短路径算法

function dijkstra(graph, start) {
  const n = graph.length;
  const dist = Array(n).fill(Infinity);
  const visited = Array(n).fill(false);
  dist[start] = 0;
  
  for (let i = 0; i < n - 1; i++) {
    let u = -1;
    for (let v = 0; v < n; v++) {
      if (!visited[v] && (u === -1 || dist[v] < dist[u])) {
        u = v;
      }
    }
    
    if (dist[u] === Infinity) break;
    visited[u] = true;
    
    for (let v = 0; v < n; v++) {
      if (graph[u][v] > 0 && !visited[v]) {
        dist[v] = Math.min(dist[v], dist[u] + graph[u][v]);
      }
    }
  }
  
  return dist;
}

七、回溯算法

1. 回溯算法概述

定义: 回溯算法通过探索所有可能的候选解来找出所有解,如果候选解不满足条件则回溯。

原理:

  1. 选择:做出当前选择
  2. 探索:递归探索后续选择
  3. 撤销:撤销当前选择,尝试其他可能

2. 回溯与递归的关系

特性回溯递归
目标找所有解解决问题
撤销需要撤销不需要
搜索空间树形/图线性/树形
典型问题组合、排列分治、DP

3. N 皇后问题

问题: 在 n×n 棋盘上放置 n 个皇后,使它们互不攻击。

代码实现:

function solveNQueens(n) {
  const result = [];
  const board = Array(n).fill().map(() => Array(n).fill('.'));
  
  function isValid(row, col) {
    for (let i = 0; i < row; i++) {
      if (board[i][col] === 'Q') return false;
      if (col - (row - i) >= 0 && board[i][col - (row - i)] === 'Q') return false;
      if (col + (row - i) < n && board[i][col + (row - i)] === 'Q') return false;
    }
    return true;
  }
  
  function backtrack(row) {
    if (row === n) {
      result.push(board.map(r => r.join('')));
      return;
    }
    
    for (let col = 0; col < n; col++) {
      if (isValid(row, col)) {
        board[row][col] = 'Q';
        backtrack(row + 1);
        board[row][col] = '.';
      }
    }
  }
  
  backtrack(0);
  return result;
}

4. 全排列

function permute(nums) {
  const result = [];
  
  function backtrack(path, used) {
    if (path.length === nums.length) {
      result.push([...path]);
      return;
    }
    
    for (let i = 0; i < nums.length; i++) {
      if (used[i]) continue;
      
      path.push(nums[i]);
      used[i] = true;
      backtrack(path, used);
      path.pop();
      used[i] = false;
    }
  }
  
  backtrack([], Array(nums.length).fill(false));
  return result;
}

// 示例
console.log(permute([1, 2, 3]));
// 输出: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

5. 组合问题

function combine(n, k) {
  const result = [];
  
  function backtrack(start, path) {
    if (path.length === k) {
      result.push([...path]);
      return;
    }
    
    for (let i = start; i <= n; i++) {
      path.push(i);
      backtrack(i + 1, path);
      path.pop();
    }
  }
  
  backtrack(1, []);
  return result;
}

6. 子集问题

function subsets(nums) {
  const result = [];
  
  function backtrack(start, path) {
    result.push([...path]);
    
    for (let i = start; i < nums.length; i++) {
      path.push(nums[i]);
      backtrack(i + 1, path);
      path.pop();
    }
  }
  
  backtrack(0, []);
  return result;
}

八、时间复杂度与空间复杂度

1. 时间复杂度

定义: 时间复杂度描述算法运行时间随输入规模增长的变化趋势。

大 O 表示法: 忽略常数因子和低阶项,只保留最高阶项。

常见时间复杂度(从小到大):

复杂度名称示例
O(1)常数数组访问
O(log n)对数二分查找
O(n)线性顺序查找
O(n log n)线性对数快速排序
O(n^2)平方冒泡排序
O(n^3)立方矩阵乘法
O(2^n)指数递归斐波那契
O(n!)阶乘全排列

2. 空间复杂度

定义: 空间复杂度描述算法所需额外存储空间随输入规模增长的变化趋势。

计算规则:

  • 忽略输入数据本身占用的空间
  • 只计算额外申请的辅助空间
  • 递归调用栈空间也算

3. 最好、最坏、平均时间复杂度

类型定义示例(快速排序)
最好最理想情况O(n log n)
最坏最糟糕情况O(n^2)
平均期望情况O(n log n)

4. 均摊时间复杂度

定义: 将多次操作的总时间均摊到每次操作上的时间复杂度。

示例: 数组 push 操作,均摊时间复杂度为 O(1)。


5. 空间换时间与时间换空间

空间换时间:

  • 使用哈希表加速查找
  • 使用缓存避免重复计算
  • 使用额外数组简化操作

时间换空间:

  • 使用原地排序算法
  • 使用迭代代替递归
  • 使用位运算减少空间

九、常见算法题解

1. 两数之和

问题: 在数组中找到两个数,使它们的和等于目标值。

function twoSum(nums, target) {
  const map = {};
  
  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];
    if (complement in map) {
      return [map[complement], i];
    }
    map[nums[i]] = i;
  }
  
  return [];
}

2. 三数之和

function threeSum(nums) {
  nums.sort((a, b) => a - b);
  const result = [];
  
  for (let i = 0; i < nums.length - 2; i++) {
    if (i > 0 && nums[i] === nums[i - 1]) continue;
    
    let left = i + 1, right = nums.length - 1;
    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right];
      if (sum === 0) {
        result.push([nums[i], nums[left], nums[right]]);
        while (left < right && nums[left] === nums[left + 1]) left++;
        while (left < right && nums[right] === nums[right - 1]) right--;
        left++;
        right--;
      } else if (sum < 0) {
        left++;
      } else {
        right--;
      }
    }
  }
  
  return result;
}

3. 反转链表

function reverseList(head) {
  let prev = null;
  let current = head;
  
  while (current) {
    const next = current.next;
    current.next = prev;
    prev = current;
    current = next;
  }
  
  return prev;
}

4. 合并两个有序链表

function mergeTwoLists(l1, l2) {
  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;
}

5. 链表是否有环

function hasCycle(head) {
  let slow = head;
  let fast = head;
  
  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next;
    if (slow === fast) return true;
  }
  
  return false;
}

6. 有效的括号

function isValid(s) {
  const stack = [];
  const map = { ')': '(', '}': '{', ']': '[' };
  
  for (const char of s) {
    if (map[char]) {
      if (stack.pop() !== map[char]) return false;
    } else {
      stack.push(char);
    }
  }
  
  return stack.length === 0;
}

7. 最小栈

class MinStack {
  constructor() {
    this.stack = [];
    this.minStack = [];
  }
  
  push(val) {
    this.stack.push(val);
    if (this.minStack.length === 0 || val <= this.getMin()) {
      this.minStack.push(val);
    }
  }
  
  pop() {
    if (this.pop() === this.getMin()) {
      this.minStack.pop();
    }
  }
  
  top() {
    return this.stack[this.stack.length - 1];
  }
  
  getMin() {
    return this.minStack[this.minStack.length - 1];
  }
}

8. 最大子数组和

function maxSubArray(nums) {
  let maxSum = nums[0];
  let currentSum = nums[0];
  
  for (let i = 1; i < nums.length; i++) {
    currentSum = Math.max(nums[i], currentSum + nums[i]);
    maxSum = Math.max(maxSum, currentSum);
  }
  
  return maxSum;
}

9. 滑动窗口最大值

function maxSlidingWindow(nums, k) {
  const result = [];
  const deque = [];
  
  for (let i = 0; i < nums.length; i++) {
    while (deque.length > 0 && deque[0] < i - k + 1) {
      deque.shift();
    }
    
    while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) {
      deque.pop();
    }
    
    deque.push(i);
    
    if (i >= k - 1) {
      result.push(nums[deque[0]]);
    }
  }
  
  return result;
}

10. LRU 缓存

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.map = new Map();
  }
  
  get(key) {
    if (!this.map.has(key)) return -1;
    const value = this.map.get(key);
    this.map.delete(key);
    this.map.set(key, value);
    return value;
  }
  
  put(key, value) {
    if (this.map.has(key)) {
      this.map.delete(key);
    }
    this.map.set(key, value);
    if (this.map.size > this.capacity) {
      const firstKey = this.map.keys().next().value;
      this.map.delete(firstKey);
    }
  }
}

11. 数组去重

方法一:Set

function uniqueWithSet(arr) {
  return [...new Set(arr)];
}

方法二:Filter + indexOf

function uniqueWithFilter(arr) {
  return arr.filter((item, index) => arr.indexOf(item) === index);
}

方法三:Reduce

function uniqueWithReduce(arr) {
  return arr.reduce((acc, current) => {
    if (!acc.includes(current)) {
      acc.push(current);
    }
    return acc;
  }, []);
}

12. 数组扁平化

// 基础版本
function flatten(arr) {
  return arr.reduce((acc, val) => {
    return acc.concat(Array.isArray(val) ? flatten(val) : val);
  }, []);
}

// 指定深度
function flattenDepth(arr, depth = 1) {
  if (depth <= 0) return arr;
  return arr.reduce((acc, val) => {
    return acc.concat(Array.isArray(val) ? flattenDepth(val, depth - 1) : val);
  }, []);
}

// 示例
console.log(flatten([1, [2, [3, [4]]]])); // [1, 2, 3, 4]

13. 深拷贝实现

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  
  if (hash.has(obj)) return hash.get(obj);
  
  const clone = Array.isArray(obj) ? [] : {};
  hash.set(obj, clone);
  
  for (const key of Object.keys(obj)) {
    clone[key] = deepClone(obj[key], hash);
  }
  
  return clone;
}

14. 防抖(Debounce)

function debounce(func, delay) {
  let timer = null;
  
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

15. 节流(Throttle)

function throttle(func, delay) {
  let lastTime = 0;
  
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= delay) {
      func.apply(this, args);
      lastTime = now;
    }
  };
}

16. DFS vs BFS 对比

特性DFSBFS
数据结构栈/递归队列
探索顺序深度优先广度优先
空间复杂度O(h)O(w)
最短路径不保证保证
典型场景迷宫、回溯层次遍历、最短路径

DFS 实现:

function dfs(graph, start) {
  const visited = new Set();
  const result = [];
  
  function traverse(node) {
    if (!node || visited.has(node)) return;
    visited.add(node);
    result.push(node);
    
    for (const neighbor of graph[node]) {
      traverse(neighbor);
    }
  }
  
  traverse(start);
  return result;
}

BFS 实现:

function bfs(graph, start) {
  const visited = new Set();
  const queue = [start];
  const result = [];
  
  visited.add(start);
  
  while (queue.length > 0) {
    const node = queue.shift();
    result.push(node);
    
    for (const neighbor of graph[node]) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);
        queue.push(neighbor);
      }
    }
  }
  
  return result;
}

17. 大数相加

function addStrings(num1, num2) {
  let i = num1.length - 1;
  let j = num2.length - 1;
  let carry = 0;
  let result = '';
  
  while (i >= 0 || j >= 0 || carry > 0) {
    const digit1 = i >= 0 ? parseInt(num1[i]) : 0;
    const digit2 = j >= 0 ? parseInt(num2[j]) : 0;
    const sum = digit1 + digit2 + carry;
    
    result = (sum % 10) + result;
    carry = Math.floor(sum / 10);
    i--;
    j--;
  }
  
  return result;
}

18. 接雨水

function trap(height) {
  let left = 0, right = height.length - 1;
  let leftMax = 0, rightMax = 0;
  let water = 0;
  
  while (left < right) {
    if (height[left] < height[right]) {
      if (height[left] >= leftMax) {
        leftMax = height[left];
      } else {
        water += leftMax - height[left];
      }
      left++;
    } else {
      if (height[right] >= rightMax) {
        rightMax = height[right];
      } else {
        water += rightMax - height[right];
      }
      right--;
    }
  }
  
  return water;
}

19. 手写快速排序

function quickSort(arr) {
  if (arr.length <= 1) return arr;
  
  const pivot = arr[Math.floor(arr.length / 2)];
  const left = [];
  const right = [];
  const equal = [];
  
  for (const num of arr) {
    if (num < pivot) left.push(num);
    else if (num > pivot) right.push(num);
    else equal.push(num);
  }
  
  return [...quickSort(left), ...equal, ...quickSort(right)];
}

20. 买卖股票的最佳时机

function maxProfit(prices) {
  let minPrice = Infinity;
  let maxProfit = 0;
  
  for (const price of prices) {
    if (price < minPrice) {
      minPrice = price;
    } else {
      maxProfit = Math.max(maxProfit, price - minPrice);
    }
  }
  
  return maxProfit;
}