快速排序在實際應用中會面對大量具有重復元素的數組。例如加入一個子數組全部為重復元素,則對於此數組排序就可以停止,但快排算法依然將其切分為更小的數組。這種情況下快排的性能尚可,但存在着巨大的改進潛力。(從O(nlgn)提升到O(n))
一個簡單的想法就是將數組分為三部分:小於當前切分元素的部分,等於當前切分元素的部分,大於當前切分元素的部分。
E.W.Dijlstra(對,就是Dijkstra最短路徑算法的發明者)曾經提出一個與之相關的荷蘭國旗問題(一個數組中有分別代表紅白藍三個顏色的三個主鍵值,將三個主鍵值排序,就得到了荷蘭國旗的顏色排列)。
他提出的算法是: 對於每次切分:從數組的左邊到右邊遍歷一次,維護三個指針,其中lt指針使得元素(arr[0]-arr[lt-1])的值均小於切分元素;gt指針使得元素(arr[gt+1]-arr[N-1])的值均大於切分元素;i指針使得元素(arr[lt]-arr[i-1])的值均等於切分元素,(arr[i]-arr[gt])的元素還沒被掃描,切分算法執行到i>gt為止。每次切分之后,位於gt指針和lt指針之間的元素的位置都已經被排定,不需要再去處理了。之后將(lo,lt-1),(gt+1,hi)分別作為處理左子數組和右子數組的遞歸函數的參數傳入,遞歸結束,整個算法也就結束。
三向切分的示意圖:
C++代碼如下:
1 #include <iostream> 2 #include <cstdio> 3 using namespace std; 4 #define maxn 10000 5 int a[maxn]; 6 7 void exchange( int i,int j ) 8 { 9 int tmp=a[i]; 10 a[i]=a[j]; 11 a[j]=tmp; 12 } 13 14 15 void qsort3way ( int lo,int hi ) 16 { 17 if( lo>=hi ) return; //單個元素或者沒有元素的情況 18 int lt=lo; 19 int i=lo+1; //第一個元素是切分元素,所以指針i可以從lo+1開始 20 int gt=hi; 21 int v=a[lo]; 22 while( i<=gt ) 23 { 24 if( a[i]<v ) //小於切分元素的放在lt左邊,因此指針lt和指針i整體右移 25 exchange( lt++,i++ ); 26 else if ( a[i]>v ) //大於切分元素的放在gt右邊,因此指針gt需要左移 27 exchange( i,gt-- ); 28 else 29 i++; 30 } 31 //lt-gt的元素已經排定,只需對it左邊和gt右邊的元素進行遞歸求解 32 qsort3way( lo,lt-1 ); 33 qsort3way( gt+1,hi ); 34 } 35 36 37 int main() 38 { 39 int n; 40 cin>>n; 41 for( int i=0; i<n; i++ ) 42 cin>>a[i]; 43 qsort3way( 0,n-1 ); 44 for( int i=0; i<n; i++ ) 45 cout<<a[i]; 46 cout<<endl; 47 return 0; 48 }
下面是《算法(第四版)》上對算法切分軌跡的一個示例說明:
對於包含大量重復元素的數組,這個算法將排序時間從線性對數級降到了線性級別。