算法系列(三) 快速排序

快速排序

       终于到我们人见人爱,花见花开,鸟见鸟呆,车见车爆胎大笑的快速排序了!

       快排的好处不用多说,平均时间的NlogNO(1)的辅助空间,一般比其他的排序算法要快得多。

       当然也有些不足,首先,不稳定,所以多关键字排序的最后一排肯定不能用了,另外,最坏情况下则为N^2

       快速排序和归并排序一样,都属于分治的排序方法。据说当年Hoare 首次提出该算法时,惊讶于对快排的性能及代码的优美,最后还是狠狠地进行了完全的数学分析后,才发表快排论文的。

       所有的计算机科学家、程序员排列出心中的十大算法时,快速排序总是会位列其中。可想而知快排的实用性加优美性。

       和归并一样,它将数组划分成两个部分,不同在于,归并分别把两个部分进行处理好了,再进行merge。而快排则是在划分时就进行处理,再分别对两个部分进行排序。

       划分过程中,我们会重排数组(使用swap交换),得到这样的一个数组:

       1、我们选定数组中某个元素作为pivot,划分后,该元素出现在i位。

       2、a[1]-a[i-1]中的元素,都比a[i]小。

       3、a[i+1]-a[n]中的元素,都比a[i]大。

       所以,在QuickSort中,我们需要一个调用一个用于划分的partition函数,在判断该部分数组元素个数>1后,则划分,再递归处理划分后的两个部分。代码如下:

void QuickSort(int a[], int l, int r)
{
     if (l < r)
     {
            int q = Partition(a, l, r);
            QuickSort(a, l, q-1);
            QuickSort(a, q+1, r);
     }
}

       下面是划分的处理了。划分才是快排中最重要的,就像归并里的merge一样。而划分就我所知的,就有两种不同的实现方法,一个用for,从头扫到尾,一个用while,两头往中间走。

       本质上两个方法肯定没有区别,一般用的是while的方法,但是,for的代码更显优美,所以两个都介绍下吧。

       首先是流行的while。这里先不介绍随机化的快排,我们直接选取该部分的第一个元素作为pivot,也就是把a[l]从原位置中取出来,放到pivota[l]已经为空。另外还知道最高位和最低位所处的位置和 r,就可以开始比较划分过程。

       此时我们把和 r看作是已经划分好的元素的分界线,以左一定小于pivot以右一定大于pivot,而且由于C语言函数中形参的改变不影响实参,没有必要使用额外的变量(给变量起名是件很痛苦的事)。

       下面就看你的爱好是从头到脚还是反过来,我这里是从rl,将当前a[r]pivot(即初始的a[l])比较,若大于(等于看你心情),则符合要求,r--,并继续用a[r]pivot比较;否则,结束该循环,将a[r]的值(因为<pivot),放到a[l]的位置中——a[l]原本是空的,则a[r]为空。

     while (a[r] >= pivot) r--;
     a[l] = a[r];

       接着又从lr,相似的方法,比较后,或l++,或a[l]的值放到a[r]

      过程如图:

       

       如此循环下去,什么时候结束呢?lr鹊桥相会时,此时,l == rl左边的项全小于pivotr右边的项全大于pivot,我们也就找到了pivot在划分后的位置。

    另外,我刚才专门试了一下,必须给上面的两个while的循环条件加上 l < r,否则会出界。

    于是,完整的partition while版代码为:    

int Partition(int a[], int l, int r)
{
     int pivot = a[l];
     while ( l< r)
     {
         while (a[r] >= pivot && r > l) r--;
         a[l] = a[r];
         while (a[l] <= pivot && l < r) l++;
         a[r] = a[l];
     }
     a[l] = pivot;
     return l;
}


        吐槽啊,这个编辑器偶尔给我抽一下的……发火

        然后是for版了。这回我改一改风格,先上代码:

int partition(int a[], int l, int h)
{
     int flag = a[l];
     int i = l + 1, j;
     for (j = l + 1; j <= h; ++j)
         if (a[j] < flag)
         {
             swap(i, j);
             i++;
         }
     swap(l, i - 1);
     return i - 1;
}

       这个写法最开始是从《程序设计实践》里面看来了(相近,但有改动),然后在上 Coursera 的算法分析与设计时,用 WHILE 版快排连错两次以后,一看,原来是要这个写法,印象瞬间深刻了。

       相对不是那么好理解,就像我之前面试时,就遇到不熟练的问题。现在我就来解释清楚吧!

       看图说话最方便,所以,来张图:

       这里的分法和while不同,while是中间未分,这里是末尾未划分。除了用个pivot = a[l](图中是a[r])以外,还需要循环的j,和表示<=pivot 和 >pivot的分界线的i(也有叫last的)。

        从l+1开始到r,用i来表示第一个>pivot的数的位置(原图是最后一个<= pivot的位置),所以,每个数a[j]都和pivot比较,大于pivot的,自然是在i的右边,而小于pivot的话,就a[j]a[i]进行交换,再将i右移一位。

        全部走一遍之后,我们已经得到划分好的两块。不过,pivot还在a[l]处不动呢,于是又和a[i-1]交换下,即完成整个划分过程。

    本来还打算上《程序设计实践》里的快排代码的,看看没太多差别,就算了。尴尬




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值