這裡討論的僅限於內部排序(即全部資料都在記憶體中,通過CPU運算處理元素排序),而且僅限順序表排序(即不討論鏈表,樹狀結構等結構的排序)
註:排序後的結果可以從小到大,或者從大到小,這隻是一個相反的處理而已,為方便起見,本文中的方法都是從小到大排序
1、直接插入排序(InsertOrder)
思路:從第二個元素開始向後遍曆,檢查本身(後面稱之為tmp)與前面相鄰元素的大小,如果發現前面的元素更大,則依次從近及遠(即倒序遍曆)檢查前面的所有元素,將比自身元素大的元素依次後移,這樣最終將得到一個空位,將tmp元素插在這個位置即可.
/// <summary> /// 直接插入排序法 /// </summary> /// <param name="lst"></param> static void InsertSort(int[] lst) { int _circleCount = 0; //外迴圈從第二個元素開始從前向後遍曆 for (int i = 1; i < lst.Length; i++) { _circleCount++; //Console.WriteLine("外迴圈i=" + i); //如果發現某元素比前一個元素小 if (lst[i] < lst[i - 1]) { int tmp = lst[i]; int j = 0; //則該元素前面的元素,從後到前遍曆,依次後移,直接找到應該插入的空檔在哪裡(這樣tmp元素就找到了自己的位置) for (j = i - 1; j >= 0 && tmp < lst[j]; j--) { //如果發現有比tmp小的元素,則將元素後移一位(從而把自身空出來,形成一個空檔,以方便如果前面還有更小的元素時,可以繼續向後面的空檔移動) lst[j + 1] = lst[j]; _circleCount++; //Console.WriteLine("內迴圈i=" + i + ",內迴圈j=" + j); } //Console.WriteLine("j={0}", j); //運行到這裡時,j已經是空檔的前一個下標 lst[j + 1] = tmp; } } Console.WriteLine("InsertOrder共迴圈了{0}次", _circleCount); }
點評:最好情況下,如果所有元素(N個)已經排好序了,則外迴圈跑N-1次,內迴圈一次也進不了,即0次,時間複雜度為O(N);最壞情況下,所有元素反序,外迴圈N-1次,內迴圈為i(i從1到N-1),時間複雜度為O(N*N);所以元素越有序列,該方法效率越高,其時間複雜度從O(N)到O(N*N)之間,此外,該方法是一種穩定排序。(註:若數組中有相同值的元素時,經過某方法排序後,這二個相同值的元素先後順序仍然不變,則稱這種排序方法為穩定的,反之為不穩定排序方法)
2、冒泡排序法(BubbleSort)
思路:從最後一個元素開始向前遍曆,依次檢查本元素與前面相鄰元素的大小,如果前面的元素更大,則交換位置,如此反覆,直到把自己前移到合適的位置(即 相當於後面的元素,通過這種比較,按照從小到大將不斷移動前面來,就象氣泡從下面向上冒一樣)
/// <summary> /// 冒泡排序法 /// </summary> /// <param name="lst"></param> static void BubbleSort(int[] lst) { int _circleCount = 0;//輔助用,可以去掉 int tmp; for (int i = 0; i < lst.Length; i++) { for (int j = lst.Length - 2; j >= i; j--) { if (lst[j + 1] < lst[j]) { tmp = lst[j + 1]; lst[j + 1] = lst[j]; lst[j] = tmp; } _circleCount++; } } Console.WriteLine("BubbleOrder共迴圈了{0}次", _circleCount); }
點評:與插入排序法類似,最好情況是所有元素已經排好序,這樣只跑外迴圈,內迴圈因為if判斷不成立,直接退出;最壞情況是所有元素反序,外迴圈和內迴圈每次都要處理,因此時間複雜度跟插入排序法完全相同,同樣這也是一種穩定排序。
3、簡單選擇排序法 (SimpleSelectOrder)
思路:先掃描整個數組,找出最小的元素,然後跟第一個元素交換(這樣,第一個位置的元素就排好了),然後從第二個元素開始繼續掃描,找到第二小的元素,跟第二個元素交換(這樣,第二個位置的元素也排好了)...如此反覆
/// <summary> /// 簡單選擇排序法 /// </summary> /// <param name="lst"></param> static void SimpleSelectSort(int[] lst) { int _circleCount = 0;//輔助用 int tmp = 0; int t = 0; for (int i = 0; i < lst.Length; i++) { t = i; //內迴圈,找出最小的元素下標 for (int j = i + 1; j < lst.Length; j++) { if (lst[t] > lst[j]) { t = j; } _circleCount++; } //將最小元素[下標為t]與元素i互換 tmp = lst[i]; lst[i] = lst[t]; lst[t] = tmp; } Console.WriteLine("SimpleSelectSort共迴圈了{0}次", _circleCount); }
點評:跟冒泡法很類似,不過應該注意到,這裡的元素交換操作是在內迴圈外,即不管如何這個交換操作是省不了的,所以其時間複雜度均為O(N*N),同樣這也是一個穩定排序。
4、快速排序(QuickOrder)
思路:以數組中間的元素做為分界線(該元素稱為支點),掃描其它元素,比支點小的放在左側,比支點大的放在右側,這樣就把數組分成了二段(即做了一次粗放的大致排序),然後對每一段做同樣的處理(即二段變四段,4段變8段...),直到最後每一段只有一個元素為止(沒錯,該方法是一個遞迴調用)
/// <summary> /// 快速排序 /// </summary> /// <param name="arr">待排序數組</param> /// <param name="left">數組第一個元素索引Index</param> /// <param name="right">數組最後一個元素索引Index</param> static void QuickSort(int[] arr, int left, int right) { //左邊索引小於右邊,則還未排序完成 if (left < right) { //取中間的元素作為比較基準,小於他的往左邊移,大於他的往右邊移 int middle = arr[(left + right) / 2]; //因為while中要做++與--的操作,所以這裡先將i,j各自換外擴張一位 int i = left - 1; int j = right + 1; while (true) { while (arr[++i] < middle) ;//如果前半部的元素值本身就比支點小,則直接跳過 while (arr[--j] > middle) ;//如果後半部的元素值本身就比支點大,則直接跳過 //因為前半段是向後遍曆,而後半段是向前遍曆,所以如果二者碰到了, //則說明所有元素都被掃過了一遍,完成退出 if (i >= j) { break; } //經過前面的處理後,如果發現有放錯位置的元素,則將二者對換 int tmp = arr[i]; arr[j] = arr[i]; arr[i] = tmp; } //經過上面的while迴圈後,元素已被分成左右二段(左段小於支點,右段大於支點) //遞迴調用,處理左段 QuickSort(arr, left, i - 1); //遞迴調用,處理右段 QuickSort(arr, j + 1, right); } }
點評:每次從中間分成二段,然後再中分為二段,如此反覆...這跟二叉樹很相似(每次分段,相當於樹中的某個節點分成二叉),最好情況下所有元素已經排好序,最終樹的左右分支數大致相同(即左右分支長度大致相同),所以分解次數為樹的高度log2N,而最壞情況下,所有元素反序,這時分解得到的樹相當於一個單右支二叉樹(即一個右分支超級長,沒有左分支的怪樹),即時間複雜度範圍為nLog2N 至 N*N。此外,快速排序是一種不穩定的排序(從代碼就能看出來,即使是二個相同值的節點,在分段過程中,也有可能被交換順序)
本來想將堆排序與歸併排序一起寫在這篇文章裡的,今天看了看堆排序,還有點小複雜,完全可以另起一篇詳解原理了,下篇將專門學習堆排序及歸併排序。