数据结构及算法基础--快速排序(Quick Sort)(一)

快速算法同样是非常重要的,用我们教授的话,这是一个里程碑的算法。虽然我也不知道为什么。

 

我们还是首先来看看快速排序的思想:

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

我们仍然要用到非常重要的递归。

我们给出流程图:

我们可以看到我们进行了suffle的过程,这一步的原因我们在后面关于性能分析的时候会给出。

partition过程便是以分割元素为界限,左边都小于分割元素,右边都大于分割元素。

然后我们在分别对左右进行排序。这便是快速排序的思想:

我们给出代码:

    private static int partition(Comparable[] a,int lo,int hi){
        int i=lo,j=hi+1;
        Comparable v=a[lo];
        while(true){
            while(less(a[++i],v))if(i==hi)break;
            while(less(v,a[--j]))if(j==lo)break;
            if(i>=j)break;
            exch(a,i,j);
        }
        exch(a,lo,j);
        return j;
    }
    
    private static void QuickSort(Comparable[] a){
        QuickSort(a,0,a.length-1);
    }
    
    private static void QuickSort(Comparable[] a,int lo,int hi){
        if(lo>=hi)return;
        int j=partition(a,lo,hi);
        QuickSort(a,lo,j-1);
        QuickSort(a,j+1,hi);
    }

 

注意,上述代码中是没有进行shuffle这一步操作的,这是不会影响排序结果的,shuffle只会影响算法的时间复杂度。

具体分析的话,细节流程如下:

 

接下来我们重点讲一下上述代码中的partition过程是怎么回事:

我们可以看到我们是将从序列开头和结尾同时向中进行比较,从开头找到比分割元素大的,从结尾找到比分割元素小的,然后交换两个元素,直到i与j相遇:

上图中在during过程中,灰色区域为还为partition的部分,此刻在i前面的元素一定比v小,在j后的元素一定比v大。

细节的流程例子如下:

最后再将分割元素与最后一个比其小的元素交换。

 

性能分析:

1)时间复杂度。

  我们考虑时间复杂度还是考虑比较和交换的次数。但是在快速排序中,交换的次数完全取决于序列本身,所以我们无法进行分析。

但是对于比较次数,我们可以进行分析:

首先我们考虑最优情况:

快速排序最优的情况就是每一次取到的元素都刚好平分整个数组(很显然我上面的不是);
        此时的时间复杂度公式则为:T[n] = 2T[n/2] + f(n);T[n/2]为平分后的子数组的时间复杂度,f[n] 为平分这个数组时所花的时间;
        下面来推算下,在最优的情况下快速排序时间复杂度的计算(用迭代法):
                                         T[n] =  2T[n/2] + n                                                                     ----------------第一次递归

                 令:n = n/2        =  2 { 2 T[n/4] + (n/2) }  + n                                               ----------------第二次递归

                                            =  2^2 T[ n/ (2^2) ] + 2n

                令:n = n/(2^2)   =  2^2  {  2 T[n/ (2^3) ]  + n/(2^2)}  +  2n                         ----------------第三次递归  

                                            =  2^3 T[  n/ (2^3) ]  + 3n

                ......................................................................................                        

                令:n = n/(  2^(m-1) )    =  2^m T[1]  + mn                                                  ----------------第m次递归(m次后结束)

               当最后平分的不能再平分时,也就是说把公式一直往下跌倒,到最后得到T[1]时,说明这个公式已经迭代完了(T[1]是常量了)。

               得到:T[n/ (2^m) ]  =  T[1]    ===>>   n = 2^m   ====>> m = logn;

               T[n] = 2^m T[1] + mn ;其中m = logn;

               T[n] = 2^(logn) T[1] + nlogn  =  n T[1] + nlogn  =  n + nlogn  ;其中n为元素个数

               又因为当n >=  2时:nlogn  >=  n  (也就是logn > 1),所以取后面的 nlogn;

               综上所述:快速排序最优的情况下时间复杂度为:O( nlogn )

这其实也是最优情况下比较的次数 nlogn。

 

最坏情况:

最差的情况就是每一次取到的元素就是数组中最小/最大的,这种情况其实就是冒泡排序了(每一次都排好一个元素的顺序)

     这种情况时间复杂度就好计算了,就是冒泡排序的时间复杂度:T[n] = n * (n-1) = n^2 + n~n^2;

     综上所述:快速排序最差的情况下时间复杂度为:O( n^2 )。比较次数则为~n^2;

 

平均情况:

平均高情况我并没有看懂书中的内容。ORZ,但是我们还是可以根据最优情况类比一下。

结论是:平均使用了2n*ln(n),注意,这里是ln,是自然对数啊。而它约等于1.39nlogn。所以平均比较次数比最优高出39%左右。

  而它的算法复杂度则也是O( nlogn ).

因为我也没动这个证明过程,所以我也不进行说明,以免进行误导,但是我给出《algorithm》中的证明过程:

 

 

2)空间复杂度:

其实这个空间复杂度不太好计算,因为有的人使用的是非就地排序,那样就不好计算了(因为有的人用到了辅助数组,所以这就要计算到你的元素个数了);我就分析下就地快速排序的空间复杂度吧;
        首先就地快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据;
     最优的情况下空间复杂度为:O(logn)  ;每一次都平分数组的情况
     最差的情况下空间复杂度为:O( n )      ;退化为冒泡排序的情况(当然,冒泡排序的空间复杂度为O(1),这里指的是思想,而不是代码的实现过程退化为冒泡排序)
其实就是每次迭代中提取v所使用的空间。
 
3)稳定性:
 
通过算法过程,很轻易理解,快速排序是不稳定的排序算法。
 
 
在分析了这么多过后,我们在来提出前文的问题,shuffle到底有什么用?
shuffle过程的存在就是为了降低碰到最坏情况的概率!!!!
 
 
 
 
 

 

posted @ 2017-10-30 00:08  霄十一郎  阅读(660)  评论(0编辑  收藏  举报