JavaScript常用的算法详解

1. 排序算法

1.1. 冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法,通过反复遍历数组,将相邻的两个元素进行比较并交换位置,从而将最大的元素逐步移到数组的末尾。

优点:

实现简单:算法逻辑简单,易于理解。

原地排序:不需要额外的内存空间,空间复杂度为 O(1)。

缺点:

效率低下:时间复杂度为 O(n²),对大规模数据排序效率很差。

适合小规模数据:对于小规模数据或几乎有序的数据有一定效果。

代码示例:



function bubbleSort(arr: number[]): number[] {
  const len = arr.length;
  for (let i = 0; i < len; i++) {
    for (let j = 0; j < len - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; // 交换位置
      }
    }
  }
  return arr;
}

使用场景:

小规模数据排序:当数据量很小时可以使用,尤其是数据几乎有序时。

1.2. 选择排序(Selection Sort)

选择排序每次从未排序的部分选择最小或最大的元素,放到已排序部分的末尾,逐步缩小未排序部分。

优点:

简单直观:逻辑简单,易于实现。

数据移动次数少:每次交换只涉及一次最小值移动。

缺点:

效率较低:时间复杂度为 O(n²),大规模数据表现不佳。

不稳定排序:相同元素的相对顺序可能会被改变。

代码示例:



function selectionSort(arr: number[]): number[] {
  const len = arr.length;
  for (let i = 0; i < len - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < len; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    if (minIndex !== i) {
      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]; // 交换最小值到已排序部分
    }
  }
  return arr;
}

使用场景:

小规模数据排序:适合数据量较少的情况。

对内存写操作敏感的场景:如硬盘排序或闪存存储。

1.3. 插入排序(Insertion Sort)

插入排序通过将每个元素插入到已排序部分的适当位置,逐步构建有序序列。

优点:

适合小规模数据:表现较好,尤其是数据接近有序时。

稳定排序:相同元素的相对顺序不会改变。

缺点:

效率较低:时间复杂度为 O(n²),无序数据时性能较差。

代码示例:



function insertionSort(arr: number[]): number[] {
  const len = arr.length;
  for (let i = 1; i < len; i++) {
    let key = arr[i];
    let j = i - 1;
    while (j >= 0 && arr[j] > key) {
      arr[j + 1] = arr[j]; // 将比 key 大的元素后移
      j--;
    }
    arr[j + 1] = key;
  }
  return arr;
}

使用场景:

小规模数据排序:适合数据量较少的情况。

几乎有序的数据:当数据接近有序时,插入排序的时间复杂度接近 O(n)。

1.4. 归并排序(Merge Sort)

归并排序是一种基于分治思想的排序算法,将数组分为两个子数组,递归排序后合并成一个有序数组。

优点:

时间复杂度稳定:O(n log n),适合大规模数据。

稳定排序:相同元素的相对顺序不变。

缺点:

需要额外空间:需要 O(n) 的额外空间存储合并结果。

代码示例:



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]);
      i++;
    } else {
      result.push(right[j]);
      j++;
    }
  }
 
  return result.concat(left.slice(i), right.slice(j));
}

使用场景:

大规模数据排序:归并排序适合处理大量数据。

链表排序:归并排序在链表上的效率高于其他排序算法。

1.5. 快速排序(Quick Sort)

快速排序通过选择一个基准元素,将数组分为小于和大于基准的两部分,递归排序。

优点:

平均性能优异:时间复杂度为 O(n log n),常数因子小,适合大规模数据。

原地排序:不需要额外的存储空间,空间复杂度为 O(log n))。

缺点:

最坏情况退化:在不好的情况下,时间复杂度会退化为 O(n²)。

不稳定排序:相同元素的相对顺序可能改变。

代码示例:



function quickSort(arr: number[]): number[] {
  if (arr.length <= 1) return arr;
  const pivot = arr[Math.floor(arr.length / 2)];
  const left = arr.filter(x => x < pivot);
  const right = arr.filter(x => x > pivot);
  const middle = arr.filter(x => x === pivot);
  return quickSort(left).concat(middle, quickSort(right));
}

使用场景:

大规模数据排序:快速排序是实际应用中常用的排序算法之一。

对空间要求敏感的场景:快速排序为原地排序。

