当前位置:  首页>> 技术小册>> JavaScript进阶实战

20 | 算法思想:JS中分治、贪心、回溯和动态规划

在JavaScript编程的进阶之旅中,掌握高效的算法思想是提升代码性能、解决复杂问题的关键。本章将深入探讨四种在算法设计中极为重要且广泛应用的策略:分治、贪心、回溯和动态规划。每种策略都有其独特的适用场景和解决问题的方式,理解并熟练运用它们,将极大地拓宽你的编程视野和问题解决能力。

20.1 分治策略(Divide and Conquer)

分治策略是一种将复杂问题分解成若干个简单子问题来解决,然后合并子问题的解以得到原问题的解的算法设计策略。其核心思想在于“分而治之”,即将大问题划分为小问题,解决小问题,然后将解决的小问题合并起来,从而完成对整个大问题的求解。

案例:归并排序

归并排序是分治策略的经典应用之一。它将数组分成两半,对每半部分递归地应用归并排序,然后将排序好的两半合并在一起。以下是JavaScript实现的归并排序示例:

  1. function mergeSort(arr) {
  2. if (arr.length <= 1) return arr;
  3. const middle = Math.floor(arr.length / 2);
  4. const left = arr.slice(0, middle);
  5. const right = arr.slice(middle);
  6. return merge(mergeSort(left), mergeSort(right));
  7. }
  8. function merge(left, right) {
  9. let result = [], indexLeft = 0, indexRight = 0;
  10. while (indexLeft < left.length && indexRight < right.length) {
  11. if (left[indexLeft] < right[indexRight]) {
  12. result.push(left[indexLeft]);
  13. indexLeft++;
  14. } else {
  15. result.push(right[indexRight]);
  16. indexRight++;
  17. }
  18. }
  19. return result.concat(left.slice(indexLeft)).concat(right.slice(indexRight));
  20. }

20.2 贪心策略(Greedy Algorithm)

贪心算法总是做出在当前看来最好的选择,即它希望通过所做的局部最优选择来达到全局最优解。需要注意的是,贪心算法并不总能保证得到全局最优解,但它因其简单高效而得到广泛应用。

案例:活动选择问题

给定一系列活动,每个活动都有一个开始时间和一个结束时间,求可以安排的最大活动数量,使得没有两个活动的时间区间重叠。这个问题可以使用贪心算法解决,策略是选择最早结束的活动,因为它为后面的活动留下了最多的选择空间。

  1. function maxActivities(activities) {
  2. activities.sort((a, b) => a.finish - b.finish); // 按结束时间排序
  3. let count = 1;
  4. let lastEnd = activities[0].finish;
  5. for (let i = 1; i < activities.length; i++) {
  6. if (activities[i].start >= lastEnd) {
  7. count++;
  8. lastEnd = activities[i].finish;
  9. }
  10. }
  11. return count;
  12. }

20.3 回溯算法(Backtracking)

回溯算法是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化来撤销上一步或上几步的计算,再通过不同的途径来探索其他的候选解。

案例:N皇后问题

N皇后问题是一个经典的回溯算法问题,要求在N×N的棋盘上放置N个皇后,使得她们互不攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上。

  1. function solveNQueens(n) {
  2. let result = [];
  3. let board = Array.from({ length: n }, () => Array(n).fill('.'));
  4. function isSafe(row, col) {
  5. // 检查列和两对角线
  6. for (let i = 0; i < row; i++) {
  7. if (board[i][col] === 'Q' || board[i][col - row + i] === 'Q' || board[i][col + row - i] === 'Q') {
  8. return false;
  9. }
  10. }
  11. return true;
  12. }
  13. function solve(row) {
  14. if (row === n) {
  15. result.push(board.map(row => row.join('')));
  16. return;
  17. }
  18. for (let col = 0; col < n; col++) {
  19. if (isSafe(row, col)) {
  20. board[row][col] = 'Q';
  21. solve(row + 1);
  22. board[row][col] = '.'; // 回溯
  23. }
  24. }
  25. }
  26. solve(0);
  27. return result;
  28. }

20.4 动态规划(Dynamic Programming)

动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。它存储子问题的解以避免重复计算,通常用于解决最优化问题。

案例:斐波那契数列

斐波那契数列是一个经典的动态规划问题,其中每个数字是前两个数字的和。动态规划通过自底向上的方式计算每个斐波那契数,避免了重复计算。

  1. function fibonacci(n) {
  2. if (n <= 1) return n;
  3. let dp = Array(n + 1).fill(0);
  4. dp[1] = 1;
  5. for (let i = 2; i <= n; i++) {
  6. dp[i] = dp[i - 1] + dp[i - 2];
  7. }
  8. return dp[n];
  9. }

案例:最长公共子序列(LCS)

最长公共子序列问题是另一个典型的动态规划应用。给定两个字符串,找到它们之间的最长公共子序列。

  1. function longestCommonSubsequence(s1, s2) {
  2. const m = s1.length, n = s2.length;
  3. let dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
  4. for (let i = 1; i <= m; i++) {
  5. for (let j = 1; j <= n; j++) {
  6. if (s1[i - 1] === s2[j - 1]) {
  7. dp[i][j] = dp[i - 1][j - 1] + 1;
  8. } else {
  9. dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
  10. }
  11. }
  12. }
  13. // 构建LCS
  14. let lcs = '';
  15. let i = m, j = n;
  16. while (i > 0 && j > 0) {
  17. if (s1[i - 1] === s2[j - 1]) {
  18. lcs = s1[i - 1] + lcs;
  19. i--;
  20. j--;
  21. } else if (dp[i - 1][j] > dp[i][j - 1]) {
  22. i--;
  23. } else {
  24. j--;
  25. }
  26. }
  27. return lcs;
  28. }

通过本章的学习,你应能对分治、贪心、回溯和动态规划这四种算法思想有深入的理解,并能根据实际问题选择合适的算法策略进行求解。每种算法都有其独特的优势和局限性,掌握它们将极大地提升你的编程能力和问题解决能力。


该分类下的相关小册推荐: