快速排序
主要特點
1)普通快速排序最差時間複雜度爲o(n^2)
2)期望時間複雜度爲o(nlgn)
3)在o(nlgn)中蘊含的常量比較小
4)就地排序,不需要輔助數組空間
主要操作
1.數組劃分
在快速排序算法中,最爲關鍵的就是選取一個基值,將數組分爲大於基值以及小於基值兩部分,並返回基值所以在位置以利用於遞歸劃分。
下面先介紹Lomuto partitioning方法。
對數組a,設需要劃分的其中一段爲a[p]~a[r],我們期待的結果是得到一個q,其中p<=q<=r,使得a[p]~a[q-1]<=a[q]<=a[q+1]~a[r],這個時候原先的一段數組被分成了三部分。
首先,設基值爲這段數組的最後一個元素a[r],我們希望最後得到的結果是a[r]現在對應的值在算法結束後可以排在比他大和小的兩部分的中間愛。
然後令i=p-1; j=p,當發現有a[j]>x時,j繼續前進,不需要任何移動。當發現a[j]<=x時,我們需要將這個元素放到小於基值的一邊,於是將i自加1,並交換此時a[i],與a[j]的元素,然後j自加1。這個時候i指向的是比基值小的那段數據的最後一個元素,j指向的是第一個還沒有判斷的剩餘元素。
上面一步不斷循環直到j指向了r,此時只剩下r沒有和基值判斷了,而a[r]本來就是基值,而除了a[r]以外,a[p]~a[i]是小於基值的部分,a[i+1]~a[r-1]是大於基值的部分,所以此時只需交換a[i+1]和a[r]即可。
由於對數組a從頭到尾掃描一次就可以得到結果,因此這一部分算法複雜度爲o(n)
2.遞歸排序
在首先對整個數組進行了劃分後,我們將數組分成了兩部分,一部分比基值小,一部分比基值大,並且我們知道了基值所在的位置,設爲q,因此只需對劃分出來的兩部分進行遞歸排序即可。
在最壞的情況下,每次劃分出的一部分大小爲0,一部分爲n-1,這種情況下這種遞歸劃分要進行n次,時間複雜度爲o(n^2)
在稍微好一點的情況下,每次劃分一部分大小爲十分之一,一部分大小爲十分之九,這種情況下有遞歸公式:
T (n ) ≤ T (9n /10) + T (n /10) + (n )= O (n lg n )
在平均的情況下,當最好和最壞的情況依次發生時,最壞的情況對漸進時間上的影響不大,因此時間複雜度爲o(nlgn)
3.隨機化
隨機化處理在需要劃分的區域中隨機選擇一個作爲劃分基準,這樣可以使劃分得到更好的平均情況性能。
其他討論
1.hoare劃分
這個劃分主要原理和上面的劃分原理差不多,不一樣的地方在於這個劃分將基準元素也分到某一個組中,得到是兩個分組。具體如下:
對於數組a,區間爲p~r的一段,設基準爲區域的首個元素x=a[p],i,j初始分別指向區域的兩頭,i和j不斷靠近,當遇到a[j]<x,a[i]>x時,將兩個元素進行交換,這時i指向的是比基準小的最後一個元素,j指向的是比基準大的元素。不斷循環直至i==j循環結束,此時以i或j分界將數組劃分爲兩部分。
這個算法也是掃描一次數組即可完成,因此時間複雜度爲o(n)
2.stooge排序
stooge排序的原理是將數組分爲三部分,首先對數組的前2/3進行排序,然後對數組的後2/3進行排序,最後再對數組的前2/3進行排序。遞歸公式爲T(N)=3T(2n/3)+o(1),根據主定理,得到時間複雜度爲o(n^log(3/2,3))~=o(n^2.7)
詳見http://ufownl.blog.163.com/blog/static/1250122200861072445895/
3.消除尾遞歸
由於quicksort第二次遞歸調用不是必須的,因此可以用循環代替,取消尾遞歸,具體實現見下面代碼quicksort2
4.三數取中
爲了更好地得到基準元素,可以使用隨機選取三個元素,然後取中數的方式進行優化,具體實現見下面代碼three_randomized_partition
5.區間模糊排序
對n個區間[ai,bi],進行排序,使得存在一個ci屬於[ai,bi],滿足c1<=c2<=……<=cn,使用快速排序進行改造實現。
參考:http://cnicwsh.iteye.com/blog/504394,實現代碼見最後。
問題描述:
考慮這樣的一種排序問題,即無法準確地知道待排序的各個數字到底是多少。對於其中的每個數字,我們只知道它落在實軸上的某個區間內。亦即,給定的是n個形如[a(i), b(i)]的閉區間(這裏小括後起下標的作用,後同),其中a(i) <= b(i)。算法的目標是對這些區間進行模糊排序(fuzzy-sort),亦即,產生各區間的一個排列<i(1), i(2), ..., i(n)>,使得存在一個c(j)屬於
區間[a(i(j)), b(i(j))],滿足c(1) <= c(2) <= c(3) <= ... <= c(n)。
a) 爲n個區間的模糊排序設計一個算法。你的算法應該具有算法的一般結構,它可以快速排序左部端點(即各a(i)),也要能充分利用重疊區間來改善運行時間。(隨着各區間重疊得越來越多,對各區間進行模糊排序的問題會變得越來越容易。你的算法應能充分利用這種重疊。)
b) 證明:在一般情況下,你的算法的期望運行時間爲Θ(nlgn),但當所有的區間都重疊時,期望的運行時間爲Θ(n)(亦即,當存在一個值x,使得對所有的i,都有x∈[a(i), b(i)])。你的算法不應顯式地檢查這種情況,而是應隨着重疊量的增加,性能自然地有所改善。
算法思想:
利用快速排序算法的思想,對區間序列進行劃分。但此時的主元區間元素是一個區間元素集合中所有區間元素的公共區間(交集),
即是說該集合中的所有區間元素都是“相等的”或者說“任意序都是有序的”。初始時,算法任選一個區間元素作爲主元(同快速排序)。
算法運行過程中,如果某個區間元素嚴格小於主元,則將其放到序列左邊;如果其嚴格大於主元,則將其放到序列右邊;否則,說明該區間元素與主元相交,則更新主元區間大小爲該相交的區間部分,並將該區間元素加入到與主元“相等”的區間元素集合中。處理完當前序列後,再遞歸處理左邊和右邊的子序列(分治思想嘛,同快速排序)。
在程序進行的過程中,將不斷排除掉重疊的部分,如果所有部分都會重疊,則在第一次partition的時候已經完成,用時爲o(n)
普通快速排序代碼如下:
#include<stdio.h>
#include<algorithm>
#include<stdlib.h>
#include<time.h>
using namespace std;
int a[100];
int b[100];
int c[100];
void stooge_sort(int *a,int i,int j){
if(a[i]>a[j])
swap(a[i],a[j]);
if((i+1)>=j)
return;
int k=(j-i+1)/3;
stooge_sort(a,i,j-k);;
stooge_sort(a,i+k,j);
stooge_sort(a,i,j-k);
}
int partion(int *a, int p, int r){
int x=a[r];
int i=p-1;
for(int j=p; j<r; j++){
if(a[j]<=x){
i++;
swap(a[i],a[j]);
}
}
swap(a[i+1],a[r]);
return i+1;
}
void quicksort(int *a, int p, int r){
if(p<r){
int q=partion(a,p,r);
quicksort(a,p,q-1);
quicksort(a,q+1,r);
}
}
void quicksort2(int *a, int p, int r){
while(p<r){
int q=partion(a,p,r);
if(q-p<r-q){
quicksort2(a,p,q-1);
p=q+1;
}
else{
quicksort2(a,q+1,r);
r=q-1;
}
}
}
int moare_partion(int *a,int p,int r){
int x=a[p];
int i=p-1;
int j=r+1;
while(1){
do{
j--;
}while(a[j]<x);
do{
i++;
}while(a[i]>x);
if(i<j)
swap(a[i],a[j]);
else
return j;
}
}
void moare_quicksort(int *a, int p, int r){
if(p<r){
int q=moare_partion(a,p,r);
moare_quicksort(a,p,q);
moare_quicksort(a,q+1,r);
}
}
int randomized_partition(int *a, int p, int r){
srand(time(NULL));
//i~[p,r]
int i=p+rand()%(r-p+1);
swap(a[i],a[r]);
return partion(a,p,r);
}
void randomized_quicksort(int*a, int p, int r){
if(p<r){
int q=randomized_partition(a,p,r);
randomized_quicksort(a,p,q-1);
randomized_quicksort(a,q+1,r);
}
}
int three_randomized_partition(int *a, int p, int r){
srand(time(NULL));
//i~[p,r]
int i=p+rand()%(r-p+1);
int j=p+rand()%(r-p+1);
int k=p+rand()%(r-p+1);
if(j<=k&&k<=i||i<=k&&k<=j)
i=k;
if(k<=j&&j<=i||i<=j&&j<=k)
i=j;
swap(a[i],a[r]);
return partion(a,p,r);
}
void three_randomized_quicksort(int *a, int p, int r){
if(p<r){
int q=three_randomized_partition(a,p,r);
three_randomized_quicksort(a,p,q-1);
three_randomized_quicksort(a,q+1,r);
}
}
int main(){
int len;
scanf("%d",&len);
for(int i=1; i<=len; i++){
scanf("%d",&a[i]);
}
// quicksort2(a,1,len);
// moare_quicksort(a,1,len);
// stooge_sort(a,1,len);
// three_randomized_quicksort(a,1,len);
randomized_quicksort(a,1,len);
for(int j=1; j<=len; j++){
if(j!=len)
printf("%d ",a[j]);
else
printf("%d\n",a[j]);
}
return 0;
}
區間模糊快速排序代碼:
#include<stdio.h>
using namespace std;
class Array{
public:
int begin;
int end;
Array(){}
Array(int a, int b){
if(a<=b){
this->begin=a;
this->end=b;
}
else{
this->begin=0;
this->end=0;
}
}
};
Array swap(Array*a, int x, int y){
Array tmp;
tmp=a[x];
a[x]=a[y];
a[y]=tmp;
}
//將數組分爲三段
//一段是小於基準主元部分,這一部分數組下標從p~i-1
//二段是與基準主元重疊的部分,這一部分數組下標是從i~j
//三段是大於基準主元的部分,這一部分數組下標從j+1~r
//在過程中使用k指示下一個需要判斷的數組元素
//遞歸將返回兩個整數i,j,用於標誌下一次遞歸要開始的地方
//使用Array類型作爲返回僅僅是爲了存儲i,j倆個數,標誌與基準重疊部分,不代表一個線段
Array partition(Array *a, int p, int r){
Array x=a[p];
int i=p;
int k=p+1;
int j=r;
while(k<=j){
//當前區域小於基準主元區域
if(a[k].end<x.begin){
swap(a,i,k);
i++;
k++;
}
//當前區域大於基準主元區域
else if(a[k].begin>x.end){
swap(a,j,k);
j--;
}
//當前區域於基準主元區域有重疊
else{
x.begin=a[k].begin>x.begin?a[k].begin:x.begin;
x.end=a[k].end<x.end?a[k].end:x.end;
k++;
}
}
return Array(i-1,j+1);
}
void quicksort(Array *a, int p, int r){
if(p<r){
Array q=partition(a,p,r);
quicksort(a,p,q.begin);
quicksort(a,q.end,r);
}
}
int main(){
Array a[6]={Array(2,4),Array(0,1),Array(5,6),Array(3,3),Array(1,5),Array(8,9)};
quicksort(a,0,5);
for(int i=0; i<6; i++){
printf("[%d,%d] ",a[i].begin,a[i].end);
}
printf("\n");
}