1.6. 希尔排序(Shell Sort)

希尔排序是插入排序的改进版,通过将数组按照一定的间隔分为多个子序列,先对每个子序列进行插入排序,然后逐渐减小间隔直到最后对整个序列进行插入排序。

优点:

改进了插入排序:通过间隔分组,减少了大数据量的移动,提升了效率。

适合中等规模数据:在一些特定场景中,希尔排序比 O(n log n) 的算法更快。

缺点:

时间复杂度依赖于间隔序列的选择:通常的复杂度在 O(n log² n) 到 O(n³/2) 之间。

不稳定排序:相同元素的相对顺序可能会改变。

代码示例:



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

使用场景:

希尔排序适用于中等规模的数组或列表,尤其是在插入排序表现不佳时使用。

1.7. 堆排序(Heap Sort)

堆排序利用堆这种数据结构来排序,首先将数组构造成一个大顶堆或小顶堆,然后反复取出堆顶元素并将其放在已排好序的部分。

优点:

时间复杂度稳定:最坏、最好、平均情况下时间复杂度均为 O(n log n)。

原地排序:只需要少量的额外存储空间O(1)。

缺点:

不稳定排序:相同元素的相对顺序可能改变。

常数较大:相比于快速排序,堆排序的常数因子较大,速度稍慢。

代码示例:



function heapSort(arr: number[]): number[] {
  const heapify = (arr: number[], length: number, i: number) => {
    let largest = i;
    const left = 2 * i + 1;
    const right = 2 * i + 2;
 
    if (left < length && arr[left] > arr[largest]) {
      largest = left;
    }
 
    if (right < length && arr[right] > arr[largest]) {
      largest = right;
    }
 
    if (largest !== i) {
      [arr[i], arr[largest]] = [arr[largest], arr[i]];
      heapify(arr, length, largest);
    }
  };
 
  const length = arr.length;
  for (let i = Math.floor(length / 2) - 1; i >= 0; i--) {
    heapify(arr, length, i);
  }
 
  for (let i = length - 1; i > 0; i--) {
    [arr[0], arr[i]] = [arr[i], arr[0]];
    heapify(arr, i, 0);
  }
 
  return arr;
}

使用场景:

数据量大,且不要求稳定性的排序场景中使用。

1.8. 桶排序(Bucket Sort)

桶排序将数组划分为若干个桶,然后对每个桶内部进行排序,最后合并所有桶中的元素。

优点:

时间复杂度:在最佳情况下,时间复杂度可以达到 O(n + k),其中 k 是桶的数量。

适合数据分布均匀的场景:能够利用数据的特性提高排序效率。

缺点:

需要额外空间:由于需要存储桶,空间复杂度较高。

不适用于数据分布不均匀的场景:如果数据分布极端,可能退化为 O(n²)。

代码示例:



function bucketSort(arr: number[], bucketsize = 5): number[] {
  if (arr.length === 0) return arr;
  const min = Math.min(...arr);
  const max = Math.max(...arr);
  const buckets = Array.from({ length: Math.floor((max - min) / bucketsize) + 1 }, () => []);
 
  arr.forEach((num) => {
    const index = Math.floor((num - min) / bucketsize);
    buckets[index].push(num);
  });
 
  return buckets.reduce((acc, bucket) => acc.concat(bucket.sort((a, b) => a - b)), []);
}

使用场景:

适用于已知数据范围且分布较为均匀的场景,如成绩排序。

1.9. 计数排序(Counting Sort)

计数排序适用于整数排序,它通过统计数组中元素的出现次数来实现排序。

优点:

时间复杂度为 O(n + k):适用于小范围整数的排序。

稳定排序:相同元素的相对顺序保持不变。

缺点:

受数据范围限制:当数据范围很大时,空间复杂度较高。

只能用于整数排序:无法直接对浮点数等进行排序。

代码示例:



function countingSort(arr: number[]): number[] {
  const max = Math.max(...arr);
  const count = new Array(max + 1).fill(0);
 
  arr.forEach(num => count[num]++);
  const result: number[] = [];
 
  count.forEach((num, index) => {
    while (num > 0) {
      result.push(index);
      num--;
    }
  });
 
  return result;
}

