编程珠玑——快速排序总结

快速排序

快速排序是二十世纪最伟大的10大算法之一。如何写好一个快速排序通常不是一件容易的事,同时面试中或者实际应用中也经常要用到快速排序。

编程珠玑第11章专门介绍了快速排序,涉及到了基本的算法思想实现和一些优化,这里进行简单的总结以便以后快速的回忆。


基本思想

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

我们直观上可以想到式子: T(n)=2T(n/2)+On ,从而得到时间复杂度为 O(nlgn) ,好于 O(n2) 的时间复杂度。

实现

至于具体怎么分割数组,《编程珠玑》首先提到了一种方法,一趟快速排序流程如下:

  • 设置两个变量l、y,[l,r]表示要排序的数组范围,排序开始的时候:l=0,r=N-1;
  • 以范围内的第一个数组元素作为关键比较数据,赋值给pivot,即pivot=A[l];
  • 设index=l, 从i=l+1开始搜索,即由左开始向右搜索(i++),若找到一个小于key的值A[i],将A[i]和A[++index]互换;
  • i遍历到范围[l,r]的最右边r之后,再将pivot元素A[l]与A[index]互换(index此时指向最后一个小于pivot的元素的位置)

整个过程也是比较教科书式的,为了直观一些,举个例子int A[]={3,4,5,1,2,6};那么整个排序过程如下

  • 开始,l=0,r=5,pivot = A[0]=3;第一次分割之后数组如下:
    这里写图片描述

  • 分割之后,分为两块左边,l=0,r=1; 右边l=3,r=5. 两边进行第二次分割之后数组如下:
    这里写图片描述
    最后再将l=4,r=5的数组快进行partition排序,即完成了整个数组的排序。

实现代码如下:

void quicksort(int a[],int l,int r)
{
    if(l>=r) return;
    int pivot = a[l];
    int index = l;
    for(int i=l+1;i<=r;i++)
    {
        if(a[i]<pivot)
            swap(a[++index],a[i]);
    }
    swap(a[l],a[index]);
    quicksort(a,l,index-1);
    quicksort(a,index+1,r);
}

特殊情况

上述讲了一般情况下的快排思想,平均时间复杂度为 Onlgn 。但是一些特殊情况的输入下仍然会导致时间复杂度为 On2

全是重复元素

比如数组 a={3,3,3,3,3,3}.此时可以看到,第一次我们将数组分成长度1和5的两块;之后将长度为5的数组块又分成长度1和长度4的数组块….没有达到分治的效果。

此时时间复杂度为 Tn=Tn1+On ,显然就是 On2 的复杂度了。针对这样的情况,我们可以稍微改变快排的思路,每一趟的快排前两步和上述的快排一样

  1. 和上述一样;
  2. 和上述一样;
  3. 设i=l+1,j=r;
  4. 从i开始向后搜索,即由左开始向右搜索(i++),找到第一个大于等于key的值A[i];从j开始向前搜索,即由右开始向左搜索(j–),找到第一个小于等于key的值A[i];若i>j,终止此步,否则将A[j]和A[i]互换,重复此步骤直到终止;
  5. 交换元素A[l]与元素A[j]。

这样所有元素都相同的情况也能基本实现每次partition二分,此时时间复杂都也变成了 ONlgN

实现代码如下:

void quicksort(int a[],int l,int r)
{
    if(l>=r) return;

    int pivot = a[l];
    int i=l;
    int j=r+1;

    while(1)
    {
        do{i++;}while((i<=r)&&a[i]<pivot);
        while(a[--j]>pivot);
        if(i>j) break;
        swap(a[i],a[j]);
    }
    swap(a[l],a[j]);
    quicksort(a,l,j-1);
    quicksort(a,j+1,r);
}

输入已经排好序

面对这种情况,上述已经优化的快排代码又会出现 ON2 的时间复杂度。因为它会首先围绕最小的元素划分,将数组大小分为1和N-1,然后是围绕第二小的元素划分,依此类推,还是需要 ON2 的时间。

解决这种情况的方法很简单就是随机选择划分元素,通过把A[l]与A[l,r]中的一个随机项交换来实现这一点。只需要加两行代码:

int num=rand()%(r-l+1) + l;
swap(a[l],a[num]);

这样期望的运行时间就为 O(NlgN) 啦。

优化

《编程珠玑》课后练习11.11提出了一种快排的小优化点,如下图:
这里写图片描述
之前我们都没有单独考虑过重复的元素,若是将数组中和比较元素t相同的所有元素都放到中间位置,那么之后进行下一趟快排的时候就会多过滤掉一些元素,起到了优化的效果。实现代码如下:

void quciksort(int a[],int l,int r)
{
    if(l>=r) return;
    int m=n=r+1;
    int pivot = a[l];
    for(int i=r;i>=l;i--)
    {
       while(a[i]<pivot) i--;
       if(a[i]==pivot)
          swap(a[--m],a[i]);
       else{//a[i]>pivot
          swap(a[--n],a[i]);
          swap(a[--m],a[i]);
       }
    }
   quciksort(a,l,m-1);
   quciksort(a,n,r);
} 
本书是计算机科学方面的经典名著。书的内容围绕程序设计人员面对的一系列实际问题展开。作者Jon Bentley 以其独有的洞察力和创造力,引导读者理解这些问题并学会解决方法,而这些正是程序员实际编程生涯中至关重要的。本书的特色是通过一些精心设计的有趣而又颇具指导意义的程序,对实用程序设计技巧及基本设计原则进行了透彻而睿智的描述,为复杂的编程问题提供了清晰而完备的解决思路。本书对各个层次的程序员都具有很高的阅读价值。. 多年以来,当程序员们推选出最心爱的计算机图书时,《编程珠玑》总是位列前列。正如自然界里珍珠出自细沙对牡蛎的磨砺,计算机科学大师Jon Bentley以其独有的洞察力和创造力,从磨砺程序员的实际问题中凝结出一篇篇不朽的编程珠玑”,成为世界计算机界名刊《ACM通讯》历史上最受欢迎的专栏,最终结集为两部不朽的计算机科学经典名著,影响和激励着一代又一代程序员和计算机科学工作者。本书为第一卷,主要讨论计算机科学中最本质的问题:如何正确选择和高效地实现算法。.. 在书中,作者选取许多具有典型意义的复杂编程算法问题,生动描绘了历史上众大师们在探索解决方案中发生的轶事、走过的弯路和不断精益求精的历程,引导读者像真正的程序员和软件工程师那样富于创新性地思考,并透彻阐述和总结了许多独特而精妙的设计原则、思考和解决问题的方法以及实用程序设计技巧。解决方案的代码均以C/C++语言编写,不仅有趣,而且有很大的实战示范意义。每章后所附习题极具挑战性和启发性,书末给出了简洁的解答。...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值