排序(上)——為什麼插入排序比氣泡排序更受歡迎?
本文是學習演算法的筆記,《資料結構與演算法之美》,極客時間的課程
排序對於每個程式設計師來說,可能都不會陌生。平常的專案中,也經常遇到排序。按時間複雜度,分成三類,分三節來說
這裡,先丟擲一個問題。插入排序和氣泡排序的時間複雜度都是O(n2),可實際開發中,為什麼我們更傾向於使用插入排序演算法呢?
評價一個排序演算法,
從效率角度說,要考慮各種時間複雜度;資料量小時,時間複復雜度的係數、常數也要考慮;元素比較和交換的次數。
從記憶體消耗角度說,要考慮演算法的空間複雜度。空間複雜度是o(1)的排序演算法,叫原地排序演算法(Sorted in place)。今天說的三種演算法都是原地排序演算法。
從演算法穩定性來說,排序演算法又分為穩定性演算法和非穩定性演算法。穩定性,指相等元素,在排序後,原來的先後順序不改變。
穩定性演算法相對於不穩定性演算法,有什麼特別的用麼,反正是等值,誰先誰後,不都一個樣子麼?實際業務中,可能會按多個屬性排序。比如這麼一個場景:有一批訂單,按日期降序排列,如果日期一樣的,按訂單金額降序。
這個需要很好理解,實現起來,可能就沒那麼容易,按日期排好了,再把日期一樣的按訂單金額排一次。這個就不好實現,先把日期一樣的分別取出來排下,再對應放回去,很麻煩。
有了穩定性排序演算法,就很容易實現。第一步,先按訂單金額降序排列,第二步,再把第一步得到的結果,按日期降序重新排列一次,就可以實現需求了。為什麼自己琢磨一下吧。
接著,我們詳細說下這三種排序演算法,我會分別給出程式碼實現(java),簡要說下複雜度,還有演算法的評價等等。
氣泡排序演算法
基本的邏輯是,取第一個元素與後一個比較,如果大於後者,就與後者互換位置,不大於,就保持位置不變。再拿第二個元素與後者比較,如果大於後者,就與後者互換位置。一輪比較之後,最大的元素就移動到末尾。相當於最大的就冒出來了。再進行第二輪,第三輪,直到排序完畢。
程式碼實現,我們會做一些優化,如果是n個元素,第一輪比較(n-1)次,第二輪,比較(n-2)次就可以了。直到剩餘兩個元素,比較1次,排序操作就結束了。再進一步優化,如果,在某一輪操作中,只有比較操作,而沒有資料移動操作,那說明已經完全有序,不需要再進行下一輪了,直接結束即可。
/**
* 氣泡排序
*
* @param a
* @return
*/
public int[] bubbleSort(int[] a) {
int n = a.length;
if (n <= 1) {
return a;
}
for (int i = 1; i < n; i++) {
boolean flag = false; // 開關,當某次內層迴圈,沒有資料交換時,已排好順序,直接跳出迴圈。
for (int j = 0; j < n - i; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
flag = true;
}
}
if (!flag) {
break;
}
}
return a;
}
分析氣泡排序的時間複雜度,最好情況時間複雜度是O(n)。最好的情況本身就是有序的,比較了一輪,一次冒泡,不需要移動元素,排序完成。
最壞情況時間複雜度是O(n2)。最壞情況剛好是反序的(結合本例,就是倒序),要進行(n-1)輪的比較,每輪比較都要進行(n-1)次的位置移動。
平均情況時間複雜度也是為O(n2)。這個用加權平均算概率有點複雜。理論是,n個元素,排列方式就在有 n! 種,每種情況下,比較多少輪,每次多少資料移動都不一樣。有一點是確定的,上限是O(n2),下限是o(n)。
這裡,我們引入一個簡化的比較方式。
有序度:滿足 a[m] > a[n],且 m > n 的一對數,
逆序度:滿足 a[m] > a[n],且 m < n 的一對數,
滿序度:有序度的最大值(或逆序度的最大值)。潢序度 = 有序度 + 逆序度
對於氣泡排序,冒泡一次,有序度至少增加一,當達到滿度時,排序就結束。在 n! 種排列中,有序度最小值是0,最大值是(n-1)*n/2,平均值就是(n-1)*n/4。有序度可以評價氣泡排序的比較操作,移動元素的操作肯定比比較的操作少,那氣泡排序的平均情況時間複雜度是 (n-1)*n/4 至 最壞情況時間複雜度之間的值,不考慮係數,低階,得O(n2)。
氣泡排序的空間複雜度是O(1),資料移動需要一個臨時變數,屬於常量級別。
氣泡排序是穩定性演算法,從實現的程式碼,可以推知,當相同元素比對時,不會進行資料移動。
插入排序演算法
基本邏輯是,把元素分為已排序的和未排序的。每次從未排序的元素取出第一個,與已排序的元素從尾到頭逐一比較,找到插入點,將之後的元素都往後移一位,騰出位置給該元素。
/**
* 插入排序
*
* @param a
* @return
*/
public int[] insertSort(int[] a) {
int n = a.length;
if (n <= 1) {
return a;
}
for (int i = 1; i < n; i++) {
int temp = a[i];
int j = i - 1;
for (; j >= 0; j--) {
if (a[j] > temp) {
a[j + 1] = a[j]; // 比temp 大的已排序資料後移一位
} else {
break;
}
}
a[j + 1] = temp; // 空出來的位置,把temp放進去
}
return a;
}
插入排序,最好情況時間複雜度,如果已經是一個有序陣列了,就不需要移動資料。只要查詢到插入位置即可,每次只需要一次比較就可以找到插入位置,所以,這種情況下,最好情況時間複雜度為O(n)。
如果陣列是倒序的,每次插入都相當於在陣列的第一個位置插入資料,有大量移動資料的操作,所以,最壞情況時間複雜度是O(n2)。
陣列中插入一個數據平均時間複雜度是O(n),要進行n次操作,所以,平均情況時間複雜度是O(n2)。
空間複雜度是O(1)。也是一種穩定性演算法。
選擇排序演算法
基本邏輯是:把所有資料分為已排序區間和未排序區間。每次從未排序區間中,選出最小值,之後將該值與未排序區間第一個元素互換位置,此時已排序區間元素個數多了一個,未排序區間內的元素少了一個。如此迴圈直到未排序區間沒有元素為止。
/**
* 選擇排序
*
* @param a
* @return
*/
public int[] selectionSort(int[] a) {
int n = a.length;
if (n <= 1) {
return a;
}
for (int i =1; i < n; i++) {
int j = i-1;
int min = a[j]; // 最小的數值
int index = j; // 最小值對應的下標
for (; j < n-1 ; j++) {
if (min > a[j+1]) {
min = a[j+1];
index = j+1;
}
}
//最小值a[index]與放未排序的首位a[i-1]互換位置
if (index != i-1) {
int temp = a[i-1];
a[i-1] = a [index];
a [index] = temp;
}
}
return a;
}
選擇排序演算法,最好情況時間複雜度與最壞情況時間複雜度,都是O(n2),空間複雜度是O(1),它是不穩定的演算法。相同元素,排序後,相對位置可能發生改變。
比較下三種演算法的效率,我在測試類中,分別取800,8000,80000個元素進行排序,結果如下
@Test
public void testParseResult() {
Random rand = new Random();
int length = 800;
int[] a = new int[length];
int[] b = new int[length];
int[] c = new int[length];
for (int i = 0; i < length; i++) {
a[i] = rand.nextInt(100);
b[i] = a[i];
c[i] = a[i];
// System.out.print(" " + b[i]);
// if (i % 50 == 49) {
// System.out.println();
// }
}
long one = System.currentTimeMillis();
insertSort(a);
long two = System.currentTimeMillis();
bubbleSort(b);
long three = System.currentTimeMillis();
selectionSort(c);
long four = System.currentTimeMillis();
System.out.println(" ");
System.out.println("陣列長度值為:" + length);
System.out.println("插入排序用時(單位毫秒):" + (two - one));
System.out.println("氣泡排序用時(單位毫秒):" + (three - two));
System.out.println("選擇排序用時(單位毫秒):" + (four - three));
// System.out.println();
// for (int i = 0; i < length; i++) {
// System.out.print(" " + a[i]);
// if (i % 10 == 9) {
// System.out.println();
// }
// }
}
陣列長度值為:800
插入排序用時(單位毫秒):3
氣泡排序用時(單位毫秒):5
選擇排序用時(單位毫秒):2
陣列長度值為:8000
插入排序用時(單位毫秒):56
氣泡排序用時(單位毫秒):148
選擇排序用時(單位毫秒):24
陣列長度值為:80000
插入排序用時(單位毫秒):2385
氣泡排序用時(單位毫秒):11067
選擇排序用時(單位毫秒):1163
現在說說文章開頭的問題,同為穩定性排序演算法,時間複雜度也一樣,插入排序的效率比氣泡排序要好,尤其是資料量大的時候,差距更明顯。為什麼?
氣泡排序移動資料的操作更多,只要是小於後一個元素,就移動一次。所以它的效率低。看測試結果,八萬的時候,就是數量級的差距了。