使用场景:

适用于数据范围较小、重复数据较多的场景,如考试成绩、年龄等。

1.10. 基数排序(Radix Sort)

基数排序将数据按位(个位、十位等)进行多次排序,通常使用稳定的排序算法,如计数排序作为子过程。

优点:

时间复杂度为 O(d * (n + k)),d 是数字位数,k 是基数,适合大规模数字的排序。

稳定排序:每次子排序都使用稳定排序,保证最终结果的稳定性。

缺点:

受数据类型限制:通常只能用于整数和定长字符串的排序。

空间复杂度较高:需要额外空间存储中间结果。

代码示例:



function radixSort(arr: number[]): number[] {
  const getMax = (arr: number[]): number => Math.max(...arr);
  const getDigit = (num: number, place: number) => Math.floor(Math.abs(num) / Math.pow(10, place)) % 10;
 
  const countingSortByDigit = (arr: number[], place: number): number[] => {
    const count = Array(10).fill(0);
    const output = Array(arr.length);
 
    arr.forEach(num => {
      const digit = getDigit(num, place);
      count[digit]++;
    });
 
    for (let i = 1; i < 10; i++) {
      count[i] += count[i - 1];
    }
 
    for (let i = arr.length - 1; i >= 0; i--) {
      const digit = getDigit(arr[i], place);
      output[--count[digit]] = arr[i];
    }
 
    return output;
  };
 
  const max = getMax(arr);
  let place = 0;
  while (Math.floor(max / Math.pow(10, place)) > 0) {
    arr = countingSortByDigit(arr, place);
    place++;
  }
 
  return arr;
}

使用场景:

适用于大规模且定长的数据,如身份证号、电话号码等。

2. 查找算法

2.1. 二分查找(Binary Search)

二分查找是一种高效的查找算法,前提是数组必须有序。每次将查找范围减半,从而快速找到目标元素的位置。

优点:

时间复杂度低:O(log n),查找速度非常快。

适合大规模数据:对于大规模有序数据,查找效率高。

缺点:

仅适用于有序数据:数组必须是有序的,否则无法使用。

插入与删除复杂:插入或删除元素需要移动大量数据,时间复杂度为 O(n)。

代码示例:



function binarySearch(arr: number[], target: number): number {
  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; // 未找到
}

使用场景:

查找有序数据:适用于静态有序数据集的高效查找。

查找频繁的场景:如字典查找、数据库索引等。

2.2. 线性查找(Linear Search)

线性查找是一种最基本的查找算法,从数组的第一个元素开始,依次与目标值进行比较,直到找到目标元素或遍历完整个数组。

优点:

简单易实现:不需要数据有序,任何情况下都能使用。

适用于小规模数据:当数据量较小时,线性查找可以很容易实现。

缺点:

时间复杂度高:最坏情况的时间复杂度为 O(n)。

效率低:特别是对于大规模数据,查找效率较低。

代码示例:



function linearSearch(arr: number[], target: number): number {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === target) {
      return i;
    }
  }
  return -1; // 未找到
}

使用场景:

小规模数据查找:适用于数据量较小且无序的数据集。

数据不频繁查找:数据变化频繁的场景中可以直接使用。

2.3. 插值查找(Interpolation Search)

插值查找是二分查找的改进版,基于目标值在数组中的位置比例来计算查找点。适合用于元素值分布较为均匀的有序数组。

优点:

平均性能优于二分查找:当数据分布均匀时,时间复杂度为 O(log log n)。

高效查找:比二分查找更适合数据范围已知、分布均匀的场景。

缺点:

仅适用于有序且均匀分布的数据:数据分布不均时,最坏情况退化为 O(n)。

插入与删除复杂:和二分查找类似,插入或删除的代价较高。

代码示例:



function interpolationSearch(arr: number[], target: number): number {
  let low = 0;
  let high = arr.length - 1;
  while (low <= high && target >= arr[low] && target <= arr[high]) {
    if (low === high) {
      if (arr[low] === target) return low;
      return -1;
    }
    const pos = low + Math.floor(((target - arr[low]) * (high - low)) / (arr[high] - arr[low]));
    if (arr[pos] === target) return pos;
    if (arr[pos] < target) low = pos + 1;
    else high = pos - 1;
  }
  return -1; // 未找到
}

