文章目录
本文参考UCAS卜东波老师的计算机算法设计分析课程完成
前言
快速排序是复杂度 的排序算法中最常用的一个,也是分治思想的一个具体应用,关于分治思想的理解可以参考我的这篇文章分治思想。在了解快速排序前,我建议先理解分治思想和归并排序,掌握之后再来看快速排序,会有更好的效果。
快速排序
原理
快速排序与归并排序最明显的不同点是,前者基于数值划分,后者基于下标划分。快速排序会从数组中找一个中间数pivot,将数组小于piovt的放到左边,大于pivot的放到右边。这个貌似很容易理解(先不管pivot如何选取),我们可以得到如下伪代码:
quick_sort(A):
S_l = [],S_r = [] # s_l存储小于pivot的值,s_r存储大于pivot的值
choose pivot A[p] # 随机抽取
for i = 0 to |A|-1:
if A[i] < A[p]:
S_l += A[i] # 若比pivot小,则将A[i]放到pivot左边
else:
S_r += A[i] # 否则将A[i]放到pivot右边
quick_sort(S-)
quick_sort(S+)
output S-,A[p],S+ # 按照顺序输出三者
将上面的过程转换成图例如下:
第一次pivot选择4(随机),213放左边,5放右边,第二次pivot选择3,21放左边,右边为空,第三次pivot选择1,左边为空,2放右边。得到有序数组。
时间复杂度
- 估算时间复杂度
上述过程的时间复杂度是多少呢?好像是 ?我们考虑两种极端情况,pivot每次都选中了中位数和pivot每次都选了边边的数。两种划分的结果如下:
发现时间相差较大,写出递推公式分别如下(每一次选定pivot,所有数要和其比较,额外开销时间复杂度 ):- 最好情况
- 最坏情况
- 最好情况
那么一般情况下是什么样的呢?
假设取pivot的时候使得划分比例是3:1,那么划分得到的树如下所示(也就是分治思想一文中提到的不均匀划分):
容易知道这棵树的高度是
,由于每一次划分的额外开销是
所以总的时间复杂度是
,和最好情况一样的复杂度,同样的,按8:1,100:1的划分也都有
有人可能不理解这里,为什么划分100:1这么大,仍然复杂度还是 ,可以从两个角度解释:
1: 无论你分的比例是多少,相对于n都是一个固定的数,100虽然很大,但当n是1000000时,100就显得很小。所以从时间复杂度的角度看是不变的。
2:根据换底公式,有 ,其中 常数对于复杂度不影响
所以,可以发现大部分的情况下,快速排序的时间复杂度都是
-
计算时间复杂度
上面我们大致估计了快排的时间复杂度,由于privot不确定,我们选择用期望来计算真实的时间复杂度。注意快排中耗时的地方主要是元素之间比较,被pivot分到两侧的元素之间不会继续进行比较(因为左边一定小于右边,这个结论一会要用到所以标记为结论1)。如下图所示:
由上可知快排中任意两个元素之间至多比较一次。那么总共要进行多少次比较?为此,定义 如下:
那么A[i]到A[j]之间的比较次数即
公式比较好理解,唯一需要解释的是为什么A[i]与A[j]比较的期望是 ?
我们从最简单的例子入手:
红色圈圈选中的数字是pivot
如图所示,选取(i=1,j=3),可以发现选取1,2,3为pivot的概率等同,而只有在选中1和3为piovt的时候,i与j才会比较,期望为 。同样可证,i=1,j=2等等其他情况下的期望。
更一般地,对于4个元素,有如下图示:
推广到最一般的情况,我们采用数学归纳法。假设对 都有 成立(对数学归纳法不了解的同学可以参考一下百度百科)。我们需要考虑集中pivot选择的可能性:- 1、pivot选在i之前或pivot选择j之后(即i,j在同一边)
那么此时i与j比较就可以递归到一边。此时数据规模小于n,满足归纳假设,所以每一个的概率都是 ( 是每一个元素被选为pivot的概率, 运用了之前的归纳假设),那有多少个呢?显然有 个,所以这部分的概率是 - 2、pivot选到了i或j
此时i与j必然比较一次,对应概率 ,这种情况有两个,所以这部分的概率为 - 3、pivot选到了i和j中间
此时i,j不可能比较,基于结论1,所以这部分的概率是0
综合三种情况,可以得到
因此,有快排的时间复杂度是 - 1、pivot选在i之前或pivot选择j之后(即i,j在同一边)
快速排序的pivot选择
快速排序的好坏与pivot有关,根据上面的推论可以知道,大部分情况下我们都能使得pivot选的不错。为了使得这个可能性更大(即要使pivot选的更接近中间),可以有以下几种思路提高划分效果:
- 1、随机选三个数,取三个数的中位数作为pivot
- 2、取首中尾三个元素,取中位数作为pivot
- 3、添加一层循环,每一层划分之后判断一下 两个条件是否成立,如果不成立,则重新选择,否则退出循环
普通快速排序实现代码
所谓普通快速排序,是不考虑存储空间消耗的非原地排序方式,pivot选择第一个元素,依据上文给出的快排伪代码实现,如下(采用python):
def quick_sort(items,
reverse = False):
'''
快速排序
'''
length = len(items)
if length <= 1:
return items
else:
mid_item = items[0]
right_items = [item for item in items[1:] if item > mid_item]
left_items = [item for item in items[1:] if item <= mid_item]
if reverse:
return list(reversed(quick_sort(left_items) + [mid_item] + quick_sort(right_items)))
return quick_sort(left_items) + [mid_item] + quick_sort(right_items)
if __name__ == '__main__':
items = [4, 3.6, 2, 8.5, 10.5]
new_items = quick_sort(items)
new_reversed_items = quick_sort(items, reverse = True)
print(new_items)
print(new_reversed_items)
总结
关于快速排序,还有一个很重要的特性,就是可以实现原地排序(不需要额外的存储空间),这一点使得它地位要高于归并排序(需要额外一倍存储空间存储排序数组)。这个内容留到下次添加。
如果你觉得文章对你有用,不妨顺手点个赞哦~