簡介 排序演算法是我們編程中遇到的最多的演算法。目前主流的演算法有8種。 平均時間複雜度從高到低依次是: 冒泡排序(o(n2)),選擇排序(o(n2)),插入排序(o(n2)),堆排序(o(nlogn)), 歸併排序(o(nlogn)),快速排序(o(nlogn)), 希爾排序(o(n1.25)),基數排 ...
簡介
排序演算法是我們編程中遇到的最多的演算法。目前主流的演算法有8種。
平均時間複雜度從高到低依次是:
冒泡排序(o(n2)),選擇排序(o(n2)),插入排序(o(n2)),堆排序(o(nlogn)),
歸併排序(o(nlogn)),快速排序(o(nlogn)), 希爾排序(o(n1.25)),基數排序(o(n))
這些平均時間複雜度是參照維基百科排序演算法羅列的。
是計算的理論平均值,並不意味著你的代碼實現能達到這樣的程度。
例如希爾排序,時間複雜度是由選擇的步長決定的。基數排序時間複雜度最小,
但我實現的基數排序的速度並不是最快的,後面的結果測試圖可以看到。
本文代碼實現使用的數據源類型為IList<int>,這樣可以相容int[]和List<int>(雖然int[]有ToList(),
List<int>有ToArray(),哈哈!)。
選擇排序
選擇排序是我覺得最簡單暴力的排序方式了。
以前剛接觸排序演算法的時候,感覺演算法太多搞不清,唯獨記得選擇排序的做法及實現。
原理:找出參與排序的數組最大值,放到末尾(或找到最小值放到開頭) 維基入口
實現如下:
public static void SelectSort(IList<int> data) { for (int i = 0; i < data.Count - 1; i++) { int min = i; int temp = data[i]; for (int j = i + 1; j < data.Count; j++) { if (data[j] < temp) { min = j; temp = data[j]; } } if (min != i) Swap(data, min, i); } }
過程解析:將剩餘數組的最小數交換到開頭。
冒泡排序
冒泡排序是筆試面試經常考的內容,雖然它是這些演算法里排序速度最慢的(汗),後面有測試為證。
原理:從頭開始,每一個元素和它的下一個元素比較,如果它大,就將它與比較的元素交換,否則不動。
這意味著,大的元素總是在向後慢慢移動直到遇到比它更大的元素。所以每一輪交換完成都能將最大值
冒到最後。 維基入口
實現如下:
public static void BubbleSort(IList<int> data) { for (int i = data.Count - 1; i > 0; i--) { for (int j = 0; j < i; j++) { if (data[j] > data[j + 1]) Swap(data, j, j + 1); } } }
過程解析:中需要註意的是j<i,每輪冒完泡必然會將最大值排到數組末尾,所以需要排序的數應該是在減少的。
很多網上版本每輪冒完泡後依然還是將所有的數進行第二輪冒泡即j<data.Count-1,這樣會增加比較次數。
通過標識提升冒泡排序
在維基上看到,可以通過添加標識來分辨剩餘的數是否已經有序來減少比較次數。感覺很有意思,可以試試。
實現如下:
public static void BubbleSortImprovedWithFlag(IList<int> data) { bool flag; for (int i = data.Count - 1; i > 0; i--) { flag = true; for (int j = 0; j < i; j++) { if (data[j] > data[j + 1]) { Swap(data, j, j + 1); flag = false; } } if (flag) break; } }
過程解析:發現某輪冒泡沒有任何數進行交換(即已經有序),就跳出排序。
我起初也以為這個方法是應該有不錯效果的,可是實際測試結果並不如想的那樣。和未優化耗費時間一樣(對於隨機數列)。
由果推因,那麼應該是冒泡排序對於隨機數列,當剩餘數列有序的時候,也沒幾個數要排列了!?
不過如果已經是有序數列或者部分有序的話,這個冒泡方法將會提升很大速度。
雞尾酒排序(來回排序)
對冒泡排序進行更大的優化
冒泡排序只是單向冒泡,而雞尾酒來回反覆雙向冒泡。
原理:自左向右將大數冒到末尾,然後將剩餘數列再自右向左將小數冒到開頭,如此迴圈往複。維基入口
實現如下:
public static void BubbleCocktailSort(IList<int> data) { bool flag; int m = 0, n = 0; for (int i = data.Count - 1; i > 0; i--) { flag = true; if (i % 2 == 0) { for (int j = n; j < data.Count - 1 - m; j++) { if (data[j] > data[j + 1]) { Swap(data, j, j + 1); flag = false; } } if (flag) break; m++; } else { for (int k = data.Count - 1 - m; k > n; k--) { if (data[k] < data[k - 1]) { Swap(data, k, k - 1); flag = false; } } if (flag) break; n++; } } }
過程解析:分析第i輪冒泡,i是偶數則將剩餘數列最大值向右冒泡至末尾,是奇數則將剩餘數列最小值
向左冒泡至開頭。對於剩餘數列,n為始,data.Count-1-m為末。
來回冒泡比單向冒泡:對於隨機數列,更容易得到有序的剩餘數列。因此這裡使用標識將會提升的更加明顯。
插入排序
插入排序是一種對於有序數列高效的排序。非常聰明的排序。只是對於隨機數列,效率一般,交換的頻率高。
原理:通過構建有序數列,將未排序的數從後向前比較,找到合適位置並插入。維基入口
第一個數當作有序數列。
實現如下:
public static void InsertSort(IList<int> data) { int temp; for (int i = 1; i < data.Count; i++) { temp = data[i]; for (int j = i - 1; j >= 0; j--) { if (data[j] > temp) { data[j + 1] = data[j]; if (j == 0) { data[0] = temp; break; } } else { data[j + 1] = temp; break; } } } }
過程解析:將要排序的數(索引為i)存儲起來,向前查找合適位置j+1,將i-1到j+1的元素依次向後
移動一位,空出j+1,然後將之前存儲的值放在這個位置。
這個方法寫的不如維基上的簡潔清晰,由於合適位置是j+1所以多出了對j==0的判斷,但實際效率影響無差別。
建議比照維基和我寫的排序,自行選擇。
二分查找法優化插入排序
插入排序主要工作是在有序的數列中對要排序的數查找合適的位置,而查找裡面經典的二分查找法正可以適用。
原理:通過二分查找法的方式找到一個位置索引。當要排序的數插入這個位置時,大於前一個數,小於後一個數。
實現如下:
public static void InsertSortImprovedWithBinarySearch(IList<int> data) { int temp; int tempIndex; for (int i = 1; i < data.Count; i++) { temp = data[i]; tempIndex = BinarySearchForInsertSort(data, 0, i, i); for (int j = i - 1; j >= tempIndex; j--) { data[j + 1] = data[j]; } data[tempIndex] = temp; } } public static int BinarySearchForInsertSort(IList<int> data, int low, int high, int key) { if (low >= data.Count - 1) return data.Count - 1; if (high <= 0) return 0; int mid = (low + high) / 2; if (mid == key) return mid; if (data[key] > data[mid]) { if (data[key] < data[mid + 1]) return mid + 1; return BinarySearchForInsertSort(data, mid + 1, high, key); } else // data[key] <= data[mid] { if (mid - 1 < 0) return 0; if (data[key] > data[mid - 1]) return mid; return BinarySearchForInsertSort(data, low, mid - 1, key); } }
過程解析:需要註意的是二分查找方法實現中high-low==1的時候mid==low,所以需要33行
mid-1<0即mid==0的判斷,否則下行會索引越界。
快速排序
快速排序是一種有效比較較多的高效排序。它包含了“分而治之”以及“哨兵”的思想。
原理:從數列中挑選一個數作為“哨兵”,使比它小的放在它的左側,比它大的放在它的右側。將要排序是數列遞歸地分割到
最小數列,每次都讓分割出的數列符合“哨兵”的規則,自然就將數列變得有序。 維基入口
實現如下:
public static void QuickSortStrict(IList<int> data) { QuickSortStrict(data, 0, data.Count - 1); } public static void QuickSortStrict(IList<int> data, int low, int high) { if (low >= high) return; int temp = data[low]; int i = low + 1, j = high; while (true) { while (data[j] > temp) j--; while (data[i] < temp && i < j) i++; if (i >= j) break; Swap(data, i, j); i++; j--; } if (j != low) Swap(data, low, j); QuickSortStrict(data, j + 1, high); QuickSortStrict(data, low, j - 1); }
過程解析:取的哨兵是數列的第一個值,然後從第二個和末尾同時查找,左側要顯示的是小於哨兵的值,
所以要找到不小於的i,右側要顯示的是大於哨兵的值,所以要找到不大於的j。將找到的i和j的數交換,
這樣可以減少交換次數。i>=j時,數列全部查找了一遍,而不符合條件j必然是在小的那一邊,而哨兵
是第一個數,位置本應是小於自己的數。所以將哨兵與j交換,使符合“哨兵”的規則。
這個版本的缺點在於如果是有序數列排序的話,遞歸次數會很可怕的。
另一個版本
這是維基上的一個C#版本,我覺得很有意思。這個版本並沒有嚴格符合“哨兵”的規則。但卻將“分而治之”
以及“哨兵”思想融入其中,代碼簡潔。
實現如下:
public static void QuickSortRelax(IList<int> data) { QuickSortRelax(data, 0, data.Count - 1); } public static void QuickSortRelax(IList<int> data, int low, int high) { if (low >= high) return; int temp = data[(low + high) / 2]; int i = low - 1, j = high + 1; while (true) { while (data[++i] < temp) ; while (data[--j] > temp) ; if (i >= j) break; Swap(data, i, j); } QuickSortRelax(data, j + 1, high); QuickSortRelax(data, low, i - 1); }
過程解析:取的哨兵是數列中間的數。將數列分成兩波,左側小於等於哨兵,右側大於等於哨兵。
也就是說,哨兵不一定處於兩波數的中間。雖然哨兵不在中間,但不妨礙“哨兵”的思想的實現。所以
這個實現也可以達到快速排序的效果。但卻造成了每次遞歸完成,要排序的數列數總和沒有減少(除非i==j)。
針對這個版本的缺點,我進行了優化
實現如下:
public static void QuickSortRelaxImproved(IList<int> data) { QuickSortRelaxImproved(data, 0, data.Count - 1); } public static void QuickSortRelaxImproved(IList<int> data, int low, int high) { if (low >= high) return; int temp = data[(low + high) / 2]; int i = low - 1, j = high + 1; int index = (low + high) / 2; while (true) { while (data[++i] < temp) ; while (data[--j] > temp) ; if (i >= j) break; Swap(data, i, j); if (i == index) index = j; else if (j == index) index = i; } if (j == i) { QuickSortRelaxImproved(data, j + 1, high); QuickSortRelaxImproved(data, low, i - 1); } else //i-j==1 { if (index >= i) { if (index != i) Swap(data, index, i); QuickSortRelaxImproved(data, i + 1, high); QuickSortRelaxImproved(data, low, i - 1); } else //index < i { if (index != j) Swap(data, index, j); QuickSortRelaxImproved(data, j + 1, high); QuickSortRelaxImproved(data, low, j - 1); } } }
public static void QuickSortRelaxImproved(IList<int> data) { QuickSortRelaxImproved(data, 0, data.Count - 1); } public static void QuickSortRelaxImproved(IList<int> data, int low, int high) { if (low >= high) return; int temp = data[(low + high) / 2]; int i = low - 1, j = high + 1; int index = (low + high) / 2; while (true) { while (data[++i] < temp) ; while (data[--j] > temp) ; if (i >= j) break; Swap(data, i, j); if (i == index) index = j; else if (j == index) index = i; } if (j == i) { QuickSortRelaxImproved(data, j + 1, high); QuickSortRelaxImproved(data, low, i - 1); } else //i-j==1 { if (index >= i) { if (index != i) Swap(data, index, i); QuickSortRelaxImproved(data, i + 1, high); QuickSortRelaxImproved(data, low, i - 1); } else //index < i { if (index != j) Swap(data, index, j); QuickSortRelaxImproved(data, j + 1, high); QuickSortRelaxImproved(data, low, j - 1); } } }
過程解析:定義了一個變數Index,來跟蹤哨兵的位置。發現哨兵最後在小於自己的那堆,
那就與j交換,否則與i交換。達到每次遞歸都能減少要排序的數列數總和的目的。