“快排”筆記

來源:互聯網
上載者:User

  關於排序,我想沒有多少程式員會感覺陌生,信手拈來的可能就有不少:冒泡、選擇或者歸併等等;可在實際開發中,排序又讓許多程式員感到很不熟悉,因為在大多數情況下,自己實現一個排序演算法都不是一個好主意,相反的,改而使用語言架構內建提供的排序功能才是明智之舉,畢竟她又方便又高效……

  久而久之,“排序”便與許多程式員成了“最熟悉的陌生人”,成了我們生活中司空見慣的“工具”:想要實現數組排序?簡單,一個 sort(array) 搞定,便捷又高效!什嗎?它內部是怎麼完成排序的?這個……不是特別清楚了……不過……管他呢……嘿嘿,不知你是否屬於這種情況,反正我是 :)

   這些天自己正在看些書籍,上面便提到了不少排序的演算法,本以為自己對這些演算法早已深諳於心,沒想實際實現之時便窘態畢現,我想大抵可能便如上所述,“嬌慣縱容”多了,以前只要簡單的調調 sort,而今真刀實槍起來便不勝招架了,也罷,有了些許教訓,也算進一步認識到“知其然不知其所以然”的道理,在此簡單筆記一番,引以為戒吧 ~

  而“快排”(快速排序)便是這次筆記的主題,話說在各類排序演算法中,“快排”應該算是“明星”演算法了,因其時間、空間複雜度俱佳,而被廣泛運用於實際程式開發中(也許上面那個 sort 便是 :)),網上已有非常多優秀的教程說明(譬如這裡和這裡),在此我也無需過多費舌,主要的內容還是實現源碼以及心得體會,僅此而已 :)

  

  快速排序的基本思想便是分而治之,大體上分為三個主要步驟:

  1. 在資料集中選取一個pivot元素

  2. 根據選定的pivot,將資料集劃分為左右兩部分,左部皆小於pivot,右部皆大於pivot

  3. 迴圈1、2兩步於上述所劃分的兩部分資料之上,直到部分只剩下一個資料元素為止

  根據上述的演算法步驟,一個典型的快排程式,大抵便是這個樣子:

  

