快速排序的思想大體來說比較簡單,就是從數組中挑選一個數字當做樞紐,然后將比樞紐大的和比樞紐小的分別放在樞紐的兩邊,再遞歸地對兩邊進行操作,從而進行分治解決問題。平均情況下快速排序是復雜度為O(nlogn)O(nlogn)O(nlogn),可是有時候復雜度會退化為O(n2)O(n^2)O(n2),這與我們如何選擇樞紐以及如何將數組進行劃分有關。
總共有兩種情況下復雜度會退化:
-
數組大體有序:這個時候如果我們樞紐選擇的不夠好,那么數組的一邊將會比較大,一邊將會比較小,最嚴重的時候每次只能將規模減一,則復雜度將會變成T(n)=T(n?1)+nT(n)=T(n-1)+nT(n)=T(n?1)+n,即T(n)=n2T(n)=n^2T(n)=n2。解決這個問題的方法就是合理的選擇樞紐,例如選擇數組頭部、尾部、中間三個數字的平均值,這種方法就能有有效解決這個問題。我們一般將樞紐放在數組的頭部(只需要交換選擇的樞紐和原本位于樞紐頭部的元素即可),這樣做可以方便我們進行劃分。
-
數組中有很多一模一樣的數字:這樣同樣會產生每次只能將規模減小很少的情況,最壞的時候復雜度也將退化為O(n2)O(n^2)O(n2)。這種問題的產生我們無法通過有效的選擇樞紐解決,只能夠通過劃分的時候使得即使數組中的元素都等于樞紐我們仍舊能夠將數組大概分成相等的兩部分。
常見的有以下劃分方法:
-
從一邊進行劃分
? 大概的思想就是把第一個元素當做樞紐,然后使用一個指針保存分界點,指針的左邊是小于樞紐的元素,指針的右邊是大于等于樞紐的元素(必須有一邊支持等于,否則劃分將會卡住)。
? 將指針初始化在數組頭部,遍歷后面的元素,如果比樞紐小就將指針向后移動一位,然后將該位置的元素和遍歷到的元素交換。否則就繼續向后遍歷。這樣做可以成功的原因是任何時刻指針后面的元素都是大于等于樞紐的元素,通過交換就將小于樞紐的元素放在了指針之前,從而完成劃分。
實現代碼
void QuickSort(T* a,int l,int r) {if(r-l<2) return;//從一邊劃分int index=l;T x=a[l];for(int i=l+1;i<r;++i){if(a[i]<x)swap(a[++index],a[i]);}swap(a[index],a[l]);QuickSort(a,l,index); QuickSort(a,index+1,r); }
雖然這種方法實現起來比較簡單,但是他不能夠解決出現大量重復元素復雜度提升的問題。
- 從兩邊進行劃分
-
空穴法
我們先將樞紐元素取出數組,然后用兩個指針分別指向數組頭部和數組尾部,先從尾部找比樞紐元素小的元素,找到以后放在數組頭部因為將樞紐元素取出形成的空穴中,此時指向數組尾部的指針所指向的元素被取走形成空穴,再從頭部找比樞紐大的元素,找到以后再放在尾部形成的空穴中。如此反復,直到兩個指針相遇,然后再將樞紐放在最后的這個空穴中,完成劃分。
實現代碼
void QuickSort(T* a,int l,int r){if(r-l<2) return;//空穴法int i=l,j=r-1;T x=a[l];while(i<j){while(i<j && a[j]>x) --j; a[i]=a[j];while(i<j && a[i]<=x) ++i;a[j]=a[i];}a[i]=x;QuickSort(a,l,i); QuickSort(a,i+1,r);}
這中方法我們同樣必須在一邊允許等號,因此也可能出現復雜度退化的問題
-
直接交換
當然我們也可以直接進行交換而不使用空穴,一種簡單的實現方法
void QuickSort(T* a,int l,int r) {if(r-l<2) return;int i=l+1,j=r-1;T x=a[l];while(i<=j){while(i<r && a[i]<=x) ++i;while(j>l && a[j]>x) --j;if(i<j) swap(a[i],a[j]);}swap(a[l],a[j]);QuickSort(a,l,j);QuickSort(a,i,r); }
然而這種方法依舊不能夠解決問題(不會采用),因此我們需要進行一些變形
實現代碼
void QuickSort(T* a,int l,int r) {if(r-l<2) return;int i=l-1,j=r;T pivot = a[l];while(i<j){do ++i; while(a[i] < pivot);do --j; while(a[j] > pivot);if(i < j) swap(a[i],a[j]);}QuickSort(a,l,j+1); QuickSort(a,j+1,r); }
為什么這樣做就可以解決重復元素的問題呢?這里和上面方法最大的不同就在于我們在劃分的時候沒有使用等號。這樣的話如果遇到和樞紐相等的元素的時候我們就移動然后越過這個位置。使用dododo while;while;while;結構就是為了能夠跨越和樞紐相等的元素。如果整個數組都是相等的話雖然我們多進行一些交換,但是有效地將數組劃分成了差不多相等的兩部分。
對于代碼的理解,很重要的一點就是將i=l?1i=l-1i=l?1。剛開始我覺得這一點沒有很重要所以自己將其改為了i=li=li=l,然后最后將樞紐元素放在中間。但是在測試的時候我發現對于有些數據會出錯。仔細推敲數據以后發現,i=l?1i=l-1i=l?1的意義不僅僅在于第一個do()whiledo() whiledo()while結構可以將樞紐元素計算進去,更重要的是一個哨兵的作用。因為后面我們進行移動指針的時候并沒有判斷指針是否越界。對于右邊的指針無論如何一定會停下來,因為它的左邊至少還有一個和樞紐元素相等的元素(樞紐元素本身),但是如果左邊我們剛開始的時候跳過了樞紐元素,那么如果在數組末尾的話就會越界。只有讓左邊剛開始為i=l?1i=l-1i=l?1,那么指針至少會停在樞紐元素的位置。如果發生了交換的話,那么指針也一定會停在交換時右邊指針位置的前面。還有一點就是第12行區間分割為[l,j+1) [j+1,r)
而不是[l,j),[j,r)
,因為后面這種做法有可能導致左邊[l,j)
的區間長度為0,這樣將會導致棧溢出。產生這種現象的原因主要是樞紐元素選擇的不恰當,對于選擇第一個元素作為樞紐來講,j
一定是小于r-1
的(因為第一次肯定會卡住),所以不用擔心j+1
等于r
。如果樞紐選擇的比較恰當,就不會出現這種問題。
? 上面的這種做法沒有將樞紐元素放在中間,但是因為他不害怕重復元素,所以不用擔心問題的規模不減小而產生棧溢出。
通過劃分解決了上面的問題以后我們就可以得到一個復雜度挺優秀的快速排序了。
實現代碼
#include <iostream>using namespace std;typedef double T;T* CreatList(int &n)
{printf("n="); scanf("%d",&n);T* ret = new T[n];for(int i=0;i<n;++i){cin>>ret[i];}return ret;
}void Init(T* a,int l,int r)
{int mid=(l+r)>>1;if(a[mid] < a[l]) swap(a[mid],a[l]);if(a[mid] < a[r-1]) swap(a[mid],a[r-1]);if(a[l] > a[r-1]) swap(a[l],a[r-1]);return;
}void QuickSort(T* a,int l,int r)
{if(r-l<2) return;Init(a,l,r);//將首部、尾部、中間三個數中的中值放在開頭int i=l-1,j=r;T pivot = a[l];while(i<j){do ++i; while(a[i] < pivot);do --j; while(a[j] > pivot);if(i < j) swap(a[i],a[j]);}QuickSort(a,l,j+1); QuickSort(a,j+1,r);
}void Show(T* a,int n)
{for(int i=0;i<n;++i){cout<<a[i]<<" ";}cout<<endl;
}int main()
{int n;T* a=CreatList(n);QuickSort(a,0,n);cout<<"經過排序之后:"<<endl;Show(a,n);delete[] a;return 0;
}
為了驗證是否我們的確對算法的效率進行了提高,我編寫了測試程序:(單位為SSS,環境為Ubuntu18.04Ubuntu18.04Ubuntu18.04)
數據規模 | 1e5亂序 | 1e6亂序 | 1e7亂序 | 5e4重復 | 5e4有序 |
---|---|---|---|---|---|
一側劃分+取中值 | 0.021184 | 0.255226 | 2.913669 | 2.964905 | 0.005573 |
空穴法劃分+取中值 | 0.014865 | 0.172306 | 1.980930 | 3.135060 | 0.002652 |
兩側直接劃分+取中值 | 0.017033 | 0.195318 | 2.236171 | 0.004814 | 0.002670 |
兩側直接劃分 | 0.016239 | 0.189592 | 2.178169 | 0.004700 | 2.622307 |
? 為了減少運行時操作系統的影響,每個數據規模運行我都運行十次然后取平均值。
? 雖然仍舊可能還有數據本身的影響,但是我們也能夠大概看出來一個大體的變化規律。當數據為亂序的時候空穴法是比較優秀的,但是當出現重復元素時,兩側直接劃分的方法碾壓前面兩種方法。當數據大體是有序的時候如果我們選取樞紐直接選擇第一個其時間復雜度也是可怕的。
? 因此綜合考慮我們采用第三種方法是比較好的。
測試程序代碼
#include <iostream>
#include <ctime>
#include <cstdio>
#include <fstream>
#include <cstdlib>using namespace std;typedef double T;
typedef void (*FP)(T*,int,int); //定義函數指針數組類型void CreatData()
{int n=10;FILE* file=fopen("TestFile","w");fprintf(file,"%d\n",n);int t;srand(t);for(int i=0;i<n;++i){t=rand();fprintf(file,"%d ",rand()%10);}fclose(file);return ;
}T* CreatList(int &n)
{//printf("n=");//CreatData();ifstream in("TestFile");in >> n;T* ret = new T[n];for(int i=0;i<n;++i){in>>ret[i];}in.close();return ret;
}void Init(T* a,int l,int r)
{int mid=(l+r)>>1;if(a[mid] > a[l]) swap(a[mid],a[l]);if(a[mid] > a[r-1]) swap(a[mid],a[r-1]);if(a[l] > a[r-1]) swap(a[l],a[r-1]);return;
}void QuickSort1(T* a,int l,int r)
{if(r-l<2) return;Init(a,l,r);//將首部、尾部、中間三個數中的中值放在開頭//從一邊劃分int index=l;T x=a[l];for(int i=l+1;i<r;++i){if(a[i]<x)swap(a[++index],a[i]);}swap(a[index],a[l]);QuickSort1(a,l,index); QuickSort1(a,index+1,r);
}void QuickSort2(T* a,int l,int r)
{if(r-l<2) return;Init(a,l,r);//將首部、尾部、中間三個數中的中值放在開頭//空穴法int i=l,j=r-1;T x=a[l];while(i<j){while(i<j && a[j]>x) --j; a[i]=a[j];while(i<j && a[i]<=x) ++i;a[j]=a[i];}a[i]=x;QuickSort2(a,l,i); QuickSort2(a,i+1,r);
}void QuickSort3(T* a,int l,int r)
{if(r-l<2) return;Init(a,l,r);//將首部、尾部、中間三個數中的中值放在開頭int i=l-1,j=r;T pivot = a[l];while(i<j){do ++i; while(a[i] < pivot);do --j; while(a[j] > pivot);if(i < j) swap(a[i],a[j]);}QuickSort3(a,l,j+1); QuickSort3(a,j+1,r);
}void QuickSort4(T* a,int l,int r)
{if(r-l<2) return;int i=l-1,j=r;T pivot = a[l];while(i<j){do ++i; while(a[i] < pivot);do --j; while(a[j] > pivot);if(i < j) swap(a[i],a[j]);}QuickSort4(a,l,j+1); QuickSort4(a,j+1,r);
}void Show(T* a,int n)
{for(int i=0;i<n;++i){cout<<a[i]<<" ";}cout<<endl;
}void Test(FP fp[])
{for(int i=0;i<4;++i){clock_t S,E;int Time = 10;double sum=0;for(int j=0;j<Time;++j){int n;T* a=CreatList(n);S=clock();fp[i](a,0,n);E=clock();sum+=(double)(E-S)/CLOCKS_PER_SEC;//cout<<"經過排序之后:"<<endl;//Show(a,n);delete[] a;}printf("QuickSort%d's times=%f\n",i+1,sum/Time);}
}int main()
{FP fp[4] = {QuickSort1,QuickSort2,QuickSort3,QuickSort4};Test(fp);return 0;
}