(1.3.4.1)交换排序:快速排序

快速排序(Quick Sort)也是一种交换排序,它在排序中采取了分治策略。

快速排序每次先处理短的好,涉及到栈空间部分



快速排序的主要思想

  1. 从待排序列中选取一元素作为轴值(也叫主元)。
  2. 将序列中的剩余元素以该轴值为基准,分为左右两部分。左部分元素不大于轴值,右部分元素不小于轴值。轴值最终位于两部分的分割处。
  3. 对左右两部分重复进行这样的分割,直至无可分割。
从快速排序的算法思想可以看出,这是一递归的过程。

两个问题:

要想彻底弄懂快速排序,得解决两个问题:
  1. 如何选择轴值?(轴值不同,对排序有影响吗?)
  2. 如何分割?
问题一:轴值的选取?
轴值的重要性在于:经过分割应使序列尽量分为长度相等的两个部分,这样分治法才会起作用。若是轴值正好为序列的最值,分割后,元素统统跑到一边儿去了,分治法就无效了。算法效率无法提高。-看别人写快排的时候,注意他轴值的选取哦。

问题二:如何分割?
这涉及到具体的技巧和策略。在稍后的代码中我们一一介绍。

快速排序版本一

直接选取第一个元素或最后一个元素为轴值。这也是国内众多教材中的写法。
举个例子:
原序列   4   8   12   1   9   6
下标     0   1   2    3   4   5    轴值 pivot=4
初始化   i                    j
         i            j           i不动,移动j,while(i<j && a[j]>=pivot)j--; 
移动元素  1   8   12   1   9   6
             i        j           j不动,移动i,while(i<j && a[i]<=pivot)i++;
移动元素 1   8   12   8   9   6
            i,j                   再次移动j,i和j相遇,结束
最后一步 1   4   12   8   9   6   pivot归位
轴值4的左右两部分接着分割……

我想你一定看懂了,并且这轴值4,真的没选好,因为分割后左部分只有一个元素。

有人称上面的做法是:挖坑填数。这种描述真的很形象。简单解释下:首先取首元素为轴值,用变量pivot存储轴值,这就是挖了一个坑。此时,a[0]就是一个坑。接着移动j,把合适位置的j填入a[0],于是a[j]成了新的坑。旧的坑被填上,新的坑就出现。直到i和j相遇,这最后一个坑,被pivot填上。至此完成了第一趟分割……
看懂了,就动手敲代码吧!
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. void QuickSort(int a[], int n)  //快速排序,版本一  
  2. {  
  3.     if (a && n > 1)  
  4.     {  
  5.         int i, j, pivot;  //pivot轴值  
  6.         i=0, j = n - 1;  
  7.         pivot = a[0];   //第一个元素为轴值  
  8.         while (i < j)  
  9.         {  
  10.             while (i < j && a[j] >= pivot)  
  11.             j--;  
  12.             if (i < j)  
  13.             a[i++] = a[j];  
  14.             while (i < j && a[i] <= pivot)  
  15.             i++;  
  16.             if (i < j)  
  17.             a[j--] = a[i];  
  18.         }  
  19.         a[i] = pivot;   //把轴值放到分割处  
  20.         QuickSort(a, i);  
  21.         QuickSort(a + i + 1, n - i -1);  
  22.     }  
  23. }   

现在想想以最后一个元素为轴值的代码了,先别急着看,先动动手哦!代码如下:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. void QuickSort(int a[], int n)  
  2. {  
  3.     if (a && n > 1)  
  4.     {  
  5.         int i, j, pivot;  //pivot轴值  
  6.         i = 0, j = n - 1;  
  7.         pivot = a[j];   //最后一个元素为轴值  
  8.         while (i < j)  
  9.         {  
  10.             while (i < j && a[i] <= pivot)  
  11.                 i++;  
  12.             if (i < j)  
  13.                 a[j--] = a[i];  
  14.             while (i < j && a[j] >= pivot)  
  15.                 j--;  
  16.             if (i < j)  
  17.                 a[i++] = a[j];  
  18.         }  
  19.         a[i] = pivot;   //把轴值放到分割处  
  20.         QuickSort(a, i);  
  21.         QuickSort(a + i + 1, n - i - 1);  
  22.     }  
  23. }  

