卜算法学习笔记-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\) 只有一个元素,此时无需排序,直接返回即可。
    插入排序伪代码
  • 时间复杂度:

\[T(n) = \begin{cases} 1 & n = 1 \\ T(n - 1) + O(n) & otherwise \end{cases} \]

将上述递归式展开:

\[\begin{aligned} T(n) &\leq T(n - 1) + cn \\ &\leq T(n - 2) + c(n - 1) + cn \\ &\cdots \\ &\leq c + \cdots + c(n - 1) + cn \\ &= O(n^2) \end{aligned} \]

算法低效的原因是:在运行过程中,问题规模是呈线性下降的,即每次递归操作都是将规模为 \(n\) 的问题分解成一个规模为 \(n − 1\) 的子问题。

第二种拆分方案及归并排序算法

  • 算法分析:
    将大的数组 \(A[0..n − 1]\)按下标拆分成规模相同的两半,即 \(A[0..⌈ \frac{n}{2} ⌉ − 1]\)\(A[⌈ \frac{n}{2} ⌉..n − 1]\);每一半依然是数组,形式相同,但是规模变小,因此是原给定实例的子实例。通过迭代执行分解操作,即可使得问题规模呈指数形式下降。在使用递归调用将小的数组排好序之后,我们只需依据这两个已排好序的小的数组,“归并”(Merge)出整个数组。这里的归并包括两层意思:合并、以及排序。
    归并排序伪代码
    归并过程:
    归并过程伪代码
    循环不变量,是指关于程序行为的一个断言;这个断言在循环起始时成立,并且每执行一轮循环时都保持成立,因此可以推论出当循环结束时,断言必定成立。
  • 时间复杂度分析

\[T(n) = \begin{cases} 1 & n = 1 \\ 2T(\frac{n}{2}) + O(n) & otherwise \end{cases} = O(n \log n) \]

分而治之算法时间复杂度分析及Master定理

在分而治之算法中,一种常见的情况是将一个规模为 \(n\) 的实例归约成 \(a\) 个子实例,每个子实例规模都相同(设为 \(\frac{n}{b}\) )。假如“组合”子实例解的时间开销是 \(O(n^d)\),则我们可将时间复杂度 \(T(n)\) 的递归关系及基始情形表示如下:

\[T(n) = \begin{cases} 1 & n = 1 \\ aT(\frac{n}{b}) + O(n^d) & otherwise \end{cases} \]

对于子问题比较规整的情况,即每个子问题的规模都相同,T(n) 上界的显式表达式已被总结成 Master 定理:

\[\begin{aligned} T(n) &= aT(\frac{n}{b}) + O(n^d) \\ &\leq aT(\frac{n}{b}) + cn^d \\ &\leq a(aT(\frac{n}{b^2}) + c(\frac{n}{b})^d) + cn^d \\ &\leq \cdots \\ &\leq cn^d(1 + \frac{a}{b^d} + (\frac{a}{b^d})^2 + \cdots + (\frac{a}{b^d})^{\log_b n}) + a^{\log_b n} \\ &= \begin{cases} O(n^{\log_b a}) & d < \log_b a \\ O(n^{\log_b a}\log n) & d = \log_b a \\ O(n^{d}) & d > \log_b a \\ \end{cases} \end{aligned} \]

依据元素的值拆分数组-快速排序

算法分析

依据元素的数值将大数组拆分成小数组,即选定一个元素作“中心元”(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 算法相同,时间复杂度为:

\[T(n) = T(n - 1) + O(n) = O(n^2) \]

(2) \(\tilde{A}[⌈ \frac{n}{2} ⌉]\): 这样会生成两个子实例,每个子实例的规模都是原来的一半,呈指数下降。如果在每一次迭代都是如此选择的话,运行过程就与 MergeSort 算法相同,时间复杂度为:

\[T(n) = 2T(\frac{n}{2}) + O(n) = O(n\log n) \]

证明运行时间的期望值依然是 \(O(n \log n)\)
修正版快排伪代码
Modified-QuickSort 算法只做了一点修改:随机选择一个元素做中心元之后,先检验一下这个中心元是否足够好;如果足够好,则继续执行后续的比较和排序,否则重新选择一个元素做中心元。所谓的中心元足够好,是指它位于A ̃的中间区域,即\(\tilde{A}[⌈ \frac{n}{4} ⌉] \cdots \tilde{A}[⌈ \frac{3n}{4} ⌉]\),修改后的算法时间复杂度为:

\[\begin{aligned} T(n) &\leq T(\frac{n}{4}) + T(\frac{3n}{4}) + 2n \\ &\leq (T(\frac{n}{16}) + T(\frac{3n}{16}) + 2\frac{n}{4}) + (T(\frac{3n}{16}) + T(\frac{9n}{16}) + 2\frac{3n}{4}) + 2n \\ &= (T(\frac{n}{16}) + T(\frac{3n}{16}) ) + (T(\frac{3n}{16}) + T(\frac{9n}{16})) + 2n + 2n \\ &\leq \cdots \\ &= O(n \log_{\frac{4}{3}} n) \end{aligned} \]

接下来我们分析 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}\)

\[\begin{aligned} Pr[\tilde{A}[i]与\tilde{A}[j]进行比较] &= \frac{1}{n} + \frac{1}{n} + \frac{n - (j - i + 1)}{n} \times \frac{2}{j - i + 1} \\ &= (\frac{j - i + 1}{n} + \frac{n - (j - i + 1)}{n}) \times \frac{2}{j - i + 1} \\ &= \frac{2}{j - i + 1} \end{aligned} \]

由此计算时间复杂度为:

\[\begin{aligned} \mathbb{E}[X] &= \mathbb{E}[\sum_{i=0}^{n-1}\sum_{j = i + 1}^{n - 1}X_{ij}] \\ &= \sum_{i=0}^{n-1}\sum_{j = i + 1}^{n - 1}\mathbb{E}[X_{ij}] \\ &= \sum_{i=0}^{n-1}\sum_{j = i + 1}^{n - 1}\frac{2}{j - i + 1} \\ &= \sum_{i=0}^{n-1}\sum_{k = 1}^{n - i - 1}\frac{2}{k + 1} \\ &\leq \sum_{i=0}^{n-1}\sum_{k = 1}^{n - 1}\frac{2}{k + 1} \\ = O(n \log n) \end{aligned} \]

空间复杂度

需要创建两个辅助数组 \(S_−\)\(S_+\),这样一来,除了数组本身之外还要额外占用 \(n\) 个内存单元,导致当 \(n\) 比较大时,内存需求有时难以满足。
所以有了原位排序算法,为避免开辟辅助数组 \(S_−\)\(S_+\),Lomuto 算法直接将数组 A 的左半部分当做 \(S_−\),存放比中心元小的元素;把数组 A 的右半部分当做 \(S_+\),存放比中心元大的元素
原位快排伪代码

posted @ 2022-09-12 16:44  eryo  阅读(69)  评论(0)    收藏  举报