Hello 算法:复杂问题的应对策略

每个系列一本前端好书,帮你轻松学重点。

本系列来自上海交通大学硕士,华为高级算法工程师 靳宇栋《Hello,算法》

常言道:大事化小,小事化了。

这不仅是解决生活问题的策略,算法领域同样用途广泛,就是"分治"。

辨别分治

分治,"分而治之",是一种非常重要的算法策略。常基于递归实现,它包括"分"和"治"两个步骤。

  1. :递归地将原问题分解为两个或多个子问题,直至到达最小子问题。
  2. :从已知解的最小子问题开始,将子问题的解进行合并,从而构建出原问题的解。

一个问题是否适合使用分治,通常有以下判断依据。

  1. 可以分解:原问题可以分解成规模更小、类似的子问题,以及能够以相同方式递归地进行划分。
  2. 子问题独立:子问题之间没有重叠,互不依赖,可以独立解决。
  3. 子问题的解可合并:原问题的解可通过合并子问题的解得来。

归并排序

"归并排序"是分治策略的典型应用之一。它满足以上三个判断依据。

  1. 可以递归地将数组划分为两个子数组。
  2. 每个子数组都可以独立进行排序。
  3. 两个有序子数组可以合并为一个有序数组。

处理流程如下:

image

核心代码

javascript 复制代码
/* 移动一个圆盘 */
function move(src, tar) {
    // 从 src 顶部拿出一个圆盘
    const pan = src.pop();
    // 将圆盘放入 tar 顶部
    tar.push(pan);
}

/* 求解汉诺塔问题 f(i) */
function dfs(i, src, buf, tar) {
    // 若 src 只剩下一个圆盘,则直接将其移到 tar
    if (i === 1) {
        move(src, tar);
        return;
    }
    // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf
    dfs(i - 1, src, tar, buf);
    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar
    move(src, tar);
    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar
    dfs(i - 1, buf, src, tar);
}

/* 求解汉诺塔问题 */
function solveHanota(A, B, C) {
    const n = A.length;
    // 将 A 顶部 n 个圆盘借助 B 移到 C
    dfs(n, A, B, C);
}

提升效率

分治不仅可以用来解决问题,还能帮助提升算法效率。

在排序算法中,快速、归并、堆排序,相较于选择、冒泡、插入排序,速度更快,就是因为采用了分治。

其底层逻辑是什么?

可以从"操作数量"和"并行计算"两方面来讨论。

  1. 操作数量

以"冒泡排序"为例,处理一个长度为 n 的数组需要 O(n²) 时间。

假设将数组从中点处分为两个子数组,则划分需要 O(n) 时间,排序每个子数组需要 O((n/2)²) 时间,合并两个子数组需要 O(n) 时间。

总体如下图:

最终,就是比较 n² 和 n²/2 + 2n,进一步简化,当满足 n(n-4) > 0,即n大于4时,划分后的操作数量更少,排序效率应该更高。

  1. 并行计算

分治生成的子问题是相互独立的,因此通常可以并行解决

也就是说,分治不仅可以降低算法的时间复杂度,还有利于操作系统的并行优化。

并行优化在多核或多处理器的环境中尤其有效,因为系统可以同时处理多个子问题,更加充分地利用计算资源,显著减少总体运行时间。

认识汉诺塔

什么是"汉诺塔"?

给定三根柱子,记为 A、B 和 C 。

起始状态下,柱子 A 上套着 n 个圆盘,它们从上到下按照从小到大的顺序排列。

我们的任务是要把这 n 个圆盘移到柱子 C 上,并保持它们的原有顺序不变。在移动圆盘的过程中,需要遵守以下规则。

  1. 圆盘只能从一根柱子顶部拿出,从另一根柱子顶部放入。
  2. 每次只能移入一个圆盘。
  3. 小圆盘必须时刻位于大圆盘之上。

我们将规模为 i 的汉诺塔问题记作f(i) 。例如 f(3) 代表将 3 个圆盘从 A 移动至 C。

求解流程:

  1. 基本情况,只有一个盘子,直接从A到C即可;
  2. 两个盘子,由于需要保证顺序,就需要借助B,先将上面的小圆盘从 A 移至 B ,再将大圆盘从 A 移至 C ,最后将小圆盘从 B 移至 C。
  3. 三个盘子,事情开始变得复杂一些。

但是,因为已知 f(1) 和 f(2) 的解,所以我们可从分治角度思考,将A顶部的两个圆盘看作一个整体,执行下图所示的步骤。这样三个圆盘就被顺利地从 A 移至 C 了。

  1. 令 B 为目标柱、C 为缓冲柱,将两个圆盘从 A 移至 B 。
  2. 将 A 中剩余的一个圆盘从 A 直接移动至 C 。
  3. 令 C 为目标柱、A 为缓冲柱,将两个圆盘从 B 移至 C 。

至此,我们可总结出解决汉诺塔问题的分治策略:将原问题f(n) 划分为子问题 f(n-1) 和 f(1) ,f(n-1) 可以通过相同的方式进行递归划分,直至达到最小子问题 f(1),f(1) 的解是已知的,只需一次移动操作即可。

常见应用

除了"汉诺塔",分治的用途还很多。

  • 寻找最近点对:该算法首先将点集分成两部分,然后分别找出两部分中的最近点对,最后找出跨越两部分的最近点对。
  • 大整数乘法:例如 Karatsuba 算法,它将大整数乘法分解为几个较小的整数的乘法和加法。
  • 矩阵乘法:例如 Strassen 算法,它将大矩阵乘法分解为多个小矩阵的乘法和加法。
  • 求解逆序对:在一个序列中,如果前面的数字大于后面的数字,那么这两个数字构成一个逆序对。求解逆序对问题可以利用分治的思想,借助归并排序进行求解。

可以看出,分治是一种 "润物细无声" 的算法思想,隐含在各种算法与数据结构之中。

小结

本篇为大家呈现了"分治"策略的概念、代码和应用,但只作为一个引子,建立初步印象,更多内容,望各位自行拓展学习,欢迎评论交流。

更多好文第一时间接收,可关注公众号:"前端说书匠"

相关推荐
chushiyunen2 小时前
python中的内置属性 todo
开发语言·javascript·python
麦麦鸡腿堡2 小时前
JavaWeb_请求参数,设置响应数据,分层解耦
java·开发语言·前端
soso19683 小时前
JavaScript性能调优实战案例
javascript
2301_819414303 小时前
C++与区块链智能合约
开发语言·c++·算法
Zaly.3 小时前
【Python刷题】LeetCode 1727 重新排列后的最大子矩阵
算法·leetcode·矩阵
做怪小疯子3 小时前
蚂蚁暑期 319 笔试
算法·职场和发展
计算机安禾3 小时前
【C语言程序设计】第37篇:链表数据结构(一):单向链表的实现
c语言·开发语言·数据结构·c++·算法·链表·蓝桥杯
啊哦呃咦唔鱼3 小时前
LeetCode hot100-73 矩阵置零
算法
阿贵---3 小时前
C++构建缓存加速
开发语言·c++·算法