使用场景:

大规模且均匀分布的数据集:例如价格列表、自然数序列等。

2.4. 跳表查找(Skip List Search)

跳表是一种用于有序链表的查找算法,利用“多级链表”结构加快查找速度,每层链表存储比上一层链表更多的元素,以实现快速定位。

优点:

支持动态增删操作:相比二分查找,跳表可以在 O(log n) 时间内完成插入、删除和查找操作。

高效查找:在保持链表结构的前提下,可以进行较为快速的查找。

缺点:

空间复杂度较高:为了加速查找,需要额外存储多层链表,占用更多空间。

代码示例:



class SkipListNode {
  constructor(public value: number, public next: SkipListNode[] = []) {}
}
 
class SkipList {
  private maxLevel = 16; // 跳表最大层数
  private head = new SkipListNode(-1, Array(this.maxLevel).fill(null));
 
  search(target: number): SkipListNode | null {
    let current = this.head;
    for (let level = this.maxLevel - 1; level >= 0; level--) {
      while (current.next[level] && current.next[level].value < target) {
        current = current.next[level];
      }
    }
    current = current.next[0];
    return current && current.value === target ? current : null;
  }
}

使用场景:

动态数据集:如频繁插入、删除元素的有序链表。

适用于实现动态数据库索引或缓存系统。

2.5. 二叉查找树(Binary Search Tree, BST)

二叉查找树是一种每个节点都有两个子节点的数据结构,左子节点值小于根节点,右子节点值大于根节点。通过递归方式查找目标元素。

优点:

支持动态增删操作:查找、插入、删除操作的平均时间复杂度为 O(log n)。

适用于动态有序数据:可以高效地插入、删除数据,并保持数据有序。

缺点:

最坏情况退化为链表:当树的结构不平衡时,最坏情况时间复杂度为 O(n)。

需要平衡操作:需要结合平衡机制(如 AVL 树或红黑树)以保持性能稳定。

代码示例:



class TreeNode {
  constructor(public value: number, public left: TreeNode | null = null, public right: TreeNode | null = null) {}
}
 
function searchBST(root: TreeNode | null, target: number): TreeNode | null {
  if (!root || root.value === target) return root;
  if (target < root.value) return searchBST(root.left, target);
  return searchBST(root.right, target);
}

使用场景:

动态数据集:适合频繁插入、删除的有序数据集,如自平衡二叉树可用于数据库或文件系统索引。

2.6. 哈希查找(Hash Search)

哈希查找通过哈希函数将数据映射到表中的一个位置,以常数时间 O(1) 完成查找操作。

优点:

时间复杂度极低:平均查找时间复杂度为 O(1)。

支持动态增删:哈希表允许快速的插入和删除操作。

缺点:

受哈希函数的影响:如果哈希函数设计不当,会产生大量冲突,最坏情况时间复杂度为 O(n)。

不能保证数据顺序:哈希表无法进行有序查找。

代码示例:



class HashTable {
  private table: any[];
 
  constructor(size: number) {
    this.table = new Array(size);
  }
 
  hash(key: string, size: number): number {
    let hash = 0;
    for (let i = 0; i < key.length; i++) {
      hash += key.charCodeAt(i);
    }
    return hash % size;
  }
 
  search(key: string): any {
    const index = this.hash(key, this.table.length);
    return this.table[index];
  }
 
  insert(key: string, value: any) {
    const index = this.hash(key, this.table.length);
    this.table[index] = value;
  }
}

使用场景:

查找频繁的场景:如缓存、数据库索引等需要高效查找的场景。

动态数据集合:适用于对数据增删要求较高的系统。

2.7. B+树查找(B+ Tree Search)

B+树是一种自平衡树结构,适用于数据库和文件系统中的数据查找,每个节点包含多个关键字,叶子节点按顺序排列。

优点:

适合磁盘存储:B+树的扇出较高,减少了树的高度,适合存储大量数据,查找时间复杂度为 O(log n)。

有序查找:叶子节点按顺序排列,便于区间查找。

缺点:

实现复杂:相比二叉树,B+树的实现和维护复杂度更高。

代码示例:

