氣泡排序及優化詳解

JYRoy發表於2019-07-26

演算法思想

  氣泡排序屬於一種典型的交換排序。

  交換排序顧名思義就是通過元素的兩兩比較,判斷是否符合要求,如過不符合就交換位置來達到排序的目的。氣泡排序名字的由來就是因為在交換過程中,類似水冒泡,小(大)的元素經過不斷的交換由水底慢慢的浮到水的頂端。

  氣泡排序的思想就是利用的比較交換,利用迴圈將第 i 小或者大的元素歸位,歸位操作利用的是對 n 個元素中相鄰的兩個進行比較,如果順序正確就不交換,如果順序錯誤就進行位置的交換。通過重複的迴圈訪問陣列,直到沒有可以交換的元素,那麼整個排序就已經完成了。

 

示例

  我們通過一個示例來理解一下基本的氣泡排序,假設當前我們有一個陣列 a,內部元素為 3,4,1,5,2,即初始狀態,如下圖所示。我們的目的就是通過 n 趟比較來實現有底向上從大到小的的順序。

第一遍排序

  我們首先進行第一遍排序,如下圖所示,紅色代表當前比較的元素,綠色代表已經歸位的元素。

  (1)比較第一個和第二個元素,4>3,交換。

  (2)比較第二個和第三個元素,1<3,不交換。

  (3)比較第三個和第四個元素,5>1,交換。

  (4)比較第四個和第五個元素,2>1,交換。

  最後,我們可以看到 1 已經位於最頂部。第一遍需要盡心四次比較才能把五個數比較完。

 

第二遍排序

  第二遍排序的初始狀態是第一遍排序的最終狀態,即4,3,5,2,1。

  (1)比較第一個和第二個元素,3<4,不交換。

  (2)比較第二個和第三個元素,5>3,交換。

  (3)比較第三個和第四個元素,2<3,不交換。

  第二遍排序,會讓 2 歸位,並且這一遍只用進行三次比較就可以了。

第三遍排序

  第三遍排序的初始狀態是第二遍排序的最終狀態,即4,5,3,2,1。

  (1)比較第一個和第二個元素,5>4,交換。

  (2)比較第二個和第三個元素,3<4,不交換。

  第三遍排序,會讓 3 歸位,並且這一遍只用進行兩次比較就可以了。

  然而我們可以看到這一次五個數已經全部完成了歸位,但是當我們採用普通的氣泡排序的時候,演算法仍然會繼續向下進行。

第四遍迴圈

  第四遍排序的初始狀態是第三遍排序的最終狀態,即5,4,3,2,1。

  這個時候就可以看出,排序實際上在第三遍已經完成了,但是演算法還是會繼續向下進行,下面就進行程式碼實現,看一下究竟是什麼情況。。

氣泡排序效能

演算法最好時間最壞時間平均時間額外空間穩定性
冒泡 O(n) O(n2) O(n2) 1 穩定

  關於穩定性:因為在比較的過程中,當兩個相同大小的元素相鄰,只比較大或者小,所以相等的時候是不會交換位置的。而當兩個相等元素離著比較遠的時候,也只是會把他們交換到相鄰的位置。他們的位置前後關係不會發生任何變化,所以演算法是穩定的。

  關於最優時間複雜度為什麼是O(n),當然是優化過演算法之後了!大家繼續向下看就知道了!。

 

