資料結構與演算法——氣泡排序
版權宣告:本文為Heriam博主原創文章,遵循CC 4.0 BY-SA 版權協議,轉載請附上原文出處連結和本宣告。
原文連結:https://jiang-hao.com/articles/2020/algorithms-algorithms-bubble-sort.html
定義
氣泡排序(Bubble Sort),是一種電腦科學領域的較簡單的排序演算法
演算法原理
冒泡排序演算法的原理如下:
- 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
- 對每一對相鄰元素做同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。
- 針對所有的元素重複以上的步驟,除了最後一個。
- 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
演算法複雜度是 O(n^2),空間複雜度是常數 O(1)。但可以記錄一個不需要交換的位置,把最好情況的時間複雜度降到 O(n)。詳細可以參考下文優化部分的實現。
演算法實現
public static int[] bubble_sort_original(int[] nums) { int[] arr = Arrays.copyOf(nums, nums.length); int count = 0, swap_count = 0; for (int i = 0; i < arr.length-1; i++) { for (int j = 0; j < arr.length-1-i; j++) { count++; if (arr[j] > arr[j+1]) { int tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; swap_count++; } } } System.out.println("bubble_sort_original: run " + count + ", swap " + swap_count + ", isSorted: " + isSorted(arr)); //列印執行次數、交換次數,以及排序檢驗 return arr; }
助記碼
i∈[0,N-1) //迴圈N-1遍
j∈[0,N-1-i) //每遍迴圈要處理的無序部分
swap(j,j+1) //兩兩排序(升序/降序)
演算法優化
優化1:一輪遍歷未發生交換可提前結束
資料的順序排好之後,冒泡演算法仍然會繼續進行下一輪的比較,直到arr.length-1次,後面的比較沒有意義的。
設定標誌位flag,如果發生了交換flag設定為true;如果沒有交換就設定為false。
這樣當一輪比較結束後如果flag仍為false,即:這一輪沒有發生交換,說明資料的順序已經排好,沒有必要繼續進行下去。
public static int[] bubble_sort_quit_if_sorted(int[] nums) {
int[] arr = Arrays.copyOf(nums, nums.length);
int tmp;
int count = 0, swap_count = 0;
for (int i = 0; i < arr.length-1; i++) {
boolean head_sorted = true;
for (int j = 0; j < arr.length-1-i; j++) {
count++;
if (arr[j] > arr[j+1]) {
tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
head_sorted = false;
swap_count++;
}
}
if (head_sorted) break;
}
System.out.println("bubble_sort_quit_if_sorted: run " + count + ", swap " + swap_count + ", isSorted: " + isSorted(arr));
return arr;
}
優化2:記錄上一輪最後一次交換的位置
在傳統的實現中有序區的長度和排序的輪數是相等的。比如第一輪排序過後的有序區長度是1,第二輪排序過後的有序區長度是2 ......實際上,數列真正的有序區可能會大於這個長度,比如有可能在第二輪,後面5個元素實際都已經屬於有序區。因此後面的許多次元素比較是沒有意義的。
我們可以在每一輪排序的最後,記錄下最後一次元素交換的位置,那個位置也就是無序數列的邊界,再往後就是有序區了。
public static int[] bubble_sort_mark_last_swap(int[] nums) {
int[] arr = Arrays.copyOf(nums, nums.length);
int count = 0, swap_count = 0;
int sorted_border = arr.length;
int tmp;
while (sorted_border > 1) {
int last_swap = 0;
for (int i = 0; i < sorted_border -1; i++) {
count++;
if (arr[i] > arr[i+1]) {
tmp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = tmp;
last_swap = i+1;
swap_count++;
}
}
sorted_border = last_swap;
}
System.out.println("bubble_sort_mark_last_swap: run " + count + ", swap " + swap_count + ", isSorted: " + isSorted(arr));
return arr;
}
上述程式碼中維護了一個已排好序的序列:[sorted_border,N)
(N是陣列大小),每次冒泡會記錄最大的那個泡泡的位置作為sorted_border
。 直到sorted_border == 1
時,說明整個序列已經排好。
因為氣泡排序中每次冒泡都相當於選最大值放到序列結尾,所以[sorted_border,N)
不僅是有序的,而且位置是正確的。 所以sorted_border == 1
時,[1,N)
已經獲得了正確的位置,那麼元素0的位置自然就確定了(它已經沒得選了)。
優化3:雞尾酒排序(雙向氣泡排序)
雞尾酒排序也就是“定向氣泡排序”、“雙向氣泡排序”和“改進氣泡排序”, 雞尾酒攪拌排序, 攪拌排序 (也可以視作選擇排序的一種變形), 漣漪排序, 來回排序 or 快樂小時排序, 是氣泡排序的一種變形。此演算法與氣泡排序的不同處在於排序時是以雙向在序列中進行排序。演算法先找到最小的數字,把他放到第一位,然後找到最大的數字放到最後一位。然後再找到第二小的數字放到第二位,再找到第二大的數字放到倒數第二位。以此類推,直到完成排序。
(1)時間複雜度:雞尾酒排序的效率還是很低的,兩層迴圈,時間複雜度為 O(n^2) 。
(2)空間複雜度:由於只需要幾個臨時變數,所以空間複雜度為 O(1) 。
那麼何以見得雞尾酒排序比氣泡排序好一點呢?
考慮這樣的一個序列:(2,3,4,5,1) 。如果使用雞尾酒排序,一個來回就可以搞定;而氣泡排序則需要跑四趟。
其根本原因在於冒泡是單向的,如果從左向右冒泡,對於小數靠後就會很不利(一趟只能挪一個位置,那就需要多次迴圈。這種數又被稱之為烏龜);相應的,如果從右向左冒泡,對於大數靠前又會很不利(靠前的一隻大烏龜)。雞尾酒排序的優點就在於這裡,由於在序列中左右搖擺(為此雞尾酒排序又稱之為 shaker sort),兩種較差的局面就能得到規避,以此在效能上帶來一些提升。
public static int[] cocktail_sort_original(int[] nums) {
int[] arr = Arrays.copyOf(nums, nums.length);
int i, tmp, left=0, right=arr.length-1;
int count = 0, swap_count = 0;
while (left < right) {
for (i=left; i < right; i++) {
count++;
if(arr[i] > arr[i+1]) {
tmp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = tmp;
swap_count++;
}
}
right--;
for (i=right; i > left; i--) {
count++;
if(arr[i-1] > arr[i]) {
tmp = arr[i];
arr[i] = arr[i-1];
arr[i-1] = tmp;
swap_count++;
}
}
left++;
}
System.out.println("cocktail_sort_original: run " + count + ", swap " + swap_count + ", isSorted: " + isSorted(arr));
return arr;
}
對於雞尾酒排序,演算法的時間複雜度與空間複雜度並沒有改進。不同的是排序的交換次數。某些情況下雞尾酒排序比普通氣泡排序的交換次數少。總體上,雞尾酒排序可以獲得比氣泡排序稍好的效能。但是完全逆序時,雞尾酒排序與氣泡排序的效率都非常差。
優化4:一輪遍歷未發生交換可提前結束的雙向氣泡排序
public static int[] cocktail_sort_quit_if_sorted(int[] nums) {
int[] arr = Arrays.copyOf(nums, nums.length);
int i, tmp, left=0, right=arr.length-1;
int count = 0, swap_count = 0;
while (left < right) {
boolean middle_sorted = true;
for (i=left; i < right; i++) {
count++;
if(arr[i] > arr[i+1]) {
tmp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = tmp;
middle_sorted = false;
swap_count++;
}
}
if (middle_sorted) break;
right--;
for (i=right; i > left; i--) {
count++;
if(arr[i-1] > arr[i]) {
tmp = arr[i];
arr[i] = arr[i-1];
arr[i-1] = tmp;
swap_count++;
}
}
left++;
}
System.out.println("cocktail_sort_quit_if_sorted: run " + count + ", swap " + swap_count + ", isSorted: " + isSorted(arr));
return arr;
}
優化5:記錄上一輪最後一次交換的位置的雙向氣泡排序
public static int[] cocktail_sort_mark_last_swap(int[] nums) {
int[] arr = Arrays.copyOf(nums, nums.length);
int i, tmp, left=0, right=arr.length-1;
int count = 0, swap_count = 0, last_swap = left;
while (left < right) {
for (i=left; i < right; i++) {
count++;
if(arr[i] > arr[i+1]) {
tmp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = tmp;
last_swap = i+1;
swap_count++;
}
}
right = last_swap;
for (i=right; i > left; i--) {
count++;
if(arr[i-1] > arr[i]) {
tmp = arr[i];
arr[i] = arr[i-1];
arr[i-1] = tmp;
last_swap = i-1;
swap_count++;
}
}
left = last_swap;
}
System.out.println("cocktail_sort_mark_last_swap: run " + count + ", swap " + swap_count + ", isSorted: " + isSorted(arr));
return arr;
}
兩個方向都同時跳著走,是目前可以想到的效果最好的優化。
優化效能測試
通過執行力扣測試資料集https://leetcode-cn.com/submissions/detail/114474973/testcase/,得到各個變形的結果如下:
bubble_sort_original: run 1249975000, swap 622443661, isSorted: true
bubble_sort_quit_if_sorted: run 1249928029, swap 622443661, isSorted: true
bubble_sort_mark_last_swap: run 1249543883, swap 622443661, isSorted: true
cocktail_sort_original: run 1249975000, swap 622443661, isSorted: true
cocktail_sort_quit_if_sorted: run 934706395, swap 622443661, isSorted: true
cocktail_sort_mark_last_swap: run 828009788, swap 622443661, isSorted: true
優化5所進行的運算量最少。大多數運算都有效進行了元素交換(排序),而排除了大量無效的迴圈比較。