卜算法学习笔记-02-分而治之算法01
给定一个问题,如何求解?首先查看最简单的实例能否求解,假如可以求解的话,下一步就是思考能否将大的实例分解成小的实例,以及能否将小的实例组合成为大的实例,如果都可以的话就称实例能归约,这个问题具有递归结构,可以设计递归算法进行求解
排序问题:对数组的归约
排序问题:
- 输入:一个包含 \(n\) 个元素的数组 \(A[0..n − 1]\),其中每个元素都是整数;
- 调整元素顺序后的数组 \(A\),使得对任意的两个下标 \(0 ≤ i < j ≤ n − 1\),有 \(A[i] ≤ A[j]\)。
依据元素下标拆分数组:插入排序与归并排序
第一种拆分方案及插入排序算法
- 算法分析
我们只需执行一个简单操作即可将数组 \(A[0..n − 1]\) 分解成两部分:前 \(n − 1\) 个元素 \(A[0..n − 2]\),以及单独一个元素 \(A[n − 1]\)。前 \(n − 1\) 个元素组成一个小 的数组,是原给定实例的子实例。在将原给定实例拆分成子实例之后,我们假定子实例已被求解,对数组来说,所谓子实例的解就是已经排好序的小的数组 \(A[0..n − 2]\)。要想完成对整个数组 \(A[0..n−1]\) 的排序,我们只需将最后一个元素 \(A[n−1]\)和小数组 \(A[0..n−2]\) 中的元素逐个比较,然后将 \(A[n − 1]\) 插入到合适的位置即可。连续应用递归调用,最终会到达基始情形:当 \(n = 1\) 时,数组 \(A\) 只有一个元素,此时无需排序,直接返回即可。
![插入排序伪代码]()
- 时间复杂度:
将上述递归式展开:
算法低效的原因是:在运行过程中,问题规模是呈线性下降的,即每次递归操作都是将规模为 \(n\) 的问题分解成一个规模为 \(n − 1\) 的子问题。
第二种拆分方案及归并排序算法
- 算法分析:
将大的数组 \(A[0..n − 1]\)按下标拆分成规模相同的两半,即 \(A[0..⌈ \frac{n}{2} ⌉ − 1]\)和 \(A[⌈ \frac{n}{2} ⌉..n − 1]\);每一半依然是数组,形式相同,但是规模变小,因此是原给定实例的子实例。通过迭代执行分解操作,即可使得问题规模呈指数形式下降。在使用递归调用将小的数组排好序之后,我们只需依据这两个已排好序的小的数组,“归并”(Merge)出整个数组。这里的归并包括两层意思:合并、以及排序。
![归并排序伪代码]()
归并过程:
![归并过程伪代码]()
循环不变量,是指关于程序行为的一个断言;这个断言在循环起始时成立,并且每执行一轮循环时都保持成立,因此可以推论出当循环结束时,断言必定成立。 - 时间复杂度分析
分而治之算法时间复杂度分析及Master定理
在分而治之算法中,一种常见的情况是将一个规模为 \(n\) 的实例归约成 \(a\) 个子实例,每个子实例规模都相同(设为 \(\frac{n}{b}\) )。假如“组合”子实例解的时间开销是 \(O(n^d)\),则我们可将时间复杂度 \(T(n)\) 的递归关系及基始情形表示如下:
对于子问题比较规整的情况,即每个子问题的规模都相同,T(n) 上界的显式表达式已被总结成 Master 定理:
依据元素的值拆分数组-快速排序
算法分析
依据元素的数值将大数组拆分成小数组,即选定一个元素作“中心元”(Pivot),比中心元数值小的元素组成一个小数组,比中心元数值大的那些元素组成另一个小数组。

时间复杂度
称排序后的数组 \(A\) 为 \(\tilde{A}\),因此数组 &A$ 的最小元是 \(\tilde{A}[0]\), 最大元是 \(\tilde{A}[n − 1]\),中位数是 \(\tilde{A}[⌈ \frac{n}{2} ⌉]\)。我们在选择中心元时可能面临如下两种情况:
(1) \(\tilde{A}[n − 1]\)/ \(\tilde{A}[0]\): 这样只会生成一个子实例,规模减少了 1,呈线性降低。如果在每一次迭代都是如此选择的话, 运行过程就与 InsertionSort 算法相同,时间复杂度为:
(2) \(\tilde{A}[⌈ \frac{n}{2} ⌉]\): 这样会生成两个子实例,每个子实例的规模都是原来的一半,呈指数下降。如果在每一次迭代都是如此选择的话,运行过程就与 MergeSort 算法相同,时间复杂度为:
证明运行时间的期望值依然是 \(O(n \log n)\)

Modified-QuickSort 算法只做了一点修改:随机选择一个元素做中心元之后,先检验一下这个中心元是否足够好;如果足够好,则继续执行后续的比较和排序,否则重新选择一个元素做中心元。所谓的中心元足够好,是指它位于A ̃的中间区域,即\(\tilde{A}[⌈ \frac{n}{4} ⌉] \cdots \tilde{A}[⌈ \frac{3n}{4} ⌉]\),修改后的算法时间复杂度为:
接下来我们分析 QuickSort 算法的时间复杂度。在做具体的分析之前,我们先陈述关于运行时间的 3 点事实:
(1) 运行时间由比较次数界定:我们的目标就是计算期望运行时间 \(\mathbb{E}[X]\)。
(2) 任意两个元素 \(\tilde{A}[i]\) 和 \(\tilde{A}[j]\) 最多只会比较一次
(3) 两个元素\(\tilde{A}[i]\) 和 \(\tilde{A}[j]\)发生比较的概率是\(\frac{2}{j - i + 1}\)
由此计算时间复杂度为:
空间复杂度
需要创建两个辅助数组 \(S_−\) 和 \(S_+\),这样一来,除了数组本身之外还要额外占用 \(n\) 个内存单元,导致当 \(n\) 比较大时,内存需求有时难以满足。
所以有了原位排序算法,为避免开辟辅助数组 \(S_−\) 和 \(S_+\),Lomuto 算法直接将数组 A 的左半部分当做 \(S_−\),存放比中心元小的元素;把数组 A 的右半部分当做 \(S_+\),存放比中心元大的元素




浙公网安备 33010602011771号