算法导论 总结索引 | 第二部分 第七章:快速排序

1、对于包含n个数 的输人数组来说,快速排序 是一种 最坏情况时间复杂度 为Θ(n2)的排序算法

虽然 最坏情况 时间复杂度很差,但是 快速排序通常是 实际排序应用中 最好的选择,因为它的平均性能 非常好:它的期望 时间复杂度 是Θ(n lgn),而且 Θ(n lgn)中 隐含的常数因子 非常小

另外,它还能够进行 原址排序,甚至 在虚存环境中 也能很好地工作

1、快速排序的描述(110)

1、分治思想
对一个典型的子数组 A[p…r] 进行快速排序的 三步分治过程:

分解:数组A[p…r] 被划分为两个(可能为空)子数组 A[p…q-1] 和 A[q+1…r],使得 A[p…q-1] 中的每一个元素 都小于等于 A[q],而A[q]也 小于等于 A[q+1…r]中的 每个元素
其中,计算下标q 也是划分过程的一部分

解决:通过递归调用 快速排序,对子数组 A[p,q-1] 和 A[q+1…r] 进行排序

合并:因为子数组 都是 原址排序的,所以 不需要 合并操作;数组 A[p…r] 已经有序

QUICKSORT(A, p, r)
	if p < r
		q = PARTITION(A, p, r)
		QUICKSORT(A, p, q-1)
		QUICKSORT(A, q+1, r)

为了排序一个数组A的 全部元素,初始调用是 QUICKSORT(A, 1, A.length)

2、数组的划分:算法的关键部分是 PARTITION 过程,它实现了 对子数组 A[p…r] 的 原址重排

PARTITION(A, p, r)
	x = A[r] // 主元,总是选 最后一个
	i = p - 1 // 开始到i是 更小的数字段,i到j是 更大的数字段
	for j = p to r - 1 // 待排数字段
		if A[j] <= x
			i = i + 1 // 选好目标位置
			exchange A[i] with A[j]
	exchange A[i + 1] with A[r]
	return i + 1 // 最后已经排好的p的 下标位置

PARTITION总是选择一个 x = A[r] 作为主元

循环不变量(快排核心:三个 数字段(代码注释中 也写了)):在 第3-6行 循环体 的每一轮 迭代 开始时,对于 任意数组下标k,有:
1)若 p<=k<=i,则 A[k] <= x
2)若 i+1 <= k <= j-1,则 A[k] > x
3)若 k = r,则 A[k] = x
对于 下标j 到 r - 1,是待排数字段
快速排序 过程
数组项A[r] 是 主元x,浅阴影部分的数组元素都在划分的 第一部分,其值都 不大于x。深色阴影部分的元素 都在划分的 第二部分,其值 都大于x。无阴影部分 是还未分入 这两个部分。最后白色元素 就是x
交换过程
该循环不变量 还可以证明 正确性(111)

3、在PARTITION的 最后两行中,通过将 主元与最左的大于x的元素 进行交换,就可以将 主元 移动到它在 整个数组中的正确位置(最终位置)上,并返回主元的新下标,此时 主元排序完毕

4、当数组 A[p…r] 中的元素 都相同时,PARTITION返回的q值 是什么?修改PARTITION,使当数组A[p…r] 中所有元素的值都相同时,q=⌊(p+r)/2⌋
当数组A[p…r]中的元素都相同时,PARTITION返回的q值是r

修改PARTITION,对主元素相等的元素 进行计数处理,因为i会走到r-1,只要 让n也走到r-1,最后返回 i + 1 - ⌈n/2⌉

PARTITION(A, p, r)
    x = A[r]
    i = p - 1
    n = 0
    for j = p to r-1
        if A[j] ≤ x
            if A[j] == x
                n = n + 1
            i = i + 1
            exchange A[i] with A[j]
    exchange A[i+1] with A[r]
    return i + 1 - ⌈n/2

5、修改QUICKSORT,使得它能够以非递增序进行排序:大于等于时 操作(i 往后推到 j的新位置,再交换:即开始的两个数字段 换成大于主元的,接着的数字段 换成小于主元的)

PARTITION(A, p, r)
    x = A[r]
    i = p - 1
    for j = p to r-1
        if A[j] ≥ x
            i = i + 1
            exchange A[i] with A[j]
    exchange A[i+1] with A[r]
    return i + 1
 
QUICKSORT(A, p, r)
    if p < r
        q = PARTITION(A, p, r)
        QUICKSORT(A, p, q-1)
        QUICKSORT(A, q+1, r)

2、快速排序的性能

1、快速排序的运行时间 依赖于 划分是否平衡,而平衡与否 又依赖于 用于划分的元素。如果划分是 平衡的,那么快速排序算法性能 与归并排序一样。如果划分是 不平衡的,那么快速排序的性能 就接近于 插入排序了

