Java 排序算法 - 為什么插入排序要比冒泡排序更受歡迎呢
數據結構與算法之美目錄(https://www.cnblogs.com/binarylei/p/10115867.html)
對於大多數程序員來說,我們學習的第一個算法,可能就是排序。大部分編程語言中,也都提供了排序函數。在平常的項目中,我們也經常會用到排序。排序算法太多了,有很多可能連名字都沒聽說過,比如猴子排序、睡眠排序、面條排序等。
這里我們只需要掌握最經典的、最常用的排序算法:冒泡排序、插入排序、選擇排序、歸並排序、快速排序、計數排序、基數排序、桶排序。我按照時間復雜度把它們分成了三類:
排序算法 | 時間復雜度 | 是否基於比較 |
---|---|---|
冒泡、選擇、插入 | O(n2) | √ |
歸並、快排 | O(nlogn) | √ |
桶、基數、計數 | O(n) | × |
本文重點分析時間復雜度為 O(n2) 的三種算法:冒泡、選擇、插入排序,其中重點需要掌握的是插入排序,其它兩種基本的算法在實際軟件開發中根本不會使用到。
1. 衡量排序算法的三個指標
1.1 時間復雜度
(1)最好情況、最壞情況、平均情況時間復雜度。
為什么要區分這三種時間復雜度呢?第一,有些排序算法會區分,為了好對比,所以我們最好都做一下區分。第二,對於要排序的數據,有的接近有序,有的完全無序。有序度不同的數據,對於排序的執行時間肯定是有影響的,我們要知道排序算法在不同數據下的性能表現。
(2)時間復雜度的系數、常數 、低階。
我們知道,時間復雜度反應的是數據規模 n 很大的時候的一個增長趨勢,所以它表示的時候會忽略系數、常數、低階。但是實際的軟件開發中,我們排序的可能是 10 個、100 個、1000 個這樣規模很小的數據,所以,在對同一階時間復雜度的排序算法性能對比的時候,我們就要把系數、常數、低階也考慮進來。
(3)比較次數和交換次數。
基於比較的排序算法的執行過程,會涉及兩種操作,一種是元素比較大小,另一種是元素交換或移動。所以,如果我們在分析排序算法的執行效率的時候,應該把比較次數和交換(或移動)次數也考慮進去。
1.2 空間復雜度
針對排序算法的空間復雜度,我們還引入了一個新的概念,原地排序(Sorted in place)。原地排序算法,就是特指空間復雜度是 O(1) 的排序算法。
1.3 穩定性
針對排序算法還有另一個重要指標:穩定性。穩定性是指,排序后相等元素之間原有的先后順序不變。
比如我們有一組數據 2,9,3,4,8,3,按照大小排序之后就是 2,3,3,4,8,9。這組數據里有兩個 3。經過某種排序算法排序之后,如果兩個 3 的前后順序沒有改變,那我們就把這種排序算法叫作穩定的排序算法;如果前后順序發生變化,那對應的排序算法就叫作不穩定的排序算法。
穩定的排序算法會什么用呢?比如我們需要針對訂單的金額和時間兩個維度排序(order by price, timestamp)。最先想到的方法是:我們先按照金額對訂單數據進行排序,然后,再遍歷排序之后的訂單數據,對於每個金額相同的小區間再按照下單時間排序。這種排序思路理解起來不難,但是實現起來會很復雜。
借助穩定排序算法,可以非常簡潔地解決多維度排序。解決思路是這樣的:我們先按照下單時間給訂單排序,再用穩定排序算法,按照訂單金額重新排序。兩遍排序之后,我們得到的訂單數據就是按照金額從小到大排序,金額相同的訂單按照下單時間從早到晚排序的。
1.4 常用排序算法比較
排序算法 | 時間復雜度 | 原地算法 | 穩定性 | 備注 |
---|---|---|---|---|
冒泡排序 | O(n2) | √ | √ | |
選擇排序 | O(n2) | √ | × | |
插入排序 | O(n2) | √ | √ | |
希爾排序 | O(n1.5) | × | 插入排序的優化 | |
歸並排序 | O(nlogn) | √ | ||
快速排序 | O(nlogn) | × | ||
桶排序 | O(n) | |||
基數排序 | O(n) | |||
計數排序 |
2. 冒泡排序
冒泡排序依次對相鄰的元素進行比較並交換,從而實現排序功能。冒泡排序是復雜度為 O(n2) 的原地穩定排序算法。事實上,實際項目中不會使用冒泡排序,冒泡排序僅僅停留在理論階段。
2.1 冒泡排序原理分析
冒泡排序重復地走訪過要排序的元素列,依次比較兩個相鄰的元素,如果他們的順序錯誤就把他們交換過來。走訪元素的工作是重復地進行直到沒有相鄰元素需要交換,也就是說該元素已經排序完成。
冒泡次數 | 冒泡結果 |
---|---|
原始數據(n = 6) | 3 8 6 5 2 1 |
第一輪第1次冒泡:3 vs 8 | 3 8 6 5 2 1 |
第一輪第2次冒泡:8 vs 6 | 3 6 8 5 2 1 |
第一輪第3次冒泡:8 vs 5 | 3 6 5 8 2 1 |
第一輪第4次冒泡:8 vs 2 | 3 6 5 2 8 1 |
第一輪第5次冒泡:8 vs 1 | 3 6 5 2 1 8 |
第二輪第1次冒泡:3 vs 6 | 3 6 5 2 1 8 |
第二輪第2次冒泡:6 vs 5 | 3 5 6 2 1 8 |
第二輪第3次冒泡:6 vs 2 | 3 5 2 6 1 8 |
第二輪第4次冒泡:6 vs 1 | 3 5 6 1 6 8 |
... | |
第 n - 1 輪 | 1 2 3 5 6 8 |
說明: 冒泡比較過程如下:
- 外層輪詢,相當於每輪詢一次都將最大的元素搬移到最后面。第一輪搬移最大元素到第 n - 1 個位置,第二輪搬移最大元素到第 n - 2 個位置,...,全部搬移完則需要搬移 n - 1 次。
- 內層輪詢,比較前后兩個元素大小,如果位置順序錯誤就交換位置。已經搬移過的最大值,則不再需要比較。
for (int i = 0; i < arr.length; i++) {
boolean swap = false; // 一次都沒有交換,說明完全有序,直接return
// 每次輪詢依次比較相鄰的兩個數,將最小的移到前面
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap = true;
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if (!swap) return;
}
2.2 冒泡排序三大指標分析
-
時間復雜度:數據完全有序,最好時間復雜度為 O(n)。數據完全逆序,最壞時間復雜度為 O(n2)。我們再思考一下平均復雜度的計算?這里引入有序度的概念。
- 有序度:數組中有序元素對的個數。如 5, 1, 2 中 (1, 2) 是有序的,有序度為 1。
- 逆序度:數據中無序元素對的個數。如 5, 1, 2 中 (5, 1) 和 (5, 2) 是無序的,逆序度為 2。
- 滿有序度:固定為 n(n - 1) / 2,且 滿有序度 = 有序度 + 逆序度。
冒泡排序中,一個逆序度就要進行一次交換,最好情況交換 0 次,最壞情況交換 n(n - 1) / 2 次,平均交換 n(n - 1) / 4 次。而比較操作肯定要比交換操作多,而復雜度的上限是 O(n2),所以平均情況下的時間復雜度就是 O(n2)。
-
空間復雜度:使用原來的數組進行排序,是原地排序算法,即 O(1)。
-
穩定性:冒泡排序可以控制相鄰元素值相等時不交換位置,也就是元素值相等時順序不變,因此是穩定算法。
3. 選擇排序
選擇排序相對於冒泡排序而言,每一輪只交換一次來提高排序效率。選擇排序是復雜度為 O(n2) 的原地非穩定排序算法。事實上,同冒泡排序一樣,實際項目中不會使用選擇排序,選擇排序僅僅停留在理論階段。
3.1 選擇排序原理分析
選擇排序同樣也基於比較算法實現的。和冒泡排序一樣,也分為兩層循環,外層循環每輪訓一次,就排序好一個數,因此外層同樣需要循環 n - 1 次。不同的是,外層循環每輪訓一次時只交換一次元素位置。
選擇排序 | 排序結果 |
---|---|
原始數據(n = 6) | 3 8 6 5 2 1 |
第一輪交換:8 vs 1 | 3 1 6 5 2 8 |
第二輪交換:6 vs 2 | 3 1 2 5 6 8 |
... | |
第 n - 1 輪 | 1 2 3 5 6 8 |
說明: 選擇排序先找出最大值才進行交換,不像冒泡排序都是相鄰元素比較后進行交換。但也正是因為選擇排序不是相鄰元素之間比較后交換,而是非相鄰元素間的交換,可能會導致相等的元素在排序后亂序,是非穩定排序。
public void sort(Integer[] arr) {
for (int i = 0; i < arr.length; i++) {
// 1. 比較找到最大值的位置,然后和最后一位進行交換
int maxIndex = 0;
int lastIndex = arr.length - i - 1;
for (int j = 1; j < arr.length - i; j++) {
if (arr[maxIndex] < arr[j]) {
maxIndex = j;
}
}
// 2. 交換
if (maxIndex != lastIndex) {
int tmpValue = arr[maxIndex];
arr[maxIndex] = arr[lastIndex];
arr[lastIndex] = tmpValue;
}
}
}
3.2 選擇排序三大指標分析
- 時間復雜度:最好情況時間復雜度、最壞情況和平均情況時間復雜度都為 O(n2)。
- 空間復雜度:使用原來的數組進行排序,是原地排序算法,即 O(1)。
- 穩定性:選擇排序是非相鄰元素間交換,也就可能導致元素值相等時順序變化,因此是非穩定算法。
4. 插入排序
插入排序原理在有序數組中插入一個元素仍是有序的。選擇排序是復雜度為 O(n2) 的原地穩定排序算法。插入排序是真正在項目中使用的排序算法,需要我們重點學習。
4.1 插入排序原理分析
插入排序將數組分為兩部分,有序部分和無序部分,依次遍歷無序部分將元素插入到有序數組中,最終達到有序。
冒泡次數 | 冒泡結果 |
---|---|
原始數據(n = 6) | 3 8 6 5 2 1 |
第一輪插入:將 a[1]=8 插入 a[0] | 3 8 6 5 2 1 |
第二輪插入:將 a[2]=6 插入 a[0] ~ a[1] | 3 6 8 5 2 1 |
... | |
第 n - 1 輪:將 a[n - 1]=1 插入 a[0] ~ a[n - 2] | 1 2 3 5 6 8 |
說明: 插入排序每次都將 將 a[n - 1]=1 插入有序數組 a[0] ~ a[n - 2] 中,保證插入后的數據仍是有序的。
public void sort(Integer[] arr) {
for (int i = 1; i < arr.length; i++) {
int tmp = arr[i];
int j;
for (j = i; j > 0 && arr[j - 1] > tmp; j--) {
arr[j] = arr[j - 1];
}
arr[j] = tmp;
}
}
4.2 插入排序三大指標分析
- 時間復雜度:數據完全有序,最好時間復雜度為 O(n)。數據完全逆序,最壞時間復雜度為 O(n2)。由於往數組中插入一個元素的平均復雜度是 O(n),因此插入排序的平均復雜度也是 O(n2)。
- 空間復雜度:使用原來的數組進行排序,是原地排序算法,即 O(1)。
- 穩定性:插入排序也是相鄰元素間交換,也就是元素值相等時順序不變,,因此是穩定算法。
5. 希爾排序
希爾排序是希爾(Donald Shell)於 1959 年提出的一種排序算法。希爾排序也是一種插入排序,它是簡單插入排序經過改進之后的一個更高效的版本,也稱為縮小增量排序,同時該算法是沖破 O(n2) 的第一批算法之一。
5.1 希爾排序原理分析
希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至 1 時,整個文件恰被分成一組,算法便終止。
當序列很長時,向前插入一次導致的移動操作可能非常大。如果原序列的無序程度較高,則能夠緩解這個問題,即小的數大致在前,大的數大致在后。

public void sort(Integer[] arr) {
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
int tmp = arr[i];
int j;
for (j = i; j >= gap && arr[j - gap] > tmp; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = tmp;
}
}
}
4.2 插入排序三大指標分析
- 時間復雜度:希爾排序時間復雜度分析很復雜,就不具體分析了。希爾排序中對於增量序列的選擇十分重要,直接影響到希爾排序的性能。我們上面選擇的增量序列 {n/2, (n/4) ... 1}(希爾增量),其最壞時間復雜度依然為 O(n2),一些經過優化的增量序列如 Hibbard 經過復雜證明可使得最壞時間復雜度為 O(n1.5)。
- 空間復雜度:實際使用的是插入排序,也是原地排序算法,即 O(1)。
- 穩定性:希爾排序增加了不同步長之間的排序,也就是非相鄰元素之間會交換,這樣也導致了元素值相等時順序會發生變化,,因此是不穩定算法。
6. 總結:為什么插入排序要比冒泡排序更受歡迎呢?
我們前面分析冒泡排序和插入排序的時候講到,冒泡排序不管怎么優化,元素交換的次數是一個固定值,是原始數據的逆序度。插入排序是同樣的,不管怎么優化,元素移動的次數也等於原始數據的逆序度。雖然插入排序和冒泡排序元素交換的最終次數都是一樣的,但:
- 冒泡排序元素交換比插入排序要復雜。插入排序只需要一次操作。
- 插入排序的時間復雜度更穩定。冒泡排序只有當數據完全有序時,時間復雜度才是 O(1),只要有一個逆序度則時間復雜度就變成 O(n2)。而插入排序在數據比較有序時更有優勢。
參考:
- 排序動畫演示:http://www.jsons.cn/sort/
每天用心記錄一點點。內容也許不重要,但習慣很重要!