排序算法(五)-快速排序

0. 原理

快速排序是一种划分交换排序,采用了分治法。分治法将原问题分解为若干规模更小,但是结构和原问题相似的子问题,递归求解子问题,最后将子问题的解组合在一起则是原问题的解。

  1. 划分,选择一个基准,以此基准将当前的无序序列划分为左右两个部分,并使得左边子区间内的数字全部小于基准,右边子区间内的数字全部大于基准
  2. 求解,递归调用对左右子区间在进行分解操作
  3. 当子区间只有一个数字时,递归结束,此时的序列就是有序序列

从上面的描述可以知道,关键就是划分这个操作,这里使用最简单易懂的划分方法

  1. 设置两个序号,一个是起始序号start,一个是结束序号end。选取无序序列的第一个记录data[start]作为基准。
  2. 让end从右向左扫描,直到第一个数字小于基准数字,交换起始序列和结束序列对应的数字,起始序列加1。
  3. 让start从左向右扫描,直到第一个数字大于基准数字,交换起始序列和结束序列对应的数字,结束序列减1.
  4. 重复第二第三步操作,直到起始序号等于结束序号,此时基准数字就处在了正确的位置上,并且左边的数字全部小于基准数字,右边的数字全部大于基准数字。

这里同样进行一次模拟对序列[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. 优化

快速排序的优化方案非常的多,这里不做具体实现,因为每一个方案都可以单独写成一篇来说,只做简单介绍。

  1. 根据分区大小调整算法。当子序列小到一定程度的时候,递归调用快速排序就会显得没有必要,因此可以设定一个阀值,当子序列规模小岛一定程度时停止快速排序,这个时候的序列应当是一个比较有序的序列,这个时候再用插入排序可以得到接近线性的复杂度
  2. 随机化。由于对于已排序序列来说会得到最差时间复杂度,因此如果将选取基准进行随机化而不是取第一个数字的话,得到最坏情况的概率就会低很多,对于大多数序列都能达到期望的O(nlogn)的时间复杂度。
  3. 三平均分区法。选用待排数组最左边、最右边和最中间的三个元素的中间值作为基准进行快排。原理和随机化类似,能很大程度的避免最坏情况。
  4. 并行的快速排序。可以充分的利用多台处理器进行大量数据的排序操作。
    有兴趣可以研究
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值