B+树的实现较为复杂,这里省略代码,通常作为数据库的核心数据结构。

使用场景:

数据库和文件系统:B+树广泛应用于数据库索引和大规模文件存储。

3. 递归算法

递归是指函数调用自身来解决问题的编程方式。通常用于分解复杂问题,将其化简为更小的同类问题。

优点:

代码简洁:对于具备递归结构的问题,递归实现通常非常简洁。

易于解决分治问题:递归非常适合解决分治问题。

缺点:

效率问题:递归可能导致重复计算,导致时间复杂度上升。

栈空间有限:过深的递归可能导致栈溢出。

代码示例:



function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

使用场景:

分治问题:如归并排序、快速排序、汉诺塔问题等。

树形结构:如二叉树遍历、图的深度优先搜索等。

4. 贪心算法(Greedy Algorithm)

贪心算法通过每一步选择当前最优解来期望得到全局最优解,适用于具有贪心选择性质的问题。

优点:

实现简单:每一步选择当前最优解,算法逻辑简单。

局部最优:对某些问题,贪心算法能够快速获得全局最优解。

缺点:

不能保证全局最优解:贪心算法不适用于所有问题,容易得到局部最优而非全局最优。

适用范围有限:只能用于某些特定问题。

代码示例:



function minCoins(coins: number[], amount: number): number {
  coins.sort((a, b) => b - a); // 从大到小排序
  let count = 0;
  for (let coin of coins) {
    if (amount === 0) break;
    count += Math.floor(amount / coin);
    amount %= coin;
  }
  return amount === 0 ? count : -1; // 如果无法凑齐返回 -1
}

使用场景:

活动选择问题:选择不冲突的最大活动数。

最小生成树问题:如 Prim 算法和 Kruskal 算法。

背包问题:在可分割的物品条件下,使用贪心算法解决背包问题。

5. 回溯算法(Backtracking Algorithm)

回溯算法通过在解空间中逐步构建解,遇到不满足条件的部分时回退,尝试其他可能的路径,直到找到所有解或满足条件的最优解。适用于解决组合、排列、子集等问题。

优点:

系统性搜索:回溯算法能够全面探索解空间,保证找到所有解或可能存在的最优解。

解决复杂问题:适合用于解决需要枚举所有可能解的复杂问题,如数独、八皇后等。

缺点:

效率较低:由于需要遍历所有可能的解,回溯算法在大规模问题上可能效率不高,容易出现指数级时间复杂度。

存储消耗:回溯过程通常会用递归来保存中间状态,可能消耗大量内存。

代码示例(八皇后问题):

在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。



const N: number = 8;
let result: string[][] = [];
 
function solveNQueens(): string[][] {
  let board: string[][] = Array.from({ length: N }, () => Array(N).fill('.'));
  backtrack(board, 0);
  return result;
}
 
function backtrack(board: string[][], row: number): void {
  // 如果已经放置了 N 个皇后,将当前棋盘的状态加入结果集
  if (row === N) {
    result.push(board.map(r => r.join('')));
    return;
  }
 
  // 遍历当前行的每一列,尝试放置皇后
  for (let col = 0; col < N; col++) {
    if (!isValid(board, row, col)) continue; // 如果当前位置不合法,跳过
 
    board[row][col] = 'Q'; // 放置皇后
    backtrack(board, row + 1); // 递归处理下一行
    board[row][col] = '.'; // 回溯,移除皇后
  }
}
 
// 检查是否可以在 board[row][col] 放置皇后
function isValid(board: string[][], row: number, col: number): boolean {
  // 检查同列是否有皇后
  for (let i = 0; i < row; i++) {
    if (board[i][col] === 'Q') return false;
  }
 
  // 检查左上对角线是否有皇后
  for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
    if (board[i][j] === 'Q') return false;
  }
 
  // 检查右上对角线是否有皇后
  for (let i = row - 1, j = col + 1; i >= 0 && j < N; i--, j++) {
    if (board[i][j] === 'Q') return false;
  }
 
  return true;
}
 
// 调用函数并输出结果
console.log(solveNQueens());

使用场景:

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

数独问题:在 9×9 的数独棋盘上填入数字,满足行、列和小九宫格的规则。

组合问题:如求解数组的所有子集、排列组合等。

