算法系列(三) 快速排序

来源:互联网 发布:中国农大网络教育平台 编辑:程序博客网 时间:2024/05/02 07:11

快速排序

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

       快排的好处不用多说,平均时间的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]交换下,即完成整个划分过程。

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