1. 程式人生 > >快速排序(三種演算法實現和非遞迴實現)

快速排序(三種演算法實現和非遞迴實現)

快速排序(Quick Sort)是對氣泡排序的一種改進,基本思想是選取一個記錄作為樞軸,經過一趟排序,將整段序列分為兩個部分,其中一部分的值都小於樞軸,另一部分都大於樞軸。然後繼續對這兩部分繼續進行排序,從而使整個序列達到有序。

遞迴實現:

void QuickSort(int* array,int left,int right)
{
    assert(array);
    if(left >= right)//表示已經完成一個組
    {
        return;
    }
    int index = PartSort(array,left,right);//樞軸的位置
QuickSort(array,left,index - 1); QuickSort(array,index + 1,right); }

PartSort()函式是進行一次快排的演算法。
對於快速排序的一次排序,有很多種演算法,我這裡列舉三種。

左右指標法

  1. 選取一個關鍵字(key)作為樞軸,一般取整組記錄的第一個數/最後一個,這裡採用選取序列最後一個數為樞軸。
  2. 設定兩個變數left = 0;right = N - 1;
  3. 從left一直向後走,直到找到一個大於key的值,right從後至前,直至找到一個小於key的值,然後交換這兩個數。
  4. 重複第三步,一直往後找,直到left和right相遇,這時將key放置left的位置即可。

這裡寫圖片描述

當left >= right時,一趟快速排序就完成了,這時將Key和array[left]的值進行一次交換。
一次快排的結果:4 1 3 0 2 5 9 8 6 7

基於這種思想,可以寫出程式碼:

int PartSort(int* array,int left,int right)
{
    int& key = array[right];
    while(left < right)
    {
        while(left < right && array[left] <= key)
        {
            ++left
; } while(left < right && array[right] >= key) { --right; } swap(array[left],array[right]); } swap(array[left],key); return left; }

問題:下面的程式碼為什麼還要判斷left < right?

while(left < right && array[left] <= key)

key是整段序列最後一個,right是key前一個位置,如果array[right]這個位置的值和key相等,滿足array[left] <= key,然後++left,這時候left會走到key的下標處。

挖坑法

  1. 選取一個關鍵字(key)作為樞軸,一般取整組記錄的第一個數/最後一個,這裡採用選取序列最後一個數為樞軸,也是初始的坑位。
  2. 設定兩個變數left = 0;right = N - 1;
  3. 從left一直向後走,直到找到一個大於key的值,然後將該數放入坑中,坑位變成了array[left]。
  4. right一直向前走,直到找到一個小於key的值,然後將該數放入坑中,坑位變成了array[right]。
  5. 重複3和4的步驟,直到left和right相遇,然後將key放入最後一個坑位。

這裡寫圖片描述

當left >= right時,將key放入最後一個坑,就完成了一次排序。
注意,left走的時候right是不動的,反之亦然。因為left先走,所有最後一個坑肯定在array[right]。

寫出程式碼:

int PartSort(int* array,int left,int right)
{
    int key = array[right];
    while(left < right)
    {
        while(left < right && array[left] <= key)
        {
            ++left;
        }
        array[right] = array[left];
        while(left < right && array[right] >= key)
        {
            --right;
        }
        array[left] = array[right];  
    }
    array[right] = key;
    return right;
}

前後指標法

  1. 定義變數cur指向序列的開頭,定義變數pre指向cur的前一個位置。
  2. 當array[cur] < key時,cur和pre同時往後走,如果array[cur]>key,cur往後走,pre留在大於key的數值前一個位置。
  3. 當array[cur]再次 < key時,交換array[cur]和array[pre]。

通俗一點就是,在沒找到大於key值前,pre永遠緊跟cur,遇到大的兩者之間機會拉開差距,中間差的肯定是連續的大於key的值,當再次遇到小於key的值時,交換兩個下標對應的值就好了。

帶著這種思想,看著圖示應該就能理解了。
這裡寫圖片描述

下面是實現程式碼:

int PartSort(int* array,int left,int right)
{
    if(left < right){
        int key = array[right];
        int cur = left;
        int pre = cur - 1;
        while(cur < right)
        {
            while(array[cur] < key && ++pre != cur)//如果找到小於key的值,並且cur和pre之間有距離時則進行交換。注意兩個條件的先後位置不能更換,可以參照評論中的解釋
            {
                swap(array[cur],array[pre]);
            }
            ++cur;
        }
        swap(array[++pre],array[right]);
        return pre;
    }
    return -1;
}

