你知道和你不知道的冒泡排序

這篇文章包含了你必定知道的,和你不必定知道的冒泡排序。java

gif看不了的能夠點擊【原文】查看gif。git

源碼: 【地址github

1. 什麼是冒泡排序

可能對於大多數的人來講好比我,接觸的第一個算法就是冒泡排序。web

我看過的不少的文章都把冒泡排序描述成咱們喝的汽水,底部不停的有二氧化碳的氣泡往上冒,還有描述成魚吐泡泡,都特別的形象。算法

其實結合一杯水來對比很好理解,將咱們的數組豎着放進杯子,數組中值小的元素密度相對較小,值大的元素密度相對較大。這樣一來,密度大的元素就會沉入杯底,而密度小的元素會慢慢的浮到杯子的最頂部,稍微專業一點描述以下。數組

冒泡算法會運行多輪,每一輪會依次比較數組中相鄰的兩個元素的大小,若是左邊的元素大於右邊的元素,則交換兩個元素的位置。最終通過多輪的排序,數組最終成爲有序數組。服務器

2. 排序過程展現

咱們先不聊空間複雜度和時間複雜度的概念,咱們先經過一張動圖來了解一下冒泡排序的過程。微信

這個圖形象的還原了密度不一樣的元素上浮和下沉的過程。數據結構

3. 算法V1

3.1 代碼實現

private void bubbleSort(int[] arr) {
  for (int i = 0; i < arr.length; i++) {
    for (int j = 0; j < arr.length - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        exchange(arr, j, j + 1);
      }
    }
  }
}

private void exchange(int arr[], int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

int[] arr = new int[]{5, 1, 3, 7, 6, 2, 4};
bubbleSort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]
複製代碼

3.2 實現分析

各位大佬看了上面的代碼以後先別激動,坐下坐下,平常操做。可能不少的第一個冒泡排序算法就是這麼寫的,好比我,同時還自我感受良好,以爲算法也不過如此。app

咱們仍是以數組[5, 1, 3, 7, 6, 2, 4]爲例,咱們經過動圖來看一下過程。

思路很簡單,咱們用兩層循環來實現冒泡排序。

  • 第一層,控制冒泡排序總共執行的輪數,例如例子數組的長度是7,那麼總共須要執行6輪。若是長度是n,則須要執行n-1輪
  • 第二層,負責從左到右依次的兩兩比較相鄰元素,而且將大的元素交換到右側

這就是冒泡排序V1的思路。

下表是經過對一個0-100000的亂序數組的標準樣本,使用V1算法進行排序所總共執行的次數,以及對同一個數組執行100次V1算法的所花的平均時間。

算法執行狀況 結果
樣本 [0 - 100000] 的亂序數組
算法 V1 執行的總次數 99990000 次(9999萬次
算法 V1 運行 100 次的平均時間 181 ms

4. 算法V2

4.1 實現分析

仔細看動圖咱們能夠發現,每一輪的排序,都從數組的最左端再到最右。而每一輪的冒泡,均可以肯定一個最大的數,固定在數組的最右邊,也就是密度最大的元素會冒泡到杯子的最上面。

仍是拿上面的數組舉例子。下圖是第一輪冒泡以後數組的元素位置。

第二輪排序以後以下。

能夠看到,每一輪排序都會確認一個最大元素,放在數組的最後面,當算法進行到後面,咱們根本就沒有必要再去比較數組後面已經有序的片斷,咱們接下來針對這個點來優化一下。

4.2 代碼實現

這是優化以後的代碼。

private void bubbleSort(int[] arr) {
  for (int i = 0; i < arr.length - 1; i++) {
    for (int j = 0; j < arr.length - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        exchange(arr, j, j + 1);
      }
    }
  }
}

private void exchange(int arr[], int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

int[] arr = new int[]{5, 1, 3, 7, 6, 2, 4};
bubbleSort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]
複製代碼

優化以後的實現,也就變成了咱們動圖中所展現的過程。

每一步以後都會肯定一個元素在數組中的位置,因此以後的每次冒泡的須要比較的元素個數就會相應的減1。這樣一來,避免了去比較已經有序的數組,從而減小了大量的時間。