/*!    \param array number array pointer    \param left left number range([left, right])    \param right right number range([left, right])    \return pivot index*/int GetPivot(int* array, int left, int right){    // simple pivot get    return left;}/*!    \param array number array pointer    \param left left number range([left, right])    \param right right number range([left, right])    \return pivot index after partition*/int Partition(int* array, int left, int right){    int pivot = GetPivot(array, left, right);    swap(array[left], array[pivot]);    // now left have the pivot value    int val = array[left];    int lp = left;    int rp = right + 1;    // preprocessing for avoiding some boder problems    do { ++lp; } while (array[lp] <= val && lp < rp);    do { --rp; } while (array[rp] > val);    // do loop partition    while (lp < rp)    {        swap(array[lp], array[rp]);        do { ++lp; } while (array[lp] <= val);        do { --rp; } while (array[rp] > val);    }    // swap back pivot value    swap(array[left], array[rp]);    return rp;}/*!    \param array number array pointer    \param left left number range([left, right])    \param right right number range([left, right])*/void QuickSort(int* array, int left, int right){    if (left < right)    {        int pivot = Partition(array, left, right);        QuickSort(array, left, pivot - 1);        QuickSort(array, pivot + 1, right);    }}

  當然,針對上述的三個步驟還有其他的實現方法,譬如這樣:

/*!    \param array number array pointer    \param left left number range([left, right])    \param right right number range([left, right])    \return pivot index*/int GetPivot(int* array, int left, int right){    // random chioce pivot    int count = right - left + 1;    return left + rand() % count;}/*!    \param array number array pointer    \param left left number range([left, right])    \param right right number range([left, right])    \return pivot index after partition*/int Partition(int* array, int left, int right){    int pivot = GetPivot(array, left, right);    swap(array[left], array[pivot]);    // now left have the pivot value    int val = array[left];    int searchIndex = left + 1;    int sortedIndex = searchIndex;    while (searchIndex <= right)    {        if (array[searchIndex] <= val)        {            swap(array[sortedIndex], array[searchIndex]);            ++sortedIndex;        }        ++searchIndex;    }    // swap back pivot value    swap(array[left], array[sortedIndex - 1]);    return sortedIndex - 1;}/*!    \param array number array pointer    \param left left number range([left, right])    \param right right number range([left, right])*/void QuickSort(int* array, int left, int right){    int pivot = Partition(array, left, right);    if (pivot - 1 > left)    {        QuickSort(array, left, pivot - 1);    }    if (pivot + 1 < right)    {        QuickSort(array, pivot + 1, right);    }}

雖說上面的兩種實現細節上有所差異,但是基本都屬於遞迴形式,並且遞迴形式也是快排演算法(或者說對於很多二分(甚至多分)演算法)實現的一般方法,有趣的是,上面提到的書籍中也說到了另一種實現快排演算法的“迴圈”方式,頗有趣味:

//! sort range structurestruct SortRange{    int l;    int r;    SortRange(int l_, int r_):l(l_),r(r_) {};    SortRange(const SortRange& si):l(si.l),r(si.r) {};    SortRange& operator = (const SortRange& si)    {        l = si.l;        r = si.r;        return *this;    }};/*!    \param array number array pointer    \param l left number range([left, right])    \param r right number range([left, right])*/void QuickSortIter(int* array, int l, int r){    std::queue<SortRange> q;    q.push(SortRange(l, r));    while (!q.empty())    {        // get front sort index and pop it        SortRange si = q.front();        q.pop();        int left = si.l;        int right = si.r;        if (left < right)        {            // do partition            int pivot = Partition(array, left, right);            // push back sub sort range            if (pivot - 1 > left)            {                q.push(SortRange(left, pivot - 1));            }            if (pivot + 1 < right)            {                q.push(SortRange(pivot + 1, right));            }        }    }}

  代碼的思路其實非常簡單,大體上便是類比原先遞迴實現的調用過程,上面的源碼中使用 std::queue 類比了這個過程,當然,使用 std::stack(或者其他類似結構)同樣也可以,兩者只在子問題的解決順序上存在差異,並不影響快速排序的正確性。

  接著,書中又順勢提到了快排的各類並行實現方法,其中最直接的一個想法可能就是延承上面的遞迴演算法,為每一次的Partition操作都產生一個線程,由於各個線程之間所操作的資料基本獨立,資料競爭問題並不存在(暫不考慮“偽共用”之類的問題),但是由於後期資料粒度過細(基本上每一個資料項目都產生了一個線程),往往導致線程調度的開銷過大,而對於一些線程數量受限的平台,以上的方法甚至都無法實現,所以總的來看,簡單延伸遞迴來實現並行的想法並不十分“靠譜”……

  但是如果我們擴充思路,並不通過資料分解,而是通過任務分解來看待快排問題的話,那麼快排的並行實現就會變的相對明晰,而這個任務分解,其實就是上面快排“迴圈”實現的一個延伸:

struct SortParam{    int* a;    int l;    int r;    SortParam(int* a_, int l_, int r_):a(a_),l(l_),r(r_) {};    SortParam(const SortParam& sp):a(sp.a),l(sp.l),r(sp.r) {};    SortParam& operator = (const SortParam& sp)    {        a = sp.a;        l = sp.l;        r = sp.r;        return *this;    }};queue<SortParam> g_queue;LONG g_count = 0;bool g_done = false;HANDLE g_sem = NULL;HANDLE g_signal = NULL;HANDLE g_mutex = NULL;//! quick sort thread function, param is array numberunsigned __stdcall QuickSort(LPVOID pArg){    int N = *((int*)pArg);    while (true)    {        WaitForSingleObject(g_sem, INFINITE);        if (g_done) break;        WaitForSingleObject(g_mutex, INFINITE);        SortParam sp = g_queue.front();        g_queue.pop();        ReleaseMutex(g_mutex);        int* a = sp.a;        int l = sp.l;        int r = sp.r;        if (l < r)        {            int p = Partition(a, l, r);            // NOTE: since every partiton will cause pivot set, so we increment sort count here            InterlockedIncrement(&g_count);            WaitForSingleObject(g_mutex, INFINITE);            g_queue.push(SortParam(a, l, p - 1));            g_queue.push(SortParam(a, p + 1, r));            ReleaseMutex(g_mutex);            ReleaseSemaphore(g_sem, 2, NULL);        }        else if (l == r) // single element, already sorted        {            LONG t = InterlockedIncrement(&g_count);            if (t == N) SetEvent(g_signal); // all sorted        }    }    return 0;}int main(){    int array[] = { 5, 4, 3, 2, 1, 7, 8, 9, 13, 0, 0, 2 };    int arrayNum = sizeof(array) / sizeof(arrayNum);    g_queue.push(SortParam(array, 0, arrayNum - 1));    g_sem = CreateSemaphore(NULL, 1, arrayNum, NULL);    g_signal = CreateEvent(NULL, true, false, NULL);    g_mutex = CreateMutex(NULL, false, NULL);    const int NUM_THREADS = 4;    for (int i = 0; i < NUM_THREADS; ++i)    {        _beginthreadex(NULL, 0, QuickSort, &arrayNum, 0, NULL);    }    WaitForSingleObject(g_signal, INFINITE);    g_done = true;    ReleaseSemaphore(g_sem, NUM_THREADS, NULL);    // NOTE: now we just sleep for a while to wait for threads quit    // TODO: join all    Sleep(100);    CloseHandle(g_sem);    CloseHandle(g_signal);    CloseHandle(g_mutex);    return 0;}

  基本思路便是通過訊號量來擷取可能存在的待排序的資料分段(即任務),並通過一個原子操作控制的計數器(g_count)來檢測是否排序完畢,並退出相應排序線程,實際開發時甚至可以引入線程池來最佳化實現,當然,其中涉及的細節還有不少,有興趣的朋友可以找來原書來看 :)

  網上同樣也有很多優秀的代碼範例,雖說其中所列的大部分語言我本人都未曾聽說過(稍囧),但是對於那些自己稍有瞭解的語言,範例所給的源碼還是非常不錯的,其中的一個Python實現令人印象深刻:

def qsort(L):    return (qsort([y for y in L[1:] if y <  L[0]]) + L[:1] + qsort([y for y in L[1:] if y >= L[0]])) if len(L) > 1 else L

  OK,That's It :)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.