得知道的快速排序

1.概述

在找实习的多次面试中,在数据结构与算法的面试题中,手写快排就跟计算机网络的三次握手一般是被问烂了也肯定得会的知识点(千万别以为被问烂了就不会被问到,在面腾讯提前批一面的时候最后就用腾讯文档手写了快排,只有简单的玩熟练了才能有机会去展示那些“深奥”的)。快排的思想简单,实现的代码却各异,且可能稍有不同就满盘皆崩。

2.快排的思想

快排采用的是分治的思想。快速排序(Quick Sort)是对冒泡排序的一种改进,基本思想是选取一个记录作为枢轴,经过一趟排序(更应该说是划分Partition)将整段序列分为两个部分,其中一部分的值都小于枢轴,另一部分都大于枢轴。枢轴位于二者中间不在变动, 然后继续对这两部分继续进行排序,递归进行直到划分到最小(子集合只有一个数据)才不再递归,最终使得整个序列达到有序。

以数据3 44 38 5 47 15 36 26 27 2 46 4 19 50 48为例,首先会以3为枢轴,只有2比3小,其他数据都比3大,第一次划分后数据为2 3 38 5 47 15 36 26 27 46 4 19 50 48。左边部分只有一个2,不用再排序。右边数据会选第一个数据38,然后对右边部分再划分,5 15 36 26 27 4 19在38左边,47 46 50 48在38右边。数据变为2 3 19 5 15 36 26 27 4 38 46 47 44 50 48

更详细更生动的快速排序演示参看:visualgo快速排序演示

3.快排的实现

先直接上代码,qSort为快速排序的方法,实现了排序划分和递归对左右两部分进行划分排序。Pratition方法则是执行划分(把小的移到左边,大的移到右边,枢轴位于两部分中间)的方法。

 public static void qSort(int[] data, int low, int high){
     // 小数组和大数组的分界点的下标
     int pivot;
 ​
     // ⑤为什么要判断low<high?当low<high不满足时,即low>=high时不用操作,为递归结束条件
     if (low < high){
         // 从data中选取一个数作为分界点,通过partition函数放置在合适位置pivot
         // pivot左边都是比pivot的值小的数,右边都是比pivot的值大的数
         pivot = Partition(data,low,high);
 ​
         // 对左右两边分别做快速排序
         qSort(data,low,pivot-1);
         qSort(data,pivot+1,high);
     }
 }
 ​
 /**
 * ④
 * Pratition的中文意思为划分
 * 这个方法做的操作是把所有比枢轴点的值小的数据移到枢轴点左边,大的移到右边。
 * 即把数据划分成三部分:左边小的、中间枢轴、右边大的
 */
 private static int Partition(int[] data, int low, int high) {
 ​
     //取每次操作的数组的第一个数为基准点
     int pivotValue = data[low];
 ​
     //从low和high两边使用双指针向中间靠拢
     //①直至low=high(因为不管左移还是右移都会判断是否到了low=high(代码中体现为判断low<high是否成立),所以不会出现low>high)
     while (low < high){
         // 如果右边数据比分界点数据大则直接跳过
         //直到在右边找到不大于等于(即小于)基准点数据值或者到了low=high
         //②注意一定要先从后往前?为什么了?因为为了让基准点能够不断移动到位置,如果先从左边到右边会无法对基准点移动
         while(low < high && data[high] >= pivotValue){
             high--;
         }
         //第一次交换,把右边不合理的数据移到左边去
         //③当然,你说左边跟它交换的数据本来就该待在左边,现在移到了右边怎么办?
         //别急,还会换回来的,先插个眼
         swap(data,low,high);
 ​
         // 如果左边数据比分界点数据小则直接跳过
         // 直到在左边找到不小于等于(即大于)基准点数据值或者到了low=high
         while(low < high && data[low] <= pivotValue){
             low++;
         }
         //交换右边“不合理”的数据到左边
         //③这里的i为第一次交换时换过去的数据,这里又换回来了,解决了。
         swap(data,low,high);
     }
     return low;
 }

一个“简单”的快排却暗藏杀机。看算法思想:我明白了;敲实现代码:我会了;动手实现:什么鬼?怎么这么多坑。下面我将介绍我在实现以上代码踩的几个坑,具体实现有详细代码,可以回过头参考上面代码。

  1. 为什么在Partition方法中的第一个while的判断条件是low<high?

  2. 在Partition的循环体中,一定要先high--,如果先low++不能完全排序。

  3. 在Partition的循环体中,两个swap怎么保证左右数据在该待的地方且枢轴点的数据移到了左右两部分的中间

  4. 为什么叫做Partition

4 时间复杂度

写完代码不一定就完了,不管面试官问不问,告诉面试官时间复杂度为多少都是必要的。

快排是递归实现的,所以时间复杂度也要按递归算法的特殊方式计算。时间复杂度的计算方法可以参考:得知道的时间复杂度计算

在qSort函数中,Portition操作时间复杂度为O(n), qSort(data,low,pivot-1)和qSort(data,pivot+1,high)的时间复杂度都是T(n/2),所以时间复杂度应该是T(n)=2*T(n)+O(n)。根据上文中递归时间复杂度计算公式得到快速排序的时间复杂度为O(nlog)

5.快排优化

快排算法终于实现了,怕就怕在面试官看你这实现速度,这注释,这思路,忍不住让你再进步进步、再优化优化。不用慌,后路已经留足,优化可以从枢轴选择、减少交换、优化小数组情况下的排序和优化递归操作四个方面进行快排算法的优化。

5.1 优化选取枢轴

枢轴的选取非常关键,假如像上面代码中每次选取第一个数据为枢轴点,枢轴点的值过大或过小都会影响性能。所以我们需要尽量选择值在中间的数为枢轴点

随机法:随机选取一个数?这是个概率问题啊,很有可能选择的不够优秀,且要针对不同数组实现一个通用的随机选择算法本身也会带来时间上的开销。所以还是不考虑了吧。

三数取中:选择第一个数、中间数、最后一个数这三个数,取三者的中间值的那个数。从概率来说,取三个数均为最小或最大数的可能性是微乎其微的。

实现左端、右端和中间三个数的代码,在Partition方法开头增加如下代码

 int mid = low +(high-low)/2;
 if (data[low]>data[high]){
     swap(data,low,high);
 }
 if (data[mid]>data[high]){
     swap(data,mid,high);
 }
 if (data[mid]>data[low]){
     swap(data,mid,low);
 }

5.2 优化不必要的交换

对Partition方法进行改动,如下:

     private static int Partition(int[] data, int low, int high) {
         int mid = low +(high-low)/2;
 ​
         //取每次操作的数组的第一个数为基准点
         int pivotValue = data[low];
 ​
         //从low和high两边使用双指针向中间靠拢
         // 直至low=high(因为不管左移还是右移都会判断是否到了low=high(判断low<high是否成立),所以不会出现low>high)
         while (low < high){
             // 如果右边数据比分界点数据大则直接跳过
             //直到在右边找到不大于等于(即小于)基准点数据值或者到了low=high
             while(low < high && data[high] >= pivotValue){
                 high--;
             }
             
             //swap(data,low,high);
             data[low] = data[high];
 ​
             // 如果左边数据比分界点数据小则直接跳过
             // 直到在左边找到不小于等于(即大于)基准点数据值或者到了low=high
             while(low < high && data[low] <= pivotValue){
                 low++;
             }
             
             //swap(data,low,high);
             data[high]=data[low];
         }
         //把枢轴点的值移到左右两部分的中间位置
         data[low] = pivotValue;
         return low;
     }

将枢轴点的值保存在pivotValue中,然后再之前的swap操作改为相互替换的操作,最终但low与high会合,即找到了枢轴的位置时,在把pivotValue的值赋值回枢轴点。

5.3 优化小数组时的排序方案

快排算法更适合数据量较大的数组排序,但如果是小数组时,插入排序会更加高效。**所以在Partition方法中增加一个判断,但high-low(数据量)不大于某个常数时(有资料认为7比较合适,也有认为50更合理,实际应用可适当调整),就用直接插入排序,其他情况仍然使用原来的算法。

5.4 优化递归操作

众所周知,递归对性能是有一定影响的,qSort函数在最后分别对左右两部分进行递归操作。我们可以对qSort实施尾递归优化,代码如下:

    public static void qSort(int[] data, int low, int high){
         // 小数组和大数组的分界点的下标
         int pivot;
 ​
         while (low < high){
             pivot = Partition(data,low,high);
             // 对左边做快速排序
             qSort(data,low,pivot-1);
             //qSort(data,pivot+1,high);
             //使low=pivot+1,while循环后会做Partition(data,pivot+1,high),省去了一趟递归换成迭代
             low=pivot+1;
         }
     }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值