1. 程式人生 > >快速排序演算法的思想和幾種實現方式

快速排序演算法的思想和幾種實現方式

快速排序演算法是基於分治策略的另一個排序演算法。

該方法的基本思想是:

1.先從數列中取出一個數作為基準數,記為x。

2.分割槽過程,將不小於x的數全放到它的右邊,不大於x的數全放到它的左邊。(這樣key的位置左邊的沒有大於key的,右邊的沒有小於key的,只需對左右區間排序即可)

3.再對左右區間重複第二步,直到各區間只有一個數

快排目前有兩類實現演算法,第一種是標準演算法,第二種是兩頭交換法。總的思想與上面三步一樣,在細節處理上有一些差異。

標準演算法思想及實現

標準算演算法採用的思想是挖坑填坑的思想:

以一個數組作為示例,取區間第一個數為基準數。

0

1

2

3

4

5

6

7

8

9

72

6

57

88

60

42

83

73

48

85

初始時,i = 0;  j = 9;   X = a[i] = 72

由於已經將a[0]中的數儲存到X中,可以理解成在陣列a[0]上挖了個坑,可以將其它資料填充到這來。

j開始向前找一個比X小或等於X的數。當j=8,符合條件,將a[8]挖出再填到上一個坑a[0]中。a[0]=a[8]; i++;  這樣一個坑a[0]就被搞定了,但又形成了一個新坑a[8],這怎麼辦了?簡單,再找數字來填a[8]這個坑。這次從i開始向後找一個大於X的數,當i=3,符合條件,將a[3]挖出再填到上一個坑中a[8]=a[3]; j--;

陣列變為:

0

1

2

3

4

5

6

7

8

9

48

6

57

88

60

42

83

73

88

85

i = 3;   j = 7;   X=72

再重複上面的步驟,先從後向前找,再從前向後找

j開始向前找,當j=5,符合條件,將a[5]挖出填到上一個坑中,a[3] = a[5]; i++;

i開始向後找,當i=5時,由於i==j退出。

此時,i = j = 5,而a[5]剛好又是上次挖的坑,因此將X填入a[5]

陣列變為:

0

1

2

3

4

5

6

7

8

9

48

6

57

42

60

72

83

73

88

85

可以看出a[5]前面的數字都小於它,a[5]後面的數字都大於它。因此再對a[0…4]a[6…9]這二個子區間重複上述步驟就可以了。

對挖坑填數進行總結

1.i =L; j = R; 將基準數挖出形成第一個坑a[i]

2.j--由後向前找比它小的數,找到後挖出此數填前一個坑a[i]中。

3.i++由前向後找比它大的數,找到後也挖出此數填到前一個坑a[j]中。

4.再重複執行23二步,直到i==j,將基準數填入a[i]中。

程式碼實現如下:

#include <iostream>
using namespace std;
void quick_sort(int s[],int l, int r){
    int i = l,j = r, x = s[l];
    while(i < j){
        while(i < j && s[j] > x) j--;
        if(i < j)
            s[i++] = s[j];
        while(i < j && s[i] < x) i++;
        if(i < j)
            s[j--] = s[i]; 
    }
    //此時i==j,下面s[i]或者s[j]都可以,j-1,j+1也ok
    s[j] = x;
    if (l<i) quick_sort(s,l, i - 1);
    if (r>i) quick_sort(s,i + 1, r);
};
int main()
{
 int test[] = {34,5,4,5,3,2,6,90,5};
    quick_sort(test,0,8);
    for(auto c : test){
        cout<<c<<"  ";
    }
    cout<<endl;
 return 0;
}

兩頭交換法思想及實現

兩頭交換法與標準演算法思想的差異是,先從左邊開始找到大於基準值的那個數,再從右邊找到小於基準值的那個數,將兩個數交換(這樣比基準值小的都在左邊,比基準值大的都在右邊)。直到數列分成大於基準值和小於基準值的兩個區間,以這兩個區間進行同樣的排序操作。

程式碼實現如下:

#include <iostream>
using namespace std;
void quickSort(int a[],int beg,int end){
   //partition非遞迴實現,官方版
   if(beg >= end) return;
    int i = beg, j = end, x = a[(i + j)>>1],tmp =0;//這裡基準值選了中間的值
    while(i <= j){//取等號,確保分成兩個不相交區間
        while( a[i] < x) i++;
        while(a[j] > x) j--;
        if(i <= j ){
            tmp = a[i];
            a[i] = a[j];
            a[j] = tmp;
            i++;
            j--;
        }     
    }
    quickSort(a,beg,j);
    quickSort(a,i,end);
};
int main()
{
    int test[] = {34,6,4,5,1,2,6,90,7};
    quickSort(test,0,8);
    for(auto c : test){
        cout<<c<<"  ";
    }
    cout<<endl;
    return 0;}

上面的演算法是兩頭交換法官方的版本,邊界情況較少,比較健壯。

兩頭交換法還有另一個實現方式,這種實現方式,基準值只能選區間第一個值或最後一個值。基準值不參與交換,將除基準值之外的所有值按照與基準值的大小關係分成兩部分,然後將區間分界點的值填到基準值的坑裡,將基準值放在區間分界點。對於基準值左右的區間進行再次排序。

程式碼實現:

#include <iostream>
using namespace std;
//兩點交換法,固定軸點的實現,基準點不參與排序
//基準點選第一個值,中間交換點選j,
//基準點選第一個值,中間交換點選i
//不然,會出現死迴圈
//將除第一個值之外的其他值交換使得小於基準值的在前,大於的在後,然後最中間點較小的j位置的值的與第一個值交換,交換後前面的小於基準值,後面的大於基準值
int partition(int b[],int first,int last){
    int x = b[first],temp = 0;
    int i = first,j = last + 1;//因為後面判斷是--j
    while(true){
        while(b[++i] < x && i <= last);
        while(b[--j] > x);
        if(i >= j){
            break;    
        }
        temp = b[i];
        b[i] = b[j];
        b[j] = temp;
    }
    b[first] = b[j];
    b[j] = x;
    return j;
};

void quickSort(int a[],int beg,int end){
    if(beg < end){
        int q = partition(a,beg,end);
        quickSort(a,beg,q-1);
        quickSort(a,q+1,end);
    }
};
int main()
{
	int test[] = {34,5,4,5,3,2,6,90,5};
    quickSort(test,0,8);
    for(auto c : test){
        cout<<c<<"  ";
    }
    cout<<endl;
	return 0;
}

注意:兩頭交換法,最後填坑點的選擇與基準值的選擇有關係,當基準值在區間前半部分則填坑點選值較小的j,反之則選i.

效率分析

快排的時間複雜度理論上是Nlog(N). i,j點的值與基準值比較時取“嚴格大於/小於”還是”大於等於/小於等於”會影響復法最壞複雜度。一般的想法是用大於等於/小於等於,忽略與樞紐元相同的元素,這樣可以減少不必要的交換,因為這些元素無論放在哪一邊都是一樣的。但是如果遇到所有元素都一樣的情況,這種方法每次都會產生最壞的劃分,也就是一邊1個元素,令一邊n-1個元素,使得時間複雜度變成O(n2)。而如果用嚴格大於/小於,雖然兩邊指標每此只挪動1位,但是它們會在正中間相遇,產生一個最好的劃分,時間複雜度為log2nlog2n。但是當取嚴格大於/小於的時候,交換的次數也會相應的增加。實際的交換次數應該是相同的。

心得

一般建議使用標準演算法或者兩頭交換演算法的標準版,這兩個版本邊界情況較少,適用面廣。