2、最坏情况划分:当划分产生的两个子问题 分别包含了n-1个元素 和 0个元素 时,假设算法的每一次递归调用中 都出现了这种 不平衡划分。划分操作的 时间复杂度是 Θ(n)
算法代价
从直观上来看,每一层递归的代价可以被累加起来,结果为 Θ(n2)

在 最坏情况下,快速排序算法的运行时间 并不比 插入排序更好。当 数组已经 完全有序时,快速排序的时间复杂度 仍然为 Θ(n2)。而 在同样的情况下,插入排序 的时间复杂度为 O(n)

3、最好情况划分:在可能的 最平衡的划分 中,PARTITION得到的 两个子问题的规模 都不大于 n/2。这是因为 其中一个子问题的规模为 ⌊n/2⌋,而 另一个子问题的规模为 ⌈n/2⌉-1。此时,算法运行时间的递归式为:
递归式
根据主定理,上述递归式的解为 T(n) = Θ(n lgn)

4、平衡的划分:假设 划分算法总是产生 9:1的划分,得到的快速排序 时间复杂度的递归式
9:1划分 递归式
树中每一层的代价都是cn,直到在深度 log10(n) = Θ(lgn) 处达到递归的边界条件时为止
递归树
任何一种 常数比例的划分 都会产生深度为 Θ(lgn) 的递归树,其中每一层的时间代价 都是O(n)。因此,只要划分是 常数比例的,算法的运行时间 总是O(n lgn)

5、对于平均情况的直观观察:为了对快速排序的 各种随机情况 有一个清楚的认识,需要对 遇到各种输人的出现频率做出 假设。快速排序的行为 依赖于 输入元素中的元素的值 的相对顺序,而不是 特定值本身
与 对雇佣问题 所做的概率分析类似,这里 也假设输人数据的所有排列都是 等概率的

在一个 差的划分后面 接着一个好的划分,这种组合 产生出三个子数组,大小分别为0、(n - 1) / 2 - 1 和(n - 1) / 2。这一组合的划分代价为Θ(n) + Θ(n - 1) = Θ(n),代价 并不比 最有情况下的划分 更差

因此,当好和差的划分 交替出现时,快速排序的时间复杂度 与全是好的划分时一样,仍然是 O(n lgn)。区别只是O符号中隐含的常数因子要 略大一些
图片示意
6、当数组A的所有元素都具有相同值时,QUICKSORT的时间复杂度是 Θ(n2)

7、当数组A包含的元素不同,并且是按降序 / 顺序排列的时候,QUICKSORT的每一次递归调用划分产生的 两个子问题分别包含了 n-1个元素和0个元素,这也是QUICKSORT的 最坏情况,时间复杂度是 Θ(n2)

8、对 几乎有序的输入序列 进行排序,INSERTION-SORT的性能 往往要优于 QUICKSORT:当输入数组已经 完全有序时,插入排序的 时间复杂度为 O(n),快速排序的时间复杂度为 Θ(n2) 。所以 在一个对几乎有序的输入序列 进行排序的问题上,INSERTION-SORT 的性能 往往要优于 QUICKSORT(容易 形成不平衡的分割)

3、快速排序的随机化版本

1、前提假设是:输人数据的所有排列 都是等概率的。但是在实际工程中,这个假设并不会总是成立。通过在算法中引入 随机性,从而使得算法 对于所有的输入都能获得较好的 期望性能。很多人都选择 随机化版本的快速排序 作为大数据输人情况下的 排序算法

2、采用 随机抽样 的随机化技术,与 始终采用 A[r] 作为 主元的方法不同,随机抽样 是从 子数组 A[p…r] 随机选择一个元素 作为主元。保证 主元素 x=A[r] 是等概率地 从子数组的 r - p + 1个元素中 选取的,因为 主元素是随机选取的,期望 在评价情况下,对 输入数组的划分 是比较均衡的

对PARTITION 和 QUICKSORT的代码的改动 非常小。在新的划分程序中,只是在 真正进行划分前 进行一次交换

// 增加的代码(进行交换)
RANDOMIZED-PARTITION(A, p, r)
	i = RANDOM(p, r)
	exchange A[r] with A[i]
	return PARTITION(A, p, r)

新的快速排序 不再调用PARTITION,而是 调用RANDOMIZED-PARTITION
在RANDOMIZED-QUICKSORT的 运行过程中,在最坏情况下,随机数生成器RANDOM 被调用了 T(n) = T(n - 1) + 1 = Θ (n) 次,在最好情况下,随机数生成器RANDOM 被调用了T(n) = 2T(n / 2) + 1 = Θ (n)

