提起排序,與我們的息息相關,平時開發的程式碼少不了排序。
經典的排序演算法又非常多,我們怎麼評價一個排序演算法的好壞呢?
其實可以這樣想,要細緻的比較排序演算法好壞,那我們就從多方面儘可能詳細的對比
一、效率方面
1、排序演算法的執行效率:最好、最壞、平均
2、 我們之前捨棄的時間複雜度的係數、常量、低階,在這裡需要拿回來
3、排序,免不了比較和移動
二、記憶體消耗方面
沒錯就是 演算法的空間複雜度,不過對於排序的空間複雜度來說,又賦予了新的名詞 — 原地排序。
顧名思義是 原地排序的肯定是消耗記憶體少,反之需要往外走幾步那就需要臨時申請記憶體了。
原地排序 = O(1)
三、演算法穩定性
字面意義就是不論怎麼擺弄,這個演算法穩定,不會對順序有影響。
上面這句話應該加上一個定語:對於擁有相同值的元素的前後順序不會發生改變。
舉個例子:有兩個物件,其中的金額欄位一樣,按照金額排序,經過演算法一頓折騰後,相同金額的物件先後順序不能發生改變。
講完評估排序演算法的優劣的幾個方面,那就直接看看我們平時常見的幾個經典演算法:
1、氣泡排序
圖例演示
> C#
1 //排序 — 氣泡排序 2 private static void BubbleSort(int[] source) 3 { 4 if (source.Length <= 1) 5 return; 6 7 bool isChanged = false; 8 for (int i = 0; i < source.Length; i++) 9 { 10 for (int j = 0; j < source.Length - i - 1; j++) 11 { 12 var left = source[j]; 13 var right = source[j + 1]; 14 Console.WriteLine("【比較】"); 15 if (left <= right) 16 continue; 17 18 source[j] = right; 19 source[j + 1] = left; 20 isChanged = true; 21 Console.WriteLine("{交換}"); 22 } 23 if (!isChanged) 24 break; 25 } 26 Printf(source); 27 }
Q:氣泡排序的時間演算法複雜度
A:最壞時間複雜度 — O(n^2):迴圈 n*n次
最好時間複雜度 — O(n) :迴圈 n次即可
平均時間複雜度 — O(?)
這裡我們使用概率來分析平均複雜度,情況比較複雜。
我們使用一種新的概念來分析平均複雜度,這個就是 有序度。
有序度:看作是向量,左<= 右
逆序度:正好相反,左 >= 右
滿有序度 = n*(n-1) / 2
逆序度 = 滿有序度 - 有序度
對於 n 個資料來說,最壞情況時間複雜度的有序度是0,要交換 n*(n-1)/2次才能正確輸出。
對於最好情況複雜度的有序度是n*(n-1)/2,需要交換0次就能達到完全有序。
最壞 n*(n-1)/2次,最好0次,取箇中間值來表示中間情況,也可以看作是平均情況 n*(n-1) /4
所以平均下來 要做 n*(n-1) / 4 次才能有序,因為氣泡排序的時間複雜度的上限是 O(n^2)
所以平均情況時間複雜度為 O(n^2)
雖然這樣推論平均個情況並不嚴格,但是比起概率推論來說,這樣簡單且有效。
Q:氣泡排序是不是原地排序
A:是,臨時變數為了交換資料,常量級別的臨時空間申請,所以空間複雜度為O(1)
Q:氣泡排序是不是穩定排序
A:是,因為沒有改變相同元素的先後順序。
2、插入排序
假定,我們將排序串分為兩個區:已排序區,未排序區
一個元素要找到正確的的位置進行插入,那麼需要去已排序區域找到自己的位置後,
將這個位置的元素們向後移動,空出位置,然後新元素入坑。
從以上這個思路來看,插入排序也是涉及到了元素的比較和移動。
給我們一個無序陣列,哪塊是已排序區?哪裡是未排序區?
比如:9, 0, 1, 5, 2, 3, 6
初始時,9 就是已排序區域;
0開始去已排序區域挨個比較,即 i=1,0<9,9向後挪動,空出位置,0入坑;
1開始去 [ 0,9 ] 已排序區域比較,1 < 9,9向後移動騰位置,1入坑,1 > 0 無需操作;
依次重複以上操作,即可達成有序。
圖例演示
> C#
1 //排序 — 插入排序 2 private static void InsertionSort(int[] source) 3 { 4 if (source == null || source.Length <= 0) 5 return; 6 7 for (int i = 1; i < source.Length; i++) 8 {// 未排序區 9 var sorting = source[i]; 10 int j = i - 1; 11 12 for (; j >= 0; j--) 13 {// 已排序區 14 15 // 比較 16 if (sorting >= source[j]) 17 { 18 break; 19 } 20 21 // 後移 22 source[j + 1] = source[j]; 23 } 24 25 // 入坑 26 source[j + 1] = sorting; 27 } 28 Printf(source); 29 }
Q:插入排序的時間演算法複雜度
A:最壞時間複雜度 — O(n^2):完全倒序,迴圈n次,比較n次
最好時間複雜度 — O(n):完全有序,迴圈n次跳出
平均時間複雜度 — O(n^2):迴圈 n次資料,在一個陣列中插入資料的平均情況時間複雜度為O(n),所以是 O(n^2)
Q:插入排序是不是原地排序
A:是,沒有臨時變數申請,所以空間複雜度為O(1)
Q:插入排序是不是穩定排序
A:是, if (sorting >= source[j]) 這個判斷保證了相同元素的先後順序不變,
去掉等於號也可以發生改變。可以實現穩定排序所以說是穩定排序
開始我們也說了,這麼多排序演算法,我們要對比一下,擇優選擇。
排序 | 最好情況 | 最壞情況 | 平均情況 | 是否穩定 | 是否原地 |
冒泡 | O(n) | O(n^2) | O(n^2) | 是 | 是 |
插入 | O(n) | O(n^2) | O(n^2) | 是 | 是 |
那麼問題來了平均都是 O(n^2),為什麼傾向於使用插入排序呢?
這兩種排序我們將常量都放進來會發現,冒泡使用的常量數比排序多,所以在資料量上來後 常量*n 會有很大的差距。
我們的程式語言中的排序演算法很多都會傾向於插入排序演算法。
3、選擇排序
其實操作類似於插入排序,只不過是換了換操作方式。
所以也分為 已排序區和未排序區,操作方式是在未排序區間找到最小的,然後放到已排序區間最後。
圖例:
> C#
private static void SelectionSort(int[] source) { if (source.Length <= 1) return; for (int i = 0; i < source.Length - 1; i++) {// 已排序 var minIndex = i; for (int j = i+1; j < source.Length; j++) {//未排序 if (source[minIndex] > source[j]) { minIndex = j; } } if (i != minIndex) { int tmp = source[i]; source[i] = source[minIndex]; source[minIndex] = tmp; } } Printf(source); }
Q:選擇排序的時間演算法複雜度
A:最壞時間複雜度 — O(n^2)
最好時間複雜度 — O(n^2)
平均時間複雜度 — O(n^2)
Q:選擇排序是不是原地排序
A:是,沒有臨時變數申請,所以空間複雜度為O(1)
Q:選擇排序是不是穩定排序
A:不是
4、對比 隨機生成1000個元素的 int 陣列
分別執行時間如下: