数据结构和算法之排序二:快速排序

    上一篇文章我们讲完了归并排序,对于分而治之和递归思想应该都有了一定的理解,这篇文章我们将介绍道被认为是排序算法中最容易出错,但是又是最喜欢使用的一中排序方式,快速排序。对于快速排序而言我们必须抓住几个关键点就是基准值的选取,以及它在递归思想的运用过程中需要注意的事项。我们先看下面的图片了解一下快速排序的过程。

    

      我们可以看出每一次排序过程中都是选取第一个数据作为基准值,然后在前段和末端设置两个指针,指针对应的数据和基准值进行比较,如果大于基准值我们将它放在右边,如果小于基准值,我们就将它放在左边。这样经过N次排序后,我们将会得到一个有序的数组。话不多说,直接上代码,然后我们在进行详细的理解。

public static void quickSort(int arr[],int start,int end){
      int i  = start,j = end;
      //声明一个基准值
      int key;
     //条件判断我们是否继续进行递归
      if(i < j){
          key = arr[start];
          //控制i和j不出现交错现象的条件下进行循环的换值
          while(i < j){
               //从右侧开始与基准值进行比较   
               while(arr[j] >= key && i < j){
                   j--;
                }
                //如果当右侧值小于基准值时我们就与右侧i位置的值进行互换
                arr[i] = arr[j];
                while(arr[i] <= key && i < j){
                     i++;
                 }
                //如果左侧的值大于基准值时,我们就进行互换
                arr[j] = arr[i];
          }
          //基准值归位
          arr[i] = key;
         //继续进行递归排序
         quickSort(arr,start,i - 1);
         quickSort(arr,i + 1,end);
      }
}

    有些朋友在看完代码和示意图以后可能还比较蒙圈,这是什么意思呢,或者说还在疑惑某些地方为什么要这儿做呢,在这里我一一给大家诉说。

    一:外层if判断的意义:

      就像我们在代码中看到的一样,这是为了控制最后结束时是否继续再继续进行排序,总不可能无休止的进行下去吧,这个终止的条件就是当我们的开始和结束相等时就该结束了,我们可以画图理解一下,我      们在不断的缩小每一次排序的范围。

    二:为什么基准值的选取会在if条件内进行赋值:

      我们可以假设一下,去过我们将其放在外边进行赋值,当我们遇到你选取的基准值正好是最大的那个数据时,通过第一次排序以后基准值的位置是不是位于最后一个,那么这时我们进行了加一的处理,最后    再一次进行递归排序,你直接用原数组进行赋值,是不是会造成ArrayIndexOutOfBoundsException数组下标越界这个异常,如果可以,最好你亲自演示下,如果想这块进行改变也可以,那么我们就将整个判断if    改为以下代码,避免在赋值的时候出现下标越界。

if(i >= j){
  return;
}

    三:外层while条件的意义在哪里:

      试问一下我们在进行i位置和j位置的数据进行交换难道一次就结束了么,那必然是不现实的,快速排序的核心思想在于经过一次基准的选取以后,那么它左边的数据必然是都比它大,右边的数据都比它小,    那我们一定要对所有的数据都进行一次判断。所以我们在里面会嵌套两个while在右边开始的时候一直找到比基准值小的数为止进行互换,然后左边在开始,如此反复,直到i等于j为止。

    四:我们的顺序是不是必须从右边开始寻找 在值进行交换时没有用到中间值会不会造成数据覆盖:

      这个问题很关键,一定要记住了,必须从右边开始寻找,因为我们的基准值在左边,如果我们从左边开始,那么我们会把第一次覆盖的数据就会不可知,最后造成数据的覆盖。这也是可以解释为什么我们没    有使用中间值也不会造成数据覆盖的原因,试想一下,我们第一次交换值是不是必然是覆盖掉基准值,然后第一个比基准小的数据会有两个,这时候左边开始动作,当它找到比基准值大的数据的时候是不是又会    交换,覆盖掉一个停留在右边的数据,这样结合基准值归位,是不是就能恢复数据,实在不巧左边到遇到右边的游标时也没找到更大的,这时候停下来也要进行基准值归位,也是同一个道理。不理解的朋友可以    在画图理解一下。

    五:这么多的i < j是不是会显得多余:

      一点不会,因为我们第一次判断i<j是用来断定是否还需要进行递归,实在里层while循环i < j跳出以后进行的 。那么这时候我们会问,那么里层while循环的i<j是否又可以省略,答案很显然是不行,这样造成    的结果可以想象是不是会让i 和  j 一直不停地寻找下去。那么最里层的i < j 又是否可以省略,这可以思考一下,我们最后会进行基准值归位,如果省略了这个i < j 会不会造成i 和 j 最后停留的位置相差一位,真个    算法又会崩溃。

 

    如果都理解以后我们考虑一下会不会这个算法有问题,我们每次都选取第一个数据作为我们的基准值,那么如果哪个数据正好是最大值,或者是最小值,那么这样算法设计的意义何在?我们有什么办法可以去避  免,有人说我们可以取随机数,但是值得我们思考的是算法的核心很大意义上是基于时间复杂度和空间复杂度的考虑上进行的,如果每次都需要进行随机选取一个数作为基准值是否会影响我们算法的效率。在这里我  介绍一中公认的比较获取认可的办法,那就是三元取中法:

    其实这个名字或许听上去还不错,很高大上的感觉,其实那都是错觉,所谓的三元取中法就是将开始数据,结尾数据,中间值取出,选取 三个中的中间值作为基准值,这就是所谓的三元取中法。代码我就不演示了!

    时间复杂度的计算:

      1:如上所诉,时间复杂度会有最优秀的情况,就是恰好选取到了中间值

      在最优情况下,Partition每次都划分得很均匀,如果排序n个关键字,其递归树的深度就为 [log2n]+1( [x] 表示不大于 x 的最大整数),即仅需递归 log2n 次,需要时间为T(n)的话,第一次Partiation   应该是需要对整个数组扫描一遍,做n次比较。然后,获得的枢轴将数组一分为二,那么各自还需要T(n/2)的时间(注意是最好情况,所以平分两半)。于是不断地划分下去,就有了下面的不等式推断:

      

      这说明,在最优的情况下,快速排序算法的时间复杂度为O(nlogn)。

              2:最糟糕的情况,就是我上边诉说的选取到了最大值和最小值:

        然后再来看最糟糕情况下的快排,当待排序的序列为正序或逆序排列时,且每次划分只得到一个比上一次划分少一个记录的子序列,注意另一个为空。如果递归树画出来,它就是一棵斜树。此时需要执     行n‐1次递归调用,且第i次划分需要经过n‐i次关键字的比较才能找到第i个记录,也就是枢轴的位置,因此比较次数为

     

     最终其时间复杂度为O(n^2)。

     3:一般情况:    

        最后来看一下一般情况,平均的情况,设枢轴的关键字应该在第k的位置(1≤k≤n),那么:

        

    风萧萧兮易水寒,学会算法也不难。万道艰辛只为阻,破釜沉舟方可为。学习算法务必要多思考为什么,也许你遇到的问题不会就我说的那几点,也或许你没有遇到这样的问题,业精于勤荒于嬉,有事无事写一    遍,养成无聊就默写或者想想一下算法的历程,我相信没有什么可以阻挡你,就怕你认真。

 

posted @ 2017-11-30 21:11  GoNewLife  阅读(857)  评论(0编辑  收藏  举报