# 认识“分治”思想

⚡ 30 秒速记

  • 分治三步:拆成同类小问题 → 分别解决 → 合并结果
  • 递归要有基线(比如长度 ≤ 1 直接返回),每次拆分规模必须严格变小
  • 合并是关键,只拆不合等于没做
  • 归并排序:拆分简单、合并费力;快速排序:拆分(分区)费力、合并不用做
  • 递归太深可以改成自底向上迭代,分治思想不变

分治就是把大问题拆成几个同样结构的小问题,各自解决,再把结果合起来。 好比一个班的试卷让一个人改太慢,拆给四个组各改一摞,最后汇总分数。排序里的两个经典例子:归并排序拆分很简单,从中间切开就行,力气花在合并上;快速排序正好反过来,力气花在分区上,分完左右各自排好就天然有序,不用合并。写递归时我会先确认两件事:基线是什么,每次拆分规模是不是一定变小。

本节我们要学习的两个排序算法都是对“分治”思想的应用。 “分治”,分而治之。其思想就是将一个大问题分解为若干个子问题,针对子问题分别求解后,再将子问题的解整合为大问题的解。

利用分治思想解决问题,我们一般分三步走:

  • 分解子问题
  • 求解每个子问题
  • 合并子问题的解,得出大问题的解

下面我们一起来看看分治思想是如何帮助我们提升排序算法效率的。

💬 面试官追问

  • 数组切两半,各自排好后直接拼起来,算分治吗?

    不算完整的分治,少了合并。[3, 4] 和 [1, 2] 各自有序,直接拼是 [3, 4, 1, 2],还是乱的,得用双指针合并。

  • 写一个分治排序的递归函数,入参、返回值和终止条件怎么定?

    入参是数组或 [lo, hi) 区间,返回有序结果,长度 ≤ 1 直接返回。区间开闭全程统一用左闭右开,切分点 mid = (lo + hi) >> 1,不会漏也不会重叠。

  • 数据太大,递归太深栈溢出,要放弃分治吗?

    不用,换实现方式就行。归并可以写成自底向上:先两两合并长度 1 的段,再合并长度 2 的,以此类推。其实归并递归深度只有 log n,100 万条才 20 层,真栈溢出多半是切分写错导致不收敛。

  • 线上排序结果偶尔少元素,叶子节点都没问题,先查哪?

    查合并。最常见的是一侧耗尽后忘了把另一侧剩余部分接上。加个断言:合并结果长度等于左右长度之和。

  • 快排和归并都是分治,区别在哪?

    归并先拆后合,排序发生在合并阶段,稳定、时间稳定 O(n log n),但要 O(n) 辅助空间。快排先分区再递归,原地排,平均 O(n log n),最坏 O(n²),不稳定。

# 归并排序

⚡ 30 秒速记

  • 一直对半切到单个元素,再两两用双指针合并成有序段
  • 递归只负责拆,真正的排序发生在合并
  • 每层合并处理 n 个元素,共 log n 层 → 稳定 O(n log n),不分好坏情况
  • 数组版要 O(n) 辅助空间;相等时先取左边才稳定(<= 而不是 <)
  • 适合链表排序、外部排序、要求稳定的场景

归并排序就是先把数组一直对半切到只剩一个元素,再两两合并,合并时用双指针保证每次合出来都是有序的。 单个元素天然有序,所以整件事就变成了反复合并两个有序数组,这一步是线性的。一共 log n 层,每层合并所有 n 个元素,所以不管输入什么样都是 O(n log n),这是它比快排稳的地方。代价是数组版要 O(n) 的额外空间。合并时两边相等要先取左边,写成 <=,不然就不稳定了。

webapp
公众号
开发者导航
切换夜间模式
点击侧边栏上一篇
点击侧边栏下一篇
折叠侧边栏
收起全部