最後的前後指標法思路有點繞,多思考一下就好了。它最大的特點就是,左右指標法和挖坑法只能針對順序序列進行排序,如果是對一個連結串列進行排序, 就無用武之地了。

所以記住了,前後指標這個特點!

快速排序的優化

首先快排的思想是找一個樞軸,然後以樞軸為中介線,一遍都小於它,另一邊都大於它,然後對兩段區間繼續劃分,那麼樞軸的選取就很關鍵。

1、三數取中法
上面的程式碼思想都是直接拿序列的最後一個值作為樞軸,如果最後這個值剛好是整段序列最大或者最小的值,那麼這次劃分就是沒意義的。
所以當序列是正序或者逆序時,每次選到的樞軸都是沒有起到劃分的作用。快排的效率會極速退化。

所以可以每次在選樞軸時,在序列的第一,中間,最後三個值裡面選一箇中間值出來作為樞軸,保證每次劃分接近均等。

2、直接插入
由於是遞迴程式,每一次遞迴都要開闢棧幀,當遞迴到序列裡的值不是很多時,我們可以採用直接插入排序來完成,從而避免這些棧幀的消耗。

整個程式碼:

//三數取中
int GetMid(int* array,int left,int right)
{
    assert(array);
    int mid = left + ((right - left)>>1);
    if(array[left] <= array[right])
    {
        if(array[mid] <  array[left])
            return left;
        else if(array[mid] > array[right])
            return right;
        else
            return mid;
    }
    else
    {
        if(array[mid] < array[right])
            return right;
        else if(array[mid] > array[left])
            return left;
        else
            return mid;
    }

}

//左右指標法
int PartSort1(int* array,int left,int right)
{
    assert(array);
    int mid = GetMid(array,left,right);
    swap(array[mid],array[right]);

    int& key = array[right];
    while(left < right)
    {
        while(left < right && array[left] <= key)//因為有可能有相同的值,防止越界,所以加上left < right
            ++left;
        while(left < right && array[right] >= key)
            --right;

        swap(array[left],array[right]);
    }

    swap(array[left],key);
    return left;
}

//挖坑法
int PartSort2(int* array,int left,int right)
{
    assert(array);
    int mid = GetMid(array,left,right);
    swap(array[mid],array[right]);

    int key = array[right];
    while(left < right)
    {
        while(left < right && array[left] <= key)
            ++left;
        array[right] = array[left];

        while(left < right && array[right] >= key)
            --right;
        array[left] = array[right];
    }
    array[right] = key;
    return right;
}

//前後指標法
int PartSort3(int* array,int left,int right)
{
    assert(array);
    int mid = GetMid(array,left,right);
    swap(array[mid],array[right]);
    if(left < right){
        int key = array[right];
        int cur = left;
        int pre = left - 1;
        while(cur < right)
        {
             while(array[cur] < key && ++pre != cur)
             {
                 swap(array[cur],array[pre]);
             }
                ++cur;
        }
            swap(array[++pre],array[right]);
            return pre;
    }
    return -1;
}

void QuickSort(int* array,int left,int right)
{
    assert(array);
    if(left >= right)
        return;

    //當序列較短時,採用直接插入
    if((right - left) <= 5)
    InsertSort(array,right-left+1);

    int index = PartSort3(array,left,right);
    QuickSort(array,left,index-1);
    QuickSort(array,index+1,right);
}

int main()
{
    int array[] = {4,1,7,6,9,2,8,0,3,5};
    QuickSort(array,0,sizeof(array)/sizeof(array[0]) -1);//因為傳的是區間,所以這裡要 - 1;
}

非遞迴實現

遞迴的演算法主要是在劃分子區間,如果要非遞迴實現快排,只要使用一個棧來儲存區間就可以了。
一般將遞迴程式改成非遞迴首先想到的就是使用棧,因為遞迴本身就是一個壓棧的過程。

void QuickSortNotR(int* array,int left,int right)
{
    assert(array);
    stack<int> s;
    s.push(left);
    s.push(right);//後入的right,所以要先拿right
    while(!s.empty)//棧不為空
    {
        int right = s.top();
        s.pop();
        int left = s.top();
        s.pop();

        int index = PartSort(array,left,right);
        if((index - 1) > left)//左子序列
        {
            s.push(left);
            s.push(index - 1);
        }
        if((index) + 1) < right)//右子序列
        {
            s.push(index + 1);
            s.push(right);
        }
    }
}

上面就是關於快速排序的一些知識點,如果哪裡有錯誤,還望指出。