關於排序,我想沒有多少程式員會感覺陌生,信手拈來的可能就有不少:冒泡、選擇或者歸併等等;可在實際開發中,排序又讓許多程式員感到很不熟悉,因為在大多數情況下,自己實現一個排序演算法都不是一個好主意,相反的,改而使用語言架構內建提供的排序功能才是明智之舉,畢竟她又方便又高效……
久而久之,“排序”便與許多程式員成了“最熟悉的陌生人”,成了我們生活中司空見慣的“工具”:想要實現數組排序?簡單,一個 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 :)