轴值选取策略

为了让轴值pivot不至于无效(不让pivot出现最值的情况)。我们可以使用一些策略来改进pivot的选取。
策略一:

随机选取序列中一元素为轴值。 

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int SelectPivot(int a[], int low, int high)  
  2. {  
  3.     int size = high - low + 1;  
  4.     srand((unsigned)time(0));  
  5.     return a[low + rand()%size];  
  6. }  
选取首尾元素不就是该策略的一种特例!

策略二:
随机选取三数,取中位数。   
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int SelectPivot(int a[], int low, int high)  
  2. {  
  3.     int size = high - low + 1;  
  4.     int p1, p2, p3;  
  5.     srand((unsigned)time(0));  
  6.     p1 = low + rand()%size;  
  7.     p2 = low + rand()%size;  
  8.     p3 = low + rand()%size;  
  9.     /* 
  10.     *  下面的交换不好理解: 
  11.     *  经过前两次的交换,p1指向最小的, 
  12.     *  所以最后两个最大的比较,把次最大的交换到 p2   
  13.     */  
  14.     if(a[p1] > a[p2])  
  15.         swap(p1, p2);  
  16.     if(a[p1] > a[p3])  
  17.         swap(p1, p3);  
  18.     if(a[p2] > a[p3])  
  19.         swap(p2, p3);  
  20.     return a[p2];  
  21. }  
它的一种特例就是,选取原序列首、 尾、中间三数,取它们的中位数。

目前看来基本常用的就这两种策略。 不过我得吐槽一句:如果原序列中的元素本身就是随机存放的,也就是说,各个元素出现在各个位置的概率一样。那么特别地选取首尾元素和随机选取又有什么区别呢?不知大家怎么看?
还得补充一句:随机选取轴值后,记得要把它和首或尾的元素交换哦。至于为什么?你懂的!

快速排序版本二

这也是《算法导论》上的版本。它的普遍做法是选取尾元素为pivot。重点是使用了一个分割函数:partition()。
伪代码与如下:
PARTITION(A, low, high)
1. pivot <- A[high]    //选取尾元素为轴值
2. i <- low-1          //把low-1赋值给i,下同
3. for j <- low to high-1    //j的变化范围[low, high-1]
4.      do if A[j] <= pivot
5.            then i <- i+1
6.            exchange A[i]<->A[j]
7. exchange A[i+1} <-> A[high]
8. return i+1;    //返回的是分割的位置
然后,对整个数组进行递归排序:
QUICKSORT(A, low, high)
1  if low < high
2  then q <- PARTITION(A, low, high)  //对元素进行分割就在这里
3  QUICKSORT(A, low, q - 1)
4  QUICKSORT(A, q + 1, high)

如果你不习惯于看伪代码,我来举个例子:(还是上面的序列)
原序列   4   8   12   1   9   6
下标  -1 0   1   2    3   4   5   轴值pivot是6
初始化 i j                        a[j]=a[0]=4<6,下一步先 i++;再swap(a[i],a[j]);随后j++;
交换     4   8   12   1   9   6
         i   j                    接着移动j
         i            j           a[j]=a[3]=1<6,下一步…
交换     4   1   12   8   9   6
             i            j       
             i                j   
交换     4   1   6    8   9   12  最后一步 swap(a[i+1], a[high]);或者是 swap(a[i+1], a[j]);
所以最后返回的是 i+1
用大白话讲讲上面的排序过程:用两个指针i,j,它们初始化为i=-1;j=0,接着让j不断地自增,遇到a[j]>pivot就与i交换,直到j指向末尾。
更直白的话:从头开始遍历原序列,遇到小于轴值的就交换到序列前面。

