快速排序

快速排序

0x00 传说中最快的排序算法

​ 大多数情况下,快速排序的性能相当强悍。与归并排序思想差不多都是分而治之。

​ 1 选一个元素做主元(pivot),将原来的集合分为两大块,左边的集合小于主元,右边的大于主元

​ 2 递归的处理左边集合和右边集合

​ 3 将左边 主元 右边 构成一个集合,就完成了

​ 伪代码描述如下

QQ截图20200212191948

​ 细节之处 1如何选出主元 2怎么分成独立子集

0x01 选主元

​ 如果选出的主元能够将集合分成等大的两个 就是最好情况 ,反之 每次产生一个空集一个n-1元素的集合时间复杂度最坏,时间复杂度为O(N^2) 下图就是一个例子

QQ截图20200212192408

​ 但是刻意选出所有元素的中位数,是一件很复杂的事,所以改进了,取头中尾的中位数(一般)

QQ截图20200212192921

这个函数能从头中尾三个位置返回中位数,同时将集合进行交换,L<CENTER<R,并且将中与R-1进行交换(原因在于现在的中间位置上的数只是三个数中的中位数,不是所有数的中位数,不是合适的位置,换到尾部,方便后面确定位置)

0x02 分子集

​ 经过上面的选主元函数的处理后,集合变成如下图的样子,这时候我们用两个指针,来分子集。方法如下:

​ 1 定义指针i,j i的初始位置为L+1(不从L开始因为选主元的函数已经确定L所在位置小于主元) 同样j的初始位置为R-2(R比主元大 R-1是主元)

​ 2 data[i]与主元比较,如果比主元小则i++,一直循环到data[i]比主元大

​ 3 data[j]与主元比较,如果比主元大则j–,一直循环到data[j]比主元小

​ 4 如果i<j 那么 交换data[i] data[j]的值,使其符合要求 反之 说明已经完成了自己划分,i所在的位置就是主元应该的位置(i前面的都比主元小)所以交换主元data[R-2]与data[i] 这一步就是快速排序之所以快的原因,因为每一次递归都能确定一个元素的最终的位置,而其他诸如插入排序都是零时的位置。

​ 这个时候已经将序列分为 左边比主元小–主元–右边比主元大 三段 ,然后对于 左 右 递归这个过程

QQ截图20200212193615

​ 如果有元素和主元大小一样呢? 一个极端情况就是 所有元素都和主元一样大,有以下两种方案: ​ a 上述第二步 第三步不变 严格执行 也就是如果相等,则推出第二步 第三步的循环

​ b 上述第二步 第三步 改成小于等于 大于等于

​ 方法a 我们可以发现 每一次都能将序列 分成 等大的 左子列 右子列 但代价是 i 和j前进的非常慢,每次前进一步都要交换一次元素 时间复杂度为O(nlogn)

​ 方法b 由于都相等,所以i j会一直前进 ,由于我们最后选择i的位置当主元的位置,那么最终左子列长度为n-2,右子列为1 (主元不算在其中 所以综合为n-1),这样的情况下,分子列等于没分,时间复杂度为O(n^2)

​ 综上 我们选方案a

QQ截图20200212194733

0x03 关于递归的优化

​ 快速排序使用的递归 ,那么递归有个问题就是 需要占用系统堆栈,如果我们真的按照上面的伪代码描述的 一直递归分到 子序列为1才返回,那么对于较大的数据,其堆栈开销是很大的,而且在小规模数据的时候,快速递归不一定比其他排序比如选择排序快,那么,我们可以递归到子序列为某一个值的时候停止递归,直接调用其他排序来处理子序列,我们把这个值称为阈值cutoff

QQ截图20200212200259

​ 快速排序的时间复杂度为O(nlogn)