排序算法(三)--关于快速排序Partition的思考

上一章我们讲解了快速排序,其中快速排序一趟划分的算法Partition.Partition可不只用在快速排序中,还可Selection algorithm(在无序数组中寻找第K大的值)中.甚至有可能正是这种通过一趟扫描来进行分类的思想激发Edsger Dijkstra想出了Three-way Partitioning,高效的解决了Dutch national flag problem问题.接下来我们一起探索Partition算法.

Partition实现

快速排序中用到的partion算法思想很简单,首先从无序数组中选出枢轴点pivot,然后通过一趟扫描,以pivot为分界线将数组中的元素分为两部分:使得左边的数小于等于枢轴,右边的数大于等于枢轴(左右部分可能为空),最后返回枢轴在新数组中的位置.

Partition的一个直观简单实现如下(取数组第一个元素为pivot):

[cpp]  view plain  copy
  1. <pre name="code" class="cpp">// 直观的设置一个指针的partition算法  
  2. int partition(int array[], int nStart, int nEnd)  
  3. {  
  4.     int pivot = array[nStart];  
  5.     int nPos = nStart;  
  6.     // 从第二个数开始和基准值比较  
  7.     for (int i = nStart+1; i<=nEnd; i++)  
  8.     {  
  9.         if (array[i]<=pivot)  
  10.         {  
  11.             nPos++;  
  12.             Swap(array[i], array[nPos]);  
  13.         }  
  14.     }  
  15.     Swap(array[nPos], array[nStart]);  
  16.     return nPos;  
  17. }  


 
 这种实现思路比较直观,但是其实并不高效.从直观上来说,每个小于piovt的值基本上都需要交换一次,而大于piovt的值可能要交换多次. 
 

如果我们考虑用Two Pointers的思想,保持头尾两个指针向中间扫描,在头部找到大于piovt的值,同时在尾部找到小于piovt的值,把他们做一次交换。就可以一次把这两个数组放到最终的位置.

[cpp]  view plain  copy
  1. template <class T>  
  2. int Partition(T array[], int nStart, int nEnd)  
  3. {  
  4.     int i = nStart;  
  5.     int j = nEnd;  
  6.     while (i < j)  
  7.     {  
  8.         while (i<j)  
  9.         {  
  10.             // 右侧扫描  
  11.             if (array[j] > array[i])  
  12.             {  
  13.                 j--;  
  14.             }else  
  15.             {  
  16.                 T temp = array[j];  
  17.                 array[j] = array[i];  
  18.                 array[i] = temp;  
  19.                 i++;  
  20.                 break;  
  21.             }  
  22.         }  
  23.         while (i<j)  
  24.         {  
  25.             // 左侧扫描  
  26.             if (array[i] < array[j])  
  27.             {  
  28.                 i++;  
  29.             }else  
  30.             {  
  31.                 T temp = array[j];  
  32.                 array[j] = array[i];  
  33.                 array[i] = temp;  
  34.                 j--;  
  35.                 break;  
  36.             }  
  37.         }  
  38.   
  39.     }  
  40.     return i;  
  41. }  
Partition应用

除了用在快速排序,partition还可以用O(n)的平均时间复杂度从无序数组中寻找第k大的值.和快排一样,这里也用到了分而治之的思想.思路是,首先对数组进行一次Partition,得到坐标nPos:

如果nPos+1 == k,返回array[nPos];

如果nPos+1 > k,对数组左半部分继续进行Partition;

如果nPos+1 < k, 对数组右半部分继续进行Partition.

[cpp]  view plain  copy
  1. // 利用Partition实现复杂度为O(n)的寻找数组中第K大的数  
  2. int GetArrayMaxK(int array[], int nStart, int nEnd, int k)  
  3. {  
  4.     if (k <= 0)  
  5.     {  
  6.         throw;  
  7.     }  
  8.     int nPos = -1;  
  9.     while (true)  
  10.     {  
  11.         nPos = Partition(array, nStart, nEnd);  
  12.         if ((nPos+1) == k)  
  13.         {  
  14.             return array[nPos];  
  15.         }else if ((nPos+1) > k)  
  16.         {  
  17.             nEnd = nPos - 1;  
  18.         }else  
  19.         {  
  20.             nStart = nPos + 1;  
  21.         }  
  22.     }  
  23. }  
Partition进阶

考虑如下一个问题:给定红,白,蓝三种颜色的小球若干,将其排成一列,使相同颜色的小球相邻,且三种颜色的先后顺序为红,白,蓝.这就是经典的Dutch national flag problem.

我们可以针对红,蓝,白三种颜色的球分别计数,然后根据计数结果重新放球.我们可以将问题进一步抽象,也就是说将一个数组按照某个target分为三部分,左边部分小于target,中间部分等于target,右边部分大于target.这样就不能简单计数来确定排序后的结果,这时候,可以用另一种partition算法:three-way-partition.它的思路稍微复杂一点,同三个指针将数组分为四个部分,通过一次扫描将数组分为<,=,>三部分.

[cpp]  view plain  copy
  1. // 三个指针扫描  
  2. void three_way_partition(int array[], int nLen, int target)  
  3. {  
  4.     int next_less_pos = 0;  
  5.     int next_bigger_pos = nLen - 1;  
  6.     int next_scan_pos = 0;  
  7.     while (next_scan_pos <= next_bigger_pos)  
  8.     {  
  9.         if (array[next_scan_pos] > target)  
  10.         {  
  11.             Swap(array[next_scan_pos], array[next_bigger_pos--]);  
  12.         }else if (array[next_scan_pos] < target)  
  13.         {  
  14.             Swap(array[next_scan_pos++],array[next_less_pos++]);  
  15.         }else  
  16.         {  
  17.             next_scan_pos++;  
  18.         }  
  19.     }  
  20.   
  21. }  
这里主要的思想就是在一遍扫描中,通过交换不通位置的数字,使得数组可以维持一定的顺序.和前面快排用到的partition思想一致.区别在于快排通过pivot将数组分为两部分.而three-way-partition将数组分为<,=,>三部分.

猜你喜欢

转载自blog.csdn.net/u012530451/article/details/78839190