快速排序
重点
- 快速排序算法及其最好、最坏时间和平均时间;
- 随机快速排序算法及其期望时间;
Partition
算法;- 几个排序算法的比较:插入排序、归并排序、堆排序和快速排序
快速排序算法利用了分治法的思想,是一种非常巧妙的算法。其最坏时间复杂度为 ,期望时间复杂度为 ,且其中的常数因子非常小,另外,快速排序算法是一种原址排序。
基本思想
快速排序递归地将问题划分成两部分,然后分别解决,具体地,其分支策略如下:
- 分解:数组
A[p...r]
被划分成两个子数组A[p...q - 1]
和A[q + 1...r]
,使得前者中的每一个元素都小于等于A[q]
后者的每一个元素都大于等于A[q]
; - 解决:通过递归调用快速排序,堆子数组进行排序;
- 合并:无需合并。
因此快速排序算法的代码可以总结如下:
快速排序 | |
---|---|
显然其关键在于对数组的划分,即 Partition
算法。
数组的划分
划分数组的 Partition
算法是快速排序的灵魂,实现了对数组的原址排序。算法的关键是两个位置指针 ,其中 作为一个递增的指针对数组进行遍历, 则指示着较小的那个子数组(一般是左侧)的最后一个位置。
具体地,令 从 开始递增,且比较 A[j]
和数组的最后一个元素(被称为划分的主元素)即 A[r]
,如果 A[j]
小于等于 A[r]
,则将 A[j]
换到左侧较小的子数组中去,同时更新指针 使其满足性质。
如此对整个数组遍历一次,使得数组除了最后一个元素外,之前的每一个元素都以最后一个元素为分界,排列在位置 的两侧,因此最后只需要交换 A[r]
和 A[i + 1]
并返回 即可完成划分操作。
划分数组 | |
---|---|
利用下面的循环不变量可以证明算法的正确性:
循环不变量
在算法 3 至 6 行循环体的每一轮迭代开始时,对任意数组下标 ,有:
- 若 ,则 ;
- 若 ,则 ;
- 若 ,则 。
具体证明本文不再赘述。划分算法对数组进行一次遍历,因此其时间复杂度为 。
快速排序的性能
快速排序算法的性能与划分操作结果是否平衡有关,也即与用来划分的主元素有关。如果划分平衡,则快速排序算法的性能与归并排序一样,如果划分不平衡,那么快速排序的性能就接近于插入排序了。
最坏情况划分
划分的最坏情况发生在两个数组的大小分别为 和 时,不妨设每次划分都是这样的情况,则算法的递归式为
将每一层的结果累加得到 。
最好情况划分
在最平衡的情况下,划分的两个子数组的大小都近似为 ,不妨设每次划分都是这样的情况,则算法的递归式为
利用主定理 因此算法的复杂度为 。
期望的划分情况
快速排序的平均运行时间更接近于其最好情况,而非最坏情况。可以考虑每次划分都是 这样看起来非常不均衡的划分:设递归深度为 ,则有
每层的代价为 ,因此总代价为 ,接近于最好情况。事实上,即使是 这样的划分也一样,因此只要划分是常数比例的,算法的运行时间总是 。
快速排序的随机化版本
通过上面的分析我们知道,快速排序的性能取决于划分的子集是否平衡,因此可以考虑引入随机抽样来使得划分尽可能平衡。我们在 Partition
算法中加入一步随机抽样选择主元素,实现随机化快速排序。
快速排序的随机化版本 | |
---|---|
在输入元素互异的情况下,使用 RandomizedPartition
的快速排序算法的期望运行时间为 。