算法執行狀況 結果
樣本 [0 - 10000] 的亂序數組
算法 V2 執行的總次數 49995000 次(4999萬次
算法 V2 運行 100 次的平均時間 144 ms
運行時間與 V1 對比 V2 運行時間減小 20.44 %
執行次數與 V1 對比 V2 運行次數減小 50.00 %

可能會有人看到,時間大部分已經會以爲知足了。從數據上看,執行的次數減小了50%,而運行的時間也減小了20%,在性能上已是很大的提高了。並且已經減小了7億次的執行次數,已經很NB了。 那是否是到這就已經很完美了呢?

答案是No

4.3 哪裏能夠優化

同理,咱們仍是拿上面長度爲7的數組來舉例子,只不過元素的位置有所不一樣,假設數組的元素以下。

[7, 1, 2, 3, 4, 5, 6]

咱們再來一步一步的執行V2算法, 看看會發生什麼。

第一步執行完畢後,數組的狀況以下。

繼續推動,當第一輪執行完畢後,數組的元素位置以下。

這個時候,數組已經排序完畢,可是按照目前的V2邏輯,仍然有5輪排序須要繼續,並且程序會完整的執行完5輪的排序,若是是100000輪呢?這樣將會浪費大量的計算資源。

5. 算法V3

5.1 代碼實現

private void bubbleSort(int[] arr) {
  for (int i = 0; i < arr.length - 1; i++) {
    boolean flag = true;
    for (int j = 0; j < arr.length - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        flag = false;
        exchange(arr, j, j + 1);
      }
    }
    if (flag) {
      break;
    }
  }
}

private void exchange(int arr[], int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

int[] arr = new int[]{5, 1, 3, 7, 6, 2, 4};
bubbleSort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]
複製代碼

5.2 實現分析

咱們在V2代碼的基礎上,在第一層循環,也就是控制總冒泡輪數的循環中,加入了一個標誌爲flag。用來標示該輪冒泡排序中,數組是不是有序的。每一輪的初始值都是true。

當第二層循環,也就是冒泡排序的元素兩兩比較完成以後,flag的值仍然是true,則說明在這輪比較中沒有任何元素被交換了位置。也就是說,數組此時已是有序狀態了,沒有必要再執行後續的剩餘輪數的冒泡了。

因此,若是flag的值是true,就直接break了(沒有其餘的操做return也沒毛病)。

算法執行狀況 結果
樣本 [0 - 10000] 的亂序數組
算法 V3 執行的總次數 49993775
算法 V3 運行 100 次的平均時間 142 ms
運行時間與 V2 對比 V3 運行時間減小 00.00 %
執行次數與 V2 對比 V3 運行次數減小 00.00 %

5.3 數據分析

你們看到數據可能有點懵逼。

你這個優化以後,運行時間執行次數都沒有減小。你這優化的什麼東西?

其實,這就要說到算法的適用性了。V3的優化是針對原始數據中存在一部分或者大量的數據已是有序的狀況,V3的算法對於這樣的樣本數據才最適用。

實際上是咱們尚未到優化這種狀況的那一步,可是其實仍然有這樣的說法,面對不一樣的數據結構,幾乎沒有算法是萬能的

而目前的樣本數據仍然是隨機的亂序數組,因此並不能發揮優化以後的算法的威力。所謂對症下藥,同理並非全部的算法都是萬能的。對於不一樣的數據咱們須要選擇不一樣的算法。例如咱們選擇[9999,1,2,…,9998]這行的數據作樣原本分析,咱們來看一下V3算法的表現。

算法執行狀況 結果
樣本 [0 - 10000] 的亂序數組
算法 V3 執行的總次數 19995
算法 V3 運行 100 次的平均時間 1 ms
運行時間與 V3 亂序樣例對比 V3 運行時間減小 99.96 %
執行次數與 V3 亂序樣例對比 V3 運行次數減小 99.29 %

能夠看到,提高很是明顯。

5.4 適用狀況

當冒泡算法運行到後半段的時候,若是此時數組已經有序了,須要提早結束冒泡排序。V3針對這樣的狀況就特別有效。

6. 算法V4

嗯,什麼?爲何不是結束語?那是由於還有一種沒有考慮到啊。

6.1 適用狀況總結

咱們總結一下前面的算法可以處理的狀況。

  • V1:正常亂序數組
  • V2:正常亂序數組,但對算法的執行次數作了優化
  • V3:大部分元素已經有序的數組,能夠提早結束冒泡排序

還有一種狀況是冒泡算法的輪數沒有執行完,甚至尚未開始執行,後半段的數組就已經有序的數組,例如以下的狀況。

這種狀況,在數組徹底有序以前都不會觸發V3中的提早中止算法,由於每一輪都有交換存在,flag的值會一直是true。而下標2以後的全部的數組都是有序的,算法會依次的冒泡完全部的已有序部分,形成資源的浪費。咱們怎麼來處理這種狀況呢?

