快速排序的三種實現方式以及非遞歸版本

一、快速排序的基本思想

快速排序利用了分治的思想,分而治之。通過一趟排序將序列分爲兩部分,其中一部分比較關鍵字小,一部分比關鍵字大。之後繼續對這兩個子序列重複此過程,直到整個序列都有序。

二、快速排序的三個步驟

  1. 選基準值:在要排序的序列中選一個幾個基準值,作爲劃分區間的 關鍵字。通常選取關鍵字的方式對效率有很大的影響。
  2. 分割序列:按照基準值把序列分爲兩部分,左邊的小於基準值,右邊的大於基準值。
  3. 重複分割:對子序列重複上面的步驟,知道序列爲空或者只有一個元素。當遞歸處理的所有子序列都返回時,序列有序。

三、三種實現方式

注:下面代碼中所有區間都是左閉右開,[ left, right),且排序都是從大到小。且基準值默認選擇序列的最後一個元素

一、版本一填充法

思路:兩個指針 begng作爲序列的開始 和 end 序列的結束。

將序列的最後一個元素保存起來,作爲基準值。

開始劃分序列:直到begin < end 不成立

begin 從左往右找比 基準值 大的元素,將該值填充到 end 的位置(end 位置上的值已經保存到 key),end-- ,因爲已經劃分好了一個元素。
end 從右往左找比 基準值小的元素,如將該值填充到 begin 的位置(begin 位置上的值已經保存到 end位置),begin++,已經劃分好一個元素。

循環結束時,將 key 填充到 end 的位置,然後將 end 返回,作爲下一次劃分序列的邊界。

// 版本一 填充法
int partition(vector<int> &v, int left, int right)
{
    int begin = left;
    int end = right - 1;
    int key = v[end];          // 選基準

    while (begin < end)
    {
        while (begin < end && v[begin] <= key)
            begin++;
        if (begin < end)
            v[end--] = v[begin];
        while (begin < end && v[end] >= key)
            end--;
        if (begin < end)
            v[begin++] = v[end];
    }
    v[end] = key;
    return end;
}
void QuickSort(vector<int>& v, int left, int right)
{
    if (left < right)
    {
        int boundary = partition(v, left, right);
        QuickSort(v, left, boundary);
        QuickSort(v, boundary + 1, right);
    }
}

二、版本二 交換法

和上面的版本一類似,不同之處是,這裏不是填充,而是將找到的元素交換。

// horn版本
int partition(vector<int> &v, int left, int right)
{
    int begin = left;
    int end = right - 1;
    int key = v[end];
    while (begin < end)
    {
        while (begin < end && v[begin] <= key)
            begin++;
        while (begin < end && v[end] >= key)
            end--;

        if (begin < end)
            std::swap(v[begin], v[end]);
    }
    if(end != right -1)
     std::swap(v[end], v[right-1];)

    return end;
}
void QuickSort(vector<int>& v, int left, int right)
{
    if (left < right)
    {
        int boundary = partition(v, left, right);
        QuickSort(v, left, boundary);
        QuickSort(v, boundary + 1, right);
    }
}

我認爲一,二的主要不同在於一個是填充一個是交換,當比較對象較大時,填充的效率是高於交換,不需要在構造一個臨時變量。

三、版本三 雙指針前移法

該方法短小精悍。

思路:
兩個指針從同一個方向走。
cur 指向當前元素,每次循環都要往前走,直到遍歷完序列,所以循環結束的條件就是 cur < right ,right 爲 序列的右邊界。

prev 指向cur的前一個位置。

如果cur 指向的值小於 基準值, 那麼就讓 prev前進一個位置,並判斷 cur 是否等於 prev,如果不等,說明它兩不在同一位置,那麼交換位置上的值。

cur 要麼相鄰,要麼中間間隔的都是別基準值大的元素。cur 的目的就是替 prev 清理前進路上的障礙—“比基準值小的元素”。剩下的都是比基準值大的元素, 只要當 cur 找到比基準值小的,就放心的交換。

