0. 原理
快速排序是一种划分交换排序,采用了分治法。分治法将原问题分解为若干规模更小,但是结构和原问题相似的子问题,递归求解子问题,最后将子问题的解组合在一起则是原问题的解。
- 划分,选择一个基准,以此基准将当前的无序序列划分为左右两个部分,并使得左边子区间内的数字全部小于基准,右边子区间内的数字全部大于基准
- 求解,递归调用对左右子区间在进行分解操作
- 当子区间只有一个数字时,递归结束,此时的序列就是有序序列
从上面的描述可以知道,关键就是划分这个操作,这里使用最简单易懂的划分方法
- 设置两个序号,一个是起始序号start,一个是结束序号end。选取无序序列的第一个记录data[start]作为基准。
- 让end从右向左扫描,直到第一个数字小于基准数字,交换起始序列和结束序列对应的数字,起始序列加1。
- 让start从左向右扫描,直到第一个数字大于基准数字,交换起始序列和结束序列对应的数字,结束序列减1.
- 重复第二第三步操作,直到起始序号等于结束序号,此时基准数字就处在了正确的位置上,并且左边的数字全部小于基准数字,右边的数字全部大于基准数字。
这里同样进行一次模拟对序列[2, 8, 1, 5, 3]进行排序:
partition 0 and 4 start
2 8 1 5 3 (exchange data: 2 <-> 1) 1 8 2 5 3
1 8 2 5 3 (exchange data: 2 <-> 8) 1 2 8 5 3
partition 0 and 4 finish and partition is 1
partition 2 and 4 start
1 2 8 5 3 (exchange data: 8 <-> 3) 1 2 3 5 8
partition 2 and 4 finish and partition is 4
partition 2 and 3 start
partition 2 and 3 finish and partition is 2
- 第一轮此递归调用从0~4进行partition,取了数字2作为基准,根据上述原理进行了两次交换后2放到最终的位置1中。
- 第二轮递归以上一轮得到的1作为划分,分为2~4, 1~1两个序列。因为start == end左边的子区间以及终止,因此对2~4这个子序列进行partition,取了数字8作为基准,交换后将8放到了最终的位置4中。
- 第三轮递归以上一轮得到的4作为划分,分为2~3,4~4两个序列。4~4这个子区间终止,继续对3~4这个序列进行partition,取了数字3作为基准,不需要交换,取得了新基准为2
- 第四轮划分为2~2,3~3,因此整个递归终止,序列变为有序
1. 实现
private void quickSort(int[] data, int start, int end) {
if (start < end) {
System.out.println("partition " + start + " and " + end + " start");
int partition = partition(data, start, end);
System.out.println("partition " + start + " and " + end + " finish and partition is " + partition);
quickSort(data, start, partition - 1);
quickSort(data, partition + 1, end);
}
}
private int partition(int[] data, int start, int end) {
int pivot = data[start];
while (start < end) {
//从右到左扫描直到发现比基准小的数字
while (start < end && data[end] > pivot) {
end--;
}
if (start < end) {
swap(data, start++, end);
}
//从左到右扫描直到发现比基准大的数字
while (start < end && data[start] < pivot) {
start++;
}
if (start < end) {
swap(data, end--, start);
}
}
data[start] = pivot;
return start;
}
具体实现可查看 GitHub/QuickSort
2. 复杂度
快速排序是一种不稳定的排序算法。最差情况下时间复杂度为O(n²),平均时间复杂度为O(nlogn)。
分析时间复杂度分为两个部分,一个是partition函数,这个函数从左往右和从右到左的扫描,知道两者相同,很明显是一个O(n)复杂度的操作,这个是固定不变的。第二个就是外部的递归了,变化的是在取得的这个基准值,如果已经是一个有序数列,则需要进行O(n)此递归才能结束。因此最坏情况下为O(n²),一般情况下是基准值取在了中间的大小,能把序列切分为两个大小偏差不大的子序列,这样的时间复杂度就是O(logn)了。
因此如果要对快速排序做优化的话,基准值的选取是一个很好的优化方向。
3. 优化
快速排序的优化方案非常的多,这里不做具体实现,因为每一个方案都可以单独写成一篇来说,只做简单介绍。
- 根据分区大小调整算法。当子序列小到一定程度的时候,递归调用快速排序就会显得没有必要,因此可以设定一个阀值,当子序列规模小岛一定程度时停止快速排序,这个时候的序列应当是一个比较有序的序列,这个时候再用插入排序可以得到接近线性的复杂度
- 随机化。由于对于已排序序列来说会得到最差时间复杂度,因此如果将选取基准进行随机化而不是取第一个数字的话,得到最坏情况的概率就会低很多,对于大多数序列都能达到期望的O(nlogn)的时间复杂度。
- 三平均分区法。选用待排数组最左边、最右边和最中间的三个元素的中间值作为基准进行快排。原理和随机化类似,能很大程度的避免最坏情况。
- 并行的快速排序。可以充分的利用多台处理器进行大量数据的排序操作。
有兴趣可以研究