看懂了,就写代码了…
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int partition(int a[], int low, int high)  
  2. {  
  3.     int i, j;  
  4.     i = low - 1;  
  5.     j = low;  
  6.     while (j < high)  
  7.     {  
  8.         if (a[j] < a[high])  
  9.         swap(a[++i], a[j]);  
  10.         j++;  
  11.     }  
  12.     swap(a[++i], a[high]);    //主元归位   
  13.     return i;  //上面一步已经 ++i,所以这里不用 i+1   
  14. }  
  15. void quicksort(int a[], int low, int high)  
  16. {  
  17.     if (low < high)  //至少两个元素,才进行排序   
  18.     {  
  19.         int i = partition(a, low, high);  
  20.         quicksort(a, low, i - 1);  
  21.         quicksort(a, i + 1, high);  
  22.     }  
  23. }  
  24. void QuickSort(int a[], int n)  
  25. {  
  26.     if (a && n>1)  
  27.         quicksort(a, 0, n - 1);   
  28. }  

题外话:看到有的Api设计是这样的:QuickSort(int a[], int low, int high)。居然让用户多写一个0!如此不为用户考虑。应越简洁越好。排序只给数组名和数组大小,即可。
对上面的流程再思考:看到初始化i=-1;你不觉得奇怪吗?为什么i一定要从-1开始,仔细了解了i的作用,你会发现i本可以从0开始。这种做法的partition()方法是这样的:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int partition(int a[], int low, int high)  
  2. {  
  3.     int i, j;  
  4.     i = low;  //这里与上一种的做法不同哦!  
  5.     j = low;  
  6.     while(j < high)  
  7.     {  
  8.         if (a[j] < a[high])  
  9.         swap(a[i++], a[j]);  
  10.         j++;  
  11.     }  
  12.     swap(a[i], a[high]);    //主元归位   
  13.     return i;    
  14. }  

再思考:为什么j不能指向high?若是更改if(a[j]<a[high])为if(a[j]<=a[high),最后直接把a[high]交换到前面了,也就是说在while循环里面就完成了最后“主元归位”这一步。大家想想是不是?
此时的partition()是这样的:
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int partition(int a[], int low, int high)  
  2. {  
  3.     int i, j;  
  4.     i = low;  
  5.     j = low;  
  6.     while (j <= high)  
  7.     {  
  8.         if (a[j] <= a[high])  
  9.         swap(a[i++], a[j]);  
  10.         j++;  
  11.     }  
  12.     return i-1;   //这里为什么是i-1,得想明白?  
  13. }  

至于有时候把quicksort()和partition()写成一个函数,那是再简单不过的事情,你肯定会的。

快速排序版本三:

上面用的都是递归的方法,把递归转化非递归总是不简单的,也总让人兴奋。这个版本就是快速排序的非递归写法;
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. void QuickSort(int a[], int low, int high)  
  2. {  
  3.     if (low < high)  
  4.     {  
  5.         stack<int> s;   //使用STL中的栈   
  6.         int l,mid,h;  
  7.         mid = partition(a, low, high);  
  8.         /* 
  9.         首先存储第一次分割后的 [low, mid-1]和 [mid+1, high]  
  10.         注意:这是成对存储的,取的时候注意顺序  
  11.         */   
  12.         if (low < mid-1)  
  13.         {  
  14.             s.push(low);  
  15.             s.push(mid - 1);  
  16.         }  
  17.         if (mid + 1 < high)  
  18.         {  
  19.             s.push(mid + 1);  
  20.             s.push(high);  
  21.         }  
  22.         //只要栈不为空,说明仍有可分割的部分   
  23.         while(!s.empty())  
  24.         {  
  25.             h=s.top();  
  26.             s.pop();  
  27.             l=s.top();  
  28.             s.pop();  
  29.             mid = partition(a, l, h);  
  30.             if (l < mid - 1)  
  31.             {  
  32.                 s.push(l);  
  33.                 s.push(mid - 1);  
  34.             }  
  35.             if (mid + 1 < h)  
  36.             {  
  37.                 s.push(mid + 1);  
  38.                 s.push(h);  
  39.             }     
  40.         }  
  41.     }  
  42. }  

这个非递归的写法是很有意思的,很需要技巧。仔细想想,你能明白的。
提示:用栈保存每一个待排序子序列的首尾元素下标,下一次while循环时取出这个范围,对这段子序列进行partition操作。

小结:

快速排序号称快速搞定,时间复杂度是O(nlogn)。基本上是最优的排序方法。它的写法不外乎以上三种,大同小异。看到这里。你一定彻底了解了它。以上写法,都经过了本人测试,不知道你的测试是否和我一样?



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值