概念
快速排序是交换类排序,采用分治思想,其基本原理是:通过一趟排序,将待排序数组分割成独立的两部分,其中一部分的关键字均比另一部分小;然后再分别对这两部分序列递归进行快速排序,从而使整个序列有序。
具体算法步骤:
- 在待排序的记录序列中选取一个记录作为枢轴(pivot);
- 通过一趟排序,将所有小于枢轴的记录都移到枢轴的左边,将所有大于枢轴的记录都移到枢轴的右边,其实就是将当前待排序序列分为两部分,左边部分的记录均小于右边部分的记录,这样的操作叫做partition(分割),分割操作结束后,枢轴所处的位置就是最终排序后它所处的位置;
- 对枢轴左右两边的子序列重复步骤1和2,直至所有子记录序列只剩下一个记录为止。
以上步骤中,关键点是 1. 枢轴(pivot)的选取方式; 2. 对分割操作(partition)的细节处理。
未优化的快速排序
- 枢轴的选取:将待排序序列的第1个记录作为枢轴;
- 分割操作 : 分割操作中使用到了交换;
Java实现
1 | // 定义接口 |
演示
为了方便演示,我对上面代码中的分割操作partition方法的代码进行了标注(分别标注为 A,B,C,D,E)。
对于待排序序列 {5, 1, 9, 3, 7, 4, 8, 6, 2},我们来演示其第一趟排序过程:
low = 0, high = 8, pivot = arr[low] = 5;
A处,low = 0, high = 8, low<high,进行A循环;
B处,high的值不断递减,直至arr[high] = 2 小于pivot,跳出B循环:
1
2
3
4
5pivot
↓
5 1 9 3 7 4 8 6 2
↑ ↑
low highC处,执行low和high的元素交换:
1
2
3
4
5pivot
↓
2 1 9 3 7 4 8 6 5
↑ ↑
low highD处,low的值不断递增,直至arr[low] = 9 大于 pivot,跳出D循环:
1
2
3
4
5pivot
↓
2 1 9 3 7 4 8 6 5
↑ ↑
low highE处,执行low和high的元素交换:
1
2
3
4
5pivot
↓
2 1 5 3 7 4 8 6 9
↑ ↑
low highA处,low =2, high = 8, low < high,继续循环A;
B处,high的值不断递减,直至arr[high] = 4 小于pivot,跳出B循环:
1
2
3
4
5pivot
↓
2 1 5 3 7 4 8 6 9
↑ ↑
low highC处,执行low和high的元素交换:
1
2
3
4
5pivot
↓
2 1 4 3 7 5 8 6 9
↑ ↑
low highD处,low的值不断递增,直至arr[low] = 7 大于 pivot,跳出D循环:
1
2
3
4
5pivot
↓
2 1 4 3 7 5 8 6 9
↑ ↑
low highE处,执行low和high的元素交换:
1
2
3
4
5pivot
↓
2 1 4 3 5 7 8 6 9
↑ ↑
low highA处,low = 4, high = 5, low < high, 继续循环A:
B处,high不断递减,直至high=4 等于 low,不满足 low < high,跳出B循环:
1
2
3
4
5
6pivot
↓
2 1 4 3 5 7 8 6 9
↑
low
high因为low和high已经重合,所以在接下来的C、D、E操作中序列均未发生变化
A处,low=4, high = 4, 不满足 low < high, 跳出A循环,最后返回low=4,即为pivot所在位置;
所以第1趟排序下来之后,序列会变成 {2, 1, 4, 3, 5, 7, 8, 6, 9};然后再对子序列{2, 1, 4, 3} 和 {7, 8, 6, 9} 做同样的操作即可完成整个排序。
对于partition方法中的low和high,可以这样理解:在low左边的记录都都小于等于枢轴pivot,在high右边的记录都大于等于枢轴pivot,那么当low和high重合时,则表示已经分割完毕,重合的位置(即low的值)就是枢轴pivot的位置。
快速排序的优化
(1) 枢轴的选取方式的优化:
枢轴的选取方式有:(1) 固定位置选取;(2) 随机位置选取; (3) 三值取中法 等
固定位置选取:选取当前序列的第一个元素或者最后一个元素作为枢轴,上面的算法的枢轴选取方式即为固定位置选取。该方法不是一个好的选取方案,因为当整个序列有序时,每次分割(partition)操作只会将待排序序列减1,此时为最坏情况,算法复杂度沦为O(n^2)。然而,在待排序的序列中局部有序是相当常见的,所以固定位置选取枢轴不是一种好的选择。
随机位置选取:随机选取当前待排序序列的任意记录作为枢轴。由于采取随机,所以时间性能要强于固定位置选取。
三值取中法: 待排序序列的前(第一个位置)、中(中间位置)、后(最后一个位置)三个记录中的中间值(按大小排序)作为枢轴,比如:
1 | 9 1 7 5 2 8 6 3 4 |
由于 9 > 4 > 2; 因此将4作为此次分割(partition)操作的枢轴。
三值取中操作后,整个序列变为:
1 | 4 1 7 5 2 8 6 3 9 |
三值取中本质上就是随机位置选取,但是由于随机位置选取过程中需要用到随机种子来产生随机数,而三值取中不需要,所以三值取中要优于随机位置选取。
所以优化枢轴的选取方式时,我们选择三值取中的方式。
(2) 优化小数组时的排序方案:
当局部排序数组长度较小时,采用插入排序,而非快速排序,因为长度分割到够小后,继续分割的效率要低于直接插入排序。
(3) 略去不必要的交换
略去不必要的交换,将交换操作改为替换操作。
因为交换操作需要进行3次赋值操作,而替换操作只需要进行1次赋值操作。
Java实现
1 | // 优化的快速排序 |
演示
为了方便演示,我对上面代码中的分割操作partition方法的代码仍然进行了标注(分别标注为 A,B,C,D,E,F)。
对于待排序序列 {5, 1, 9, 3, 7, 4, 8, 6, 2},我们来演示其第一趟排序过程:
low = 0, high = 8, high-low+1=9 > MAX_LENGTH_INSERT_SORT, 所以需要进行快速排序,接下来进行分割(partition)操作;
此时待排序序列:
1
2
35 1 9 3 7 4 8 6 2
↑ ↑
low high三值取中前:
1
2
35 1 9 3 7 4 8 6 2
↑ ↑ ↑
low mid high三值取中后:
1
2
3
4
5pivot
↓
5 1 9 3 2 4 8 6 7
↑ ↑ ↑
low mid high
pivot = 5;
A处,low = 0, high = 8, low < high, 进行A循环;
B处,high的值不断递减,直至arr[high] = 4 小于pivot,跳出B循环:
1
2
35 1 9 3 2 4 8 6 7
↑ ↑
low highC处,arr[low] = arr[high],将低位的值替换成高位的值:
1
2
34 1 9 3 2 4 8 6 7
↑ ↑
low highD处,low的值不断递增,直至arr[low] = 9 大于 pivot,跳出D循环:
1
2
34 1 9 3 2 4 8 6 7
↑ ↑
low highE处,arr[high] = arr[low], 将高位的值替换成低位的值:
1
2
34 1 9 3 2 9 8 6 7
↑ ↑
low highA处,low = 2, high = 5, low < high, 进行A循环;
B处,high的值不断递减,直至arr[high] = 2 小于pivot,跳出B循环:
1
2
34 1 9 3 2 9 8 6 7
↑ ↑
low highC处,arr[low] = arr[high],将低位的值替换成高位的值:
1
2
34 1 2 3 2 9 8 6 7
↑ ↑
low highD处,low的值不断递增,直至low = 4, high = 4, low == high,不满足 low < high,跳出D循环:
1
2
3
44 1 2 3 2 9 8 6 7
↑
low
high因为low和high已经重合,所以在接下来的E操作中序列未发生变化;
A处,low=4, high = 4, 不满足 low < high, 跳出A循环;
F处, arr[low] = pivot:
1
2
3
44 1 2 3 5 9 8 6 7
↑
low
high最后返回low = 4,即为pivot所在的位置。
所以这趟排序下来之后,序列会变成 {4 1 2 3 5 9 8 6 7};然后再对子序列{4, 1, 2, 3} 和 {9, 8, 6, 7} 做同样的操作即可完成整个排序。
复杂度
时间复杂度:
时间复杂度为O(nlogn),在对快速排序进行各种细节性的优化后,快速排序的性能大大提高,在一般条件下超越了其它排序方法,故得此名。
空间复杂度:
就空间复杂度来说,主要是递归造成的栈空间的使用,最好情况,递归的深度为log2n,其空间复杂度也就为O(logn),最坏情况,需要进行n‐1递归调用,其空间复杂度为O(n),平均情况,空间复杂度也为O(logn)。