循環結束時,需要將基準值,即序列的最後一個元素,與 prev下一個位置上的元素 交換。因爲這個prev 所在的位置是比基準值小的元素,那麼prev 的下一個位置一定比 prev 大,將這個比基準值大的元素與基準值交換就完成了一次劃分。

int partition1(vector<int> &v, int left, int right)
{
    int prev = left - 1;
    int cur = left;
    int key = v[right - 1];

    while (cur < right)
    {
        if (v[cur] < key && ++prev != cur)
            std::swap(v[cur], v[prev]);
        cur++;
    }
    
    if (v[++prev] != key)
        std::swap(v[prev], v[right - 1]);
    return prev;
}


void QuickSort(vector<int>& v, int left, int right)
{
    if (left < right)
    {
        int boundary = partition1(v, left, right);
        QuickSort(v, left, boundary);
        QuickSort(v, boundary + 1, right);
    }
}

非遞歸版本

每次用到遞歸,大家都會擔心遞歸太深是否會棧溢出?我測試了900000000個int 整型的排序(3.35GB左右內存),棧也木有溢出啊。。

儘管如此還是給出非遞歸的實現版本吧,比較棧資源寶貴。

利用棧將序列起始位置與結束位置保存起來。


void QuickSortNor(vector<int> &v, int left, int right)
{
    if (left >= right)
        return;
    stack<int> s;
    s.push(left);
    s.push(right);

    while (!s.empty())
    {
        int right = s.top();
        s.pop();
        int left = s.top();
        s.pop();
        if (left < right)
        {
            int boundary = partition1(v, left, right);
            // 左區間
            s.push(left);
            s.push(boundary);
            // 右區間
            s.push(boundary + 1);
            s.push(right);
        }

    }
}

簡單總結一下:

快速排序,是一種不穩定的排序,時間複雜度平均爲 O(nlog2n)O(nlog_2n),逆序序列下效果最差,逼近 O(n2)O( n^2),其空間複雜度是 O(log2n)O(log_2n),因爲需要空間來保存遞歸的區間範圍。


2019年03月05日11:43:34 增加測試雙指針法代碼

#include <iostream>
#include <cstdlib>
#include <vector>
#include <ctime>
#include <algorithm>
#include <functional>
using namespace std;

int partition1(vector<int> &v, int left, int right)
{
    int prev = left - 1;
    int cur = left;
    int key = v[right - 1];

    while (cur < right)
    {
        if (v[cur] < key && ++prev != cur)
            std::swap(v[cur], v[prev]);
        cur++;
    }

    if (v[++prev] != key)
        std::swap(v[prev], v[right - 1]);
    return prev;
}


void QuickSort(vector<int>& v, int left, int right)
{
    if (left < right)
    {
        int boundary = partition1(v, left, right);
        QuickSort(v, left, boundary);
        QuickSort(v, boundary + 1, right);
    }
}


int main()
{
    // 生成隨機數, 使用自己寫的排序和系統默認排序, 在排序前後比較vector 檢測是否有效
    std::vector<int> test;
    std::vector<int> ok;
    int test_num = 100000;
    for(int i = 0; i < test_num; i++)
    {
        int a = std::rand()/1000000;
        test.push_back(a);
        ok.push_back(a);
    }
    if (test == ok )
    {
        std::cout << "sort before eq" << std::endl;
    }
    QuickSort(test, 0, test.size());
    // 測試數據較大時, 註釋掉了輸出每一個
    for (int i = 0; i < test.size(); i++)
    {
        //std::cout << test[i] << " ";
    }
    std::cout << endl;
    std::sort(ok.begin(), ok.end());
    for (int i = 0; i < ok.size(); i++)
    {
        //std::cout << ok[i] << " ";
    }
    std::cout << endl;

    if (test == ok )
    {
        std::cout << "sort after eq" << std::endl;
    }
    return 0;
}

輸出:

sort before eq


sort after eq
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章