氣泡排序常規版-程式碼實現

  下面詳細分析一下常規版的氣泡排序,整個演算法流程其實就是上面例項所分析的過程。可以看出,我們在進行每一次大迴圈的時候,還要進行一個小迴圈來遍歷相鄰元素並交換。所以我們的程式碼中首先要有兩層迴圈。

  外層迴圈:即主迴圈,需要輔助我們找到當前第 i 小的元素來讓它歸位。所以我們會一直遍歷 n-2 次,這樣可以保證前 n-1 個元素都在正確的位置上,那麼最後一個也可以落在正確的位置上了。

  內層迴圈:即副迴圈,需要輔助我們進行相鄰元素之間的比較和換位,把大的或者小的浮到水面上。所以我們會一直遍歷 n-1-i 次這樣可以保證沒有歸位的儘量歸位,而歸位的就不用再比較了。

  而上面的問題,出現的原因也來源於這兩次無腦的迴圈,正是因為迴圈不顧一切的向下執行,所以會導致在一些特殊情況下得多餘。例如 5,4,3,1,2 的情況下,常規版會進行四次迴圈,但實際上第一次就已經完成排序了。

 1 /**
 2  * @author jyroy
 3  * 氣泡排序常規版
 4  */
 5 public class BubbleSortNormal {
 6     public static void main(String[] args) {
 7         int[] list = {3,4,1,5,2};
 8         int temp = 0; // 開闢一個臨時空間, 存放交換的中間值
 9         // 要遍歷的次數
10         for (int i = 0; i < list.length-1; i++) {
11             System.out.format("第 %d 遍:\n", i+1);
12             //依次的比較相鄰兩個數的大小,遍歷一次後,把陣列中第i小的數放在第i個位置上
13             for (int j = 0; j < list.length-1-i; j++) {
14                 // 比較相鄰的元素,如果前面的數小於後面的數,就交換
15                 if (list[j] < list[j+1]) {
16                     temp = list[j+1];
17                     list[j+1] = list[j];
18                     list[j] = temp;
19                 }
20                 System.out.format("第 %d 遍的第%d 次交換:", i+1,j+1);
21                 for(int count:list) {
22                     System.out.print(count);
23                 }
24                 System.out.println("");
25             }
26             System.out.format("第 %d 遍最終結果:", i+1);
27             for(int count:list) {
28                 System.out.print(count);
29             }
30             System.out.println("\n#########################");
31         }
32     }
33 }

  執行結果

演算法的第一次優化

  經過了上述的討論和編碼,常規的氣泡排序已經被我們實現了。那麼接下來我們要討論的就是剛剛分析時候提出的問題。

  首先針對第一個問題,當我們進行完第三遍的時候,實際上整個排序都已經完成了,但是常規版還是會繼續排序。

  可能在上面這個示例下,可能看不出來效果,但是當陣列是,5,4,3,1,2 的時候的時候就非常明顯了,實際上在第一次迴圈的時候整個陣列就已經完成排序,但是常規版的演算法仍然會繼續後面的流程,這就是多餘的了。

  為了解決這個問題,我們可以設定一個標誌位,用來表示當前第 i 趟是否有交換,如果有則要進行 i+1 趟,如果沒有,則說明當前陣列已經完成排序。實現程式碼如下:

 1 /**
 2  * @author jyroy
 3  * 氣泡排序優化第一版
 4  */
 5 public class BubbleSoerOpt1 {
 6     public static void main(String[] args) {
 7         int[] list = {5,4,3,1,2};
 8         int temp = 0; // 開闢一個臨時空間, 存放交換的中間值
 9         // 要遍歷的次數
10         for (int i = 0; i < list.length-1; i++) {
11             int flag = 1; //設定一個標誌位
12             //依次的比較相鄰兩個數的大小,遍歷一次後,把陣列中第i小的數放在第i個位置上
13             for (int j = 0; j < list.length-1-i; j++) {
14                 // 比較相鄰的元素,如果前面的數小於後面的數,交換
15                 if (list[j] < list[j+1]) {
16                     temp = list[j+1];
17                     list[j+1] = list[j];
18                     list[j] = temp;
19                     flag = 0;  //發生交換,標誌位置0
20                 }
21             }
22             System.out.format("第 %d 遍最終結果:", i+1);
23             for(int count:list) {
24                 System.out.print(count);
25             }
26             System.out.println("");     
27             if (flag == 1) {//如果沒有交換過元素,則已經有序
28                 return;
29             }
30                    
31         }
32     }
33 }

  執行結果:可以看到優化效果非常明顯,比正常情況下少了兩次的迴圈。

     這個時候我們就來討論一下上面留下的一個小地方!沒錯就是最優時間複雜度為O(n)的問題,我們在進行了這一次演算法優化之後,就可以做到了。

  當給我們一個數列,5,4,3,2,1,讓我們從大到小排序。沒錯,這是已經排好序的啊,也就是說因為標誌位的存在,上面的迴圈只會進行一遍,flag沒有變成1,整個演算法就結束了,這也就是 O(n) 的來歷了!

 