3、因为随机化算法 引入了随机性,从而使得算法 对于所有的输入 都能获得较好的期望性能。所以 分析随机化算法的 期望运行时间,而不是 其最坏运行时间

4、快速排序分析

快速排序 更严谨的分析。首先 从最坏情况 开始,其 方法可以用于 QUICKSORT 和 RANDOMIZED-QUICKSORT 的分析,然后给出 RANDOMIZED-QUICKSORT 的 期望运行时间

4.1 最坏情况分析(116)

4.2 期望运行时间

1、运行时间 和 比较操作:QUICKSORT 和 RANDOMIZED-QUICKSORT 除了 如何选择 主元元素 有差异以外,其他方
面 完全相同。因此,可以在讨论QUICKSORT和PARTITION的基础上分析 RANDOMIZED-QUICKSORT

每次对PARTITION的调用 时,都会 选择一个 主元元素,而且 该元素不会被包含在 后续的对QUICKSORT 和 PARTITION的递归调用中
每次对PARTITION的调用 时,都会选择 一个主元元素,而且 该元素不会被包含在后续的 对QUICKSORT和PARTITION的递归调用中,因此,在快速排序算法的 整个执行期间,至多 只可能调用 PARTITION操作 n次
调用一次PARTITION的时间为 O(1)再加上 一段循环时间,这段时间 与第3~6行中for循环的选代次数 成正比。这一for循环的每一轮迭代 都要在 第4行进行一次比较:比较 主元元素 与 数组A中另一个元素。因此,如果可以统计 第4行被执行的总次数,就能够给出 在QUICKSORT的执行过程中,for循环所花时间的界

假设在PARTITION的第4行中 所做比较的次数为 X,那么QUICKSORT的运行时间为 O(n + X)

2、待排数组中 每一对元素 至多比较一次。因为 各个元素 只与 主元元素 进行比较,并且 在某一次 PARTITION 调用结束后,该次调用中 用到的主元元素 就再也不会 与其他元素 进行比较了
定义
考虑的是 比较操作是否在 算法执行过程中 任意时间发生,而不是 局限在 循环的一次迭代 或 对PARTITION的一次调用中 是否发生。因为 每一对元素 至多比较一次,可以 刻画算法的 总比较次数
算法总比较次数
对上式 两边取期望
对上式两边取期望

考虑两个元素 何时不会进行比较:以数组 {1,2,3,4,5,6,7,8,9,10} 为例,假设 第一个主元是7。对 PARTITION的第一次调用 就将这些输入数字 划分为两个集合:{1,2,3,4,5,6} 和 {8,9,10} 。在这个过程中,主元7 要与所有其他元素 进行比较。但是 第一个集合中的 任何一个元素(比如2)没有(也不会)与第二个元素中的 任何元素(比如9)进行比较

假设 每个元素的值 是互异的,一旦 一个满足Zi < x <Zj 的主元x 被选择后,就知道 以后再也不可能被比较 了
另一种情况,如果Zi在Zij中的 所有其他元素之前 被选为主元,那么Zi就将与Zij中 除了它自身以外的所有元素 进行比较

在Zij中的某个元素 被选为主元之前,整个集合Zij的元素 都属于某一划分的同一分区。因此,Zij中的任何元素 都会等可能地被首先选为 主元,因为 集合Zij中有 j-i+1个元素,并且 主元的选择是随机且独立的,所以 任何元素被首先选为主元的概率是 1 / (j - i + 1)
过程式
第二行成立的原因 在于 其中涉及的两个事件 是互斥的
计算总期望
计算这个累加和时,需要 将变量做个变换(k = j - i)
做变换后 求值
使用RANDOMIZED-PARTITION,在输入元素互异的情况下,快速排序算法的 期望运行时间 为O(n lgn)

3、在最好情况下,快速排序 每次调用PARTITION时 都将数组划分成 相等的两个部分,此时 T(n) = 2T(n/2) + Θ(n)。主方法求得:T(n) = Ω(n lgn),所以快速排序的运行时间为 Ω(n lgn)

4、当对一个长度小于L的子数组 调用快速排序时,让它不做任何排序就返回,当 上层的快速排序调用返回后,对整个数组运行插人排序来 完成排序过程
当对数组调用快速排序直至子数组的长度 小于k时,共调用了 lg(n/k)次迭代调用,时间复杂度为 O(n lg(n/k))
当上层的快速排序调用返回后,对整个数组运行 插入排序时,因为下标为i的元素 肯定小于下标为i+k的元素,所以插入排序的内层循环 每次最多迭代k轮,外层循环 固定迭代n轮,时间复杂度为O(nk)。因此,这一排序算法的期望时间复杂度为 O(nk + n lg(n / k))

  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值