數據結構-快速排序的三種實現方式及其優化

快速排序初步瞭解:

快速排序是由東尼·霍爾所發展的一種排序算法。在平均狀況下,排序 n 個項目要Ο(n log n)次比較。在最壞狀況下則需要Ο(n2)次比較,但這種狀況並不常見。後面將提供一種快排的優化方式可以儘量避免出現Ο(n2)的複雜度。
基本思路
1.先從數據當中找出一個數據作爲參照數
2.然後開始分區操作,大於參照數的就放到參照數右邊,小於參照數的就放到參照數的左邊.
3.然後對左右分區繼續進行該操作,知道區間裏面只有一個數據的是時候,快排完成

排序效果:

這裏寫圖片描述


一、左右指針法

<1>思路:給定一個數組,再給定左右區間的下標值left,right(此處我給的是左右閉區間),給定一個基準值爲arr[right],接下來就是循環和遞歸的過程。單趟過程中,先讓begin記錄該區間的第一個下標(就像一個指針指向頭一樣),end記錄最後一個值的下標,基準值爲該區間的最後一個值。循環過程中,begin記錄比基準值大的值,end記錄比基準值小的值,判斷如果begin和end還未相遇,則交換對應的值,再進行循環直至begin和end相遇。相遇後此時單趟排序完成,現在begin和end指向一個元素,該元素左側都是比基準值小的值,右側都是比基準值大的,將begin對應的值與基準值進行交換,開始遞歸左半區間和右半區間,重複上述過程。

<2>圖示單趟過程:

這裏寫圖片描述

整體過程如下:
1 5 6 2 7 4
1 2 6 5 7 4
1 2 4 5 7 6
1 2 4 5 6 7

<3>實現代碼
//左右指針法
void LRPoint(int* arr, int left, int right)
{
    if (left >= right)
        return;

    assert(arr);
    int begin = left;
    int end = right;
    int key = arr[end];

    while (begin < end)
    {
        while (begin < end && arr[begin] <= key)
            begin++;
        while (begin < end && arr[end] >= key)
            end--;

        if (begin < end)
            swap(arr[begin], arr[end]);
    }

    //begin end指向一起,與key交換
    swap(arr[begin], arr[right]);

    LRPoint(arr, left,begin - 1);
    LRPoint(arr, begin+1,right);
}

二、挖坑法

<1>思路:大體上如同左右指針法,但是左右指針法是在begin和end符合條件的值後,將begin和end對應的值進行交換;而挖坑法則是邊找邊替換,在最後再將key填入坑中。圖中橙色元素由於此處的值賦值到了別處,可以形象的看做是一個“坑”。


<2>圖示單趟過程:

這裏寫圖片描述

整體過程如下:
1 5 6 2 7 4
1 5 6 2 7 5
1 2 6 2 7 5
1 2 6 6 7 5
1 2 4 6 7 5
1 2 4 6 7 6
1 2 4 5 7 6
1 2 4 5 7 7
1 2 4 5 6 7

<3>實現代碼
void PitPoint(int* arr, int left, int right)
{
    if (left >= right)
        return;

    int begin = left;
    int end = right;
    int key = arr[right];

    while (begin < end)
    {
        while (begin < end && arr[begin] <= key)
            begin++;
        arr[end] = arr[begin];

        while (begin < end && arr[end] >= key)
            end--;
        arr[begin] = arr[end];
    }
    arr[end] = key;

    PitPoint(arr, left, begin - 1);
    PitPoint(arr, begin + 1, right);
}

三、前後指針法

<1>思路:一開始使用cur記錄left位置,prev記錄cur的前一個位置,利用cur來尋找比基準值小的值,需要注意的是隻有當arr[cur] < arr[right]++prev != cur兩個條件都滿足才進行交換,此處還涉及如果arr[cur] < arr[right]不滿足則不進行prev++,當條件都滿足時進行交換,出了循環後將基準值放在prev++的位置。看着代碼自己縷一遍就會發現規律了。

<2>圖示單趟過程:

這裏寫圖片描述

整體過程如下:
1 5 6 2 7 4
1 2 6 5 7 4
1 2 4 5 7 6
1 2 4 5 6 7

<3>實現代碼
void FBPoint(int* arr, int left, int right)
{
    if (left >= right)
        return;

    int cur = left;
    int prev = left - 1;

    while (cur < right)
    {
        if (arr[cur] < arr[right] && ++prev != cur)
            swap(arr[cur], arr[prev]);
        ++cur;
    }
    swap(arr[++prev], arr[right]);

    FBPoint(arr, left, prev - 1);
    FBPoint(arr, prev + 1, right);
}


四、非遞歸實現

<1>思路:
用棧來模擬實現遞歸操作,每次從棧中取出對應的左右邊界進行操作;在push邊界的順序與取棧頂數據作爲左右邊界的順序要對應。

<2>實現代碼:
void NRQuickSort(int* arr, int sz)
{
    assert(arr);
    stack<int> s;
    int left = 0;
    int right = sz;
    s.push(right);
    s.push(left);
    while (!s.empty())
    {
        left = s.top();
        s.pop();
        right = s.top();
        s.pop();

        if (left < right)
        {
            int begin = left;
            int end = right;
            int key = arr[right];

            while (begin < end)
            {
                //1,5,6,2,7,4
                while (begin < end && arr[begin] <= key)
                    begin++;
                while (begin < end && arr[end] >= key)
                    end--;

                if (begin < end)
                    swap(arr[begin], arr[end]);
            }
            swap(arr[begin], arr[right]);

            s.push(right);
            s.push(begin + 1);
            s.push(begin - 1);
            s.push(left);
        }
    }
}

五、優化

<1>通過三數取中法選取key的下標:
當按照上面的代碼如果每次取key的時候取到了待排序列中最大的或者最小的. 也就是序列有序的時候,快速排序的時間複雜度爲O(N^2),我們可以通過選擇合適的key值來進行優化:也就是所謂的三數取中法(給定左右區間的下標,計算出中間下標,並返回這三個值中不大不小的那個),將該下標所對應的值和區間最右側的值進行交換即可。拿左右指針法優化舉例:
int GetMid(int* a, int left, int right)
{
    int mid = left + ((right - left) >> 1);

    // left mid
    if (a[left] < a[mid])
    {
        if (a[right] < a[left])
            return left;
        else if (a[right] > a[mid])
            return mid;
        else
            return right;
    }
    else
    {
        //mid  left
        if (a[right] < a[mid])
            return mid;
        else if (a[right] < a[left])
            return left;
        else
            return right;
    }
}


void LRPoint(int* arr, int left, int right)
{
    if (left >= right)
        return;

    assert(arr);
    int begin = left;
    int end = right;
    swap(arr[end], arr[GetMid(arr, left, right)]);
    int key = arr[end];

    while (begin < end)
    {
        while (begin < end && arr[begin] <= key)
            begin++;
        while (begin < end && arr[end] >= key)
            end--;

        if (begin < end)
            swap(arr[begin], arr[end]);
    }

    //begin end指向一起,與key交換
    swap(arr[begin], arr[right]);

    LRPoint(arr, left,begin - 1);
    LRPoint(arr, begin+1,right);
}

<2>小區間優化:
思路:由於遞歸需要建立棧幀消耗十分大,我們可以判斷如果該區間內的元素值小於n(可以取5~15,視情況而定)的時候直接採取插入排序來處理該區間內的數據,就不必再多建立那麼多棧幀,從而提高效率。具體實現就不給出了,只需要添加一個判斷條件再引入插入排序即可。
發佈了55 篇原創文章 · 獲贊 192 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章