6.2 實現分析

咱們能夠在V3的基礎之上來作。

當第一輪冒泡排序結束後,元素3會被移動到下標2的位置。在此以後沒有再進行過任意一輪的排序,可是若是咱們不作處理,程序仍然會繼續的運行下去。

咱們在V3的基礎上,加上一個標識endIndex來記錄這一輪最後的發生交換的位置。這樣一來,下一輪的冒泡就只冒到endIndex所記錄的位置便可。由於後面的數組沒有發生任何的交換,因此數組一定有序。

6.3 代碼實現

private void bubbleSort(int[] arr) {
  int endIndex = arr.length - 1;
  for (int i = 0; i < arr.length - 1; i++) {
    boolean flag = true;
    int endAt = 0;
    for (int j = 0; j < endIndex; j++) {
      if (arr[j] > arr[j + 1]) {
        flag = false;
        endAt = j;
        exchange(arr, j, j + 1);
      }
    }
    endIndex = endAt;
    if (flag) {
      break;
    }
  }
}

private void exchange(int arr[], int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

int[] arr = new int[]{5, 1, 3, 7, 6, 2, 4};
bubbleSort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]
複製代碼

7. 算法V5

這一節仍然不是結束語...

7.1 算法優化

咱們來看一下這種狀況。

對於這種以上的算法都將不能發揮其應有的做用。每一輪算法都存在元素的交換,同時,直到算法完成之前,數組都不是有序的。可是若是咱們能直接從右向左冒泡,只須要一輪就能夠完成排序。這就是雞尾酒排序,冒泡排序的另外一種優化,其適用狀況就是上圖所展現的那種。

7.2 代碼實現

private void bubbleSort(int[] arr) {
  int leftBorder = 0;
  int rightBorder = arr.length - 1;

  int leftEndAt = 0;
  int rightEndAt = 0;

  for (int i = 0; i < arr.length / 2; i++) {
    boolean flag = true;
    for (int j = leftBorder; j < rightBorder; j++) {
      if (arr[j] > arr[j + 1]) {
        flag = false;
        exchange(arr, j, j + 1);
        rightEndAt = j;
      }
    }
    rightBorder = rightEndAt;
    if (flag) {
      break;
    }

    flag = true;
    for (int j = rightBorder; j > leftBorder; j--) {
      if (arr[j] < arr[j - 1]) {
        flag = false;
        exchange(arr, j, j - 1);
        leftEndAt = j;
      }
    }
    leftBorder = leftEndAt;
    if (flag) {
      break;
    }
  }
}

private void exchange(int arr[], int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

int[] arr = new int[]{2, 3, 4, 5, 6, 7, 1};
bubbleSort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]
複製代碼

7.3 實現分析

第一層循環一樣用於控制總的循環輪數,因爲每次須要從左到右再從右到左,因此總共的輪數是數組的長度 / 2。

內存循環則負責先實現從左到右的冒泡排序,再實現從右到左的冒泡,而且同時結合了V4的優化點。

咱們來看一下V5與V4的對比。

算法執行狀況 結果
樣本 [2,3,4…10000,1] 的數組
算法 V5 執行的總次數 19995
算法 V5 運行 100 次的平均時間 1 ms
運行時間與 V4 對比 V5 運行時間減小 99.97 %
執行次數與 V4 對比 V5 運行次數減小 99.34 %

8. 總結

如下是對同一個數組,使用每一種算法對其運行100次的平均時間和執行次數作的的對比。

[0 - 10000] 的亂序數組 V1 V2 V3 V4 V5
執行時間(ms) 184 142 143 140 103
執行次數(次) 99990000 49995000 49971129 49943952 16664191
大部分有序的狀況 V1 V2 V3 V4 V5
執行時間(ms) 181 141 146 145 107
執行次數(次) 99990000 49995000 49993230 49923591 16675618

而冒泡排序的時間複雜度分爲最好的狀況和最快的狀況。

  • 最好的狀況爲O(n). 也就是咱們在V5中提到的那種狀況,數組2, 3, 4, 5, 6, 7, 1。使用雞尾酒算法,只須要進行一輪冒泡,便可完成對數組的排序。
  • 最壞的狀況爲O(n^2).也就是V1,V2,V3和V4所遇到的狀況,幾乎大部分數據都是無序的。

往期文章:

相關:

  • 微信公衆號: SH的全棧筆記(或直接在添加公衆號界面搜索微信號LunhaoHu)
相關文章
相關標籤/搜索