演算法的第二次優化

   除了上面這個問題,在氣泡排序中還有一個問題存在,就是第 i 趟排的第 i 小或者大的元素已經在第 i 位上了,甚至可能第 i-1 位也已經歸位了,那麼在內層迴圈的時候,有這種情況出現就會導致多餘的比較出現。例如:6,4,7,5,1,3,2,當我們進行第一次排序的時候,結果為6,7,5,4,3,2,1,實際上後面有很多次交換比較都是多餘的,因為沒有產生交換操作。

  我們用剛剛優化過一次的演算法,跑一下這個陣列。

 1 /**
 2  * @author jyroy
 3  * 氣泡排序優化第一版
 4  */
 5 public class BubbleSoerOpt1 {
 6     public static void main(String[] args) {
 7         int[] list = {6,4,7,5,1,3,2};
 8         int len = list.length-1;
 9         int temp = 0; // 開闢一個臨時空間, 存放交換的中間值
10         // 要遍歷的次數
11         for (int i = 0; i < list.length-1; i++) {
12             int flag = 1; //設定一個標誌位
13             //依次的比較相鄰兩個數的大小,遍歷一次後,把陣列中第i小的數放在第i個位置上
14             for (int j = 0; j < len-i; j++) {
15                 // 比較相鄰的元素,如果前面的數小於後面的數,交換
16                 if (list[j] < list[j+1]) {
17                     temp = list[j+1];
18                     list[j+1] = list[j];
19                     list[j] = temp;
20                     flag = 0;  //發生交換,標誌位置0
21 
22                 }
23                 System.out.format("第 %d 遍第%d 趟結果:", i+1, j+1);
24                 for(int count:list) {
25                     System.out.print(count);
26                 }
27                 System.out.println("");     
28             }
29 
30             System.out.format("第 %d 遍最終結果:", i+1);
31             for(int count:list) {
32                 System.out.print(count);
33             }
34             System.out.println("");     
35             if (flag == 1) {//如果沒有交換過元素,則已經有序
36                 return;
37             }
38                    
39         }
40     }
41 }

  執行結果:可以看出,第三趟的多次比較實際上可以沒有,因為中間幾個位置在第二趟就沒有過交換。

 

   針對上述的問題,我們可以想到,利用一個標誌位,記錄一下當前第 i 趟所交換的最後一個位置的下標,在進行第 i+1 趟的時候,只需要內迴圈到這個下標的位置就可以了,因為後面位置上的元素在上一趟中沒有換位,這一次也不可能會換位置了。基於這個原因,我們可以進一步優化我們的程式碼。

 1 /**
 2  * @author jyroy
 3  * 氣泡排序優化第二版
 4  */
 5 public class BubbleSoerOpt2 {
 6     public static void main(String[] args) {
 7         int[] list = {6,4,7,5,1,3,2};
 8         int len = list.length-1;
 9         int temp = 0; // 開闢一個臨時空間, 存放交換的中間值
10         int tempPostion = 0;  // 記錄最後一次交換的位置
11         // 要遍歷的次數
12         for (int i = 0; i < list.length-1; i++) {
13             int flag = 1; //設定一個標誌位
14             //依次的比較相鄰兩個數的大小,遍歷一次後,把陣列中第i小的數放在第i個位置上
15             for (int j = 0; j < len; j++) {
16                 // 比較相鄰的元素,如果前面的數小於後面的數,交換
17                 if (list[j] < list[j+1]) {
18                     temp = list[j+1];
19                     list[j+1] = list[j];
20                     list[j] = temp;
21                     flag = 0;  //發生交換,標誌位置0
22                     tempPostion = j;  //記錄交換的位置
23                 }
24                 System.out.format("第 %d 遍第%d 趟結果:", i+1, j+1);
25                 for(int count:list) {
26                     System.out.print(count);
27                 }
28                 System.out.println("");     
29             }
30             len = tempPostion; //把最後一次交換的位置給len,來縮減內迴圈的次數
31             System.out.format("第 %d 遍最終結果:", i+1);
32             for(int count:list) {
33                 System.out.print(count);
34             }
35             System.out.println("");     
36             if (flag == 1) {//如果沒有交換過元素,則已經有序
37                 return;
38             }
39                    
40         }
41     }
42 }

  執行結果:

  可以清楚的看到,部分內迴圈多餘的比較已經被去掉了,演算法得到了進一步的優化

 

 


  

  因為水平有限,所以對演算法的描述和分析存在一些缺陷,而且選取的例子可能有些不恰當,大家可以多試一些數列。

 

相關文章