6. 动态规划(Dynamic Programming)

动态规划是一种优化算法,适用于求解具有重叠子问题和最优子结构性质的问题。通过将问题分解为更小的子问题,逐步解决并存储每个子问题的结果,以避免重复计算。

优点:

解决重叠子问题:通过保存子问题的解,减少了重复计算,提升了算法效率。

时间复杂度低:相对于递归算法,动态规划可以将指数时间复杂度降低到多项式时间。

缺点:

空间复杂度较高:由于需要存储子问题的解,可能会消耗较多的存储空间。

不适合所有问题:动态规划只能解决具有最优子结构和重叠子问题性质的问题。

使用场景:

最优子结构和重叠子问题:动态规划适用于具有这两个性质的问题,如最短路径问题、最优序列问题等。

资源分配:常用于解决背包问题、资源调度等与资源分配相关的问题。

字符串处理:编辑距离、最长公共子序列等与字符串匹配相关的问题。

6.1. 斐波那契数列(Fibonacci Sequence)

问题描述:给定一个整数 n,求斐波那契数列的第 n 项。

递推关系: F(n)=F(n−1)+F(n−2)

初始条件:F(0)=0,F(1)=1

代码示例:



function fibonacci(n: number): number {
  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(n)

6.2. 最长公共子序列(Longest Common Subsequence, LCS)

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

递推关系:

当 str1[i]==str2[j] 时,LCS(i, j) = LCS(i-1, j-1) + 1

否则,LCS(i, j) = max(LCS(i-1, j), LCS(i, j-1))

代码示例:



function longestCommonSubsequence(str1: string, str2: string): number {
  const m = str1.length;
  const n = str2.length;
  const dp = Array.from({ length: m + 1 }, () => 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];
}

时间复杂度:O(m * n)

空间复杂度:O(m * n)

6.3. 背包问题(Knapsack Problem)

问题描述:给定 n 件物品,每件物品有重量和价值,背包的最大承重为 W,求如何选择物品使得背包内的物品总价值最大。

递推关系:

当物品 i 不放入背包时:dp[i][w]=dp[i−1][w]

当物品 i 放入背包时:dp[i][w]=max(dp[i−1][w],dp[i−1][w−weight[i]]+value[i])

代码示例:



function knapsack(weights: number[], values: number[], W: number): number {
  const n = weights.length;
  const dp = Array.from({ length: n + 1 }, () => Array(W + 1).fill(0));
 
  for (let i = 1; i <= n; i++) {
    for (let w = 1; w <= W; 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][W];
}

时间复杂度:O(n * W)

空间复杂度:O(n * W)

6.4. 硬币找零问题(Coin Change Problem)

问题描述:给定硬币面值数组 coins,和一个目标金额 amount,求最少需要多少个硬币凑成该金额。如果无法凑成,返回 -1。

递推关系:dp[i]=min(dp[i],dp[i−coin]+1),其中 coin 是当前硬币面值。

代码示例:



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

时间复杂度:O(amount * coins.length)

空间复杂度:O(amount)

6.5. 编辑距离(Edit Distance)

问题描述:给定两个字符串 str1 和 str2,计算将 str1 转换为 str2 所需的最少编辑操作次数。操作包括插入、删除和替换。

递推关系:

当 str1[i]==str2[j] 时,dp[i][j] = dp[i-1][j-1]

否则,dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])

代码示例:



function minDistance(str1: string, str2: string): number {
  const m = str1.length;
  const n = str2.length;
  const dp = Array.from({ length: m + 1 }, () => 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 (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], dp[i][j - 1], dp[i - 1][j - 1]);
      }
    }
  }
  return dp[m][n];
}

时间复杂度:O(m * n)

空间复杂度:O(m * n)

7. 补充资料

LeetCode: https://leetcode.com/

LeetCode: 力扣 (LeetCode) 全球极客挚爱的技术成长平台

力扣加加: https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution

Hello 算法: Hello 算法

LeetCode 算法动画: https://github.com/MisterBooo/LeetCodeAnimation

算法可视化: 通过动画可视化数据结构和算法<br> – VisuAlgo

动画算法: https://www.donghuauanfa.com/portal

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
喜欢哆啦的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容