如何優化氣泡排序?

9龍發表於2019-04-14

一、氣泡排序(BubbleSort)

  • 基本思想:從左到右使用相鄰兩個元素進行比較,如果第一個比第二個大,則交換兩個元素。這樣會使較大數下沉到陣列的尾端,即較小數像泡泡一樣冒到陣列首端。
  • 排序過程:
  1. 比較相鄰兩個元素,如果第一個比第二個大,則交換兩個元素;
  2. 從左到右依次比較,直到最大數位於陣列尾端;
  3. 重複N-1次1、2步驟,(除去已經排序的最大數)依次將第二,第三。。。第N-1大的數排好位置。

如何優化氣泡排序?

如表格所示,每一趟都將當前亂序序列中最大的數移到尾端。【小夥伴們從表格中看出基本氣泡排序可以優化的地方了嗎?】下面先來基本實現程式碼。

  • java實現氣泡排序:

    private static <T extends Comparable<? super T>> void bubbleSort(T[] nums) {
          if (null == nums || nums.length == 0) {
              throw new RuntimeException("陣列為null或長度為0");
          }
          T temp = null;
          int length = nums.length;
          //外迴圈是趟數,每一趟都會將未排序中最大的數放到尾端
          for (int i = 0; i < length - 1; i++) {
              //內迴圈是從第一個元素開始,依次比較相鄰元素,
              // 比較次數隨著趟數減少,因為每一趟都排好了一個元素
              for (int j = 0; j < length - 1 - i; j++) {
                  if (nums[j].compareTo(nums[j + 1]) > 0) {
                      temp = nums[j];
                      nums[j] = nums[j + 1];
                      nums[j + 1] = temp;
                  }
              }
          }
      }
    複製程式碼

從表格中,相信小夥伴已經看出,在第5趟其實已經排好序了,但基本的氣泡排序演算法還會進行第7趟比較,這其實只是進行沒必要的比較,而不會進行元素的交換。(第6趟還是必須要走的,下面會說明)

  • 時間、空間複雜度及穩定性分析:
  1. 時間複雜度:由於內外迴圈都發生N次迭代,所以時間複雜度為O(n^2)。並且這個界是精確的。思考最壞的情況,輸入一個逆序的陣列,則比較次數為:

    (N-1)+(N-2)+(N-3)+..+2+1 = n*(n-1)/2 = O(n^2)

  2. 空間複雜度:只使用了一個臨時變數,所以為O(1)

  3. 是否穩定:穩定排序

二、優化氣泡排序

​ 我們換個角度看待這個問題。基本冒泡演算法之所以進行了無用的多餘掃描,是因為不知道已經排好了序;所以只要我們在第 i 趟(i小於N-1)就知道序列已經排好序,我們就不用進行之後的掃描了。

綜上所述,我們可以增加一個boolean變數,來標識是否已經排好序。優化程式碼如下:

氣泡排序優化普通版:

    private static <T extends Comparable<? super T>> void bubbleSort(T[] nums) {
        if (null == nums || nums.length == 0) {
            throw new RuntimeException("陣列為null或長度為0");
        }

        T temp = null;
        int length = nums.length;
        //用於標識是否已經將序列排好序
        boolean isOrdered = false;
        for (int i = 0; i < length - 1; i++) {
            //每一趟開始前都假設已經有序
            isOrdered = true;
            for (int j = 0; j < length - 1 - i; j++) {
                if (nums[j].compareTo(nums[j + 1]) > 0) {
                    temp = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j + 1] = temp;
                    //如果出現有元素交換,則表明此躺可能沒有完成排序
                    isOrdered = false;
                }
            }
            //如果當前趟都沒有進行元素的交換,證明前面一趟比較已經排好序
            //直接跳出迴圈
            if (isOrdered) {
                break;
            }
        }
    }
複製程式碼

注意:雖然第5趟已經排好序,但對於程式來說,它並不知道此趟已經排好序,需要進行下一趟掃描來確定上一趟是否已經將原序列排好序。所以第6趟是必須要去掃描的。

你以為結束了嗎?哈哈哈,還沒有,這只是第一版優化。

讓我們想一想這樣的情況。對於下列序列,前半部分亂序,後半部分有序。

如何優化氣泡排序?

簡述排序過程:

第一趟:發生交換的是5和3,接著是5和2;隨後5與6比較,不需要換位置,相同地,6與7、7與8、8與9都不需要更換位置。所以第一趟結果為:[4,3,2,5,6,7,8,9]。

第二趟:發生交換的是4與3,接著4與2;隨後4與5、5與6,6與7、7與8都不需要更換位置。【8不需要與9比較,因為第一趟已經將最大的數下沉到尾端】。所以第二趟結果為:[3,2,4,5,6,7,8,9]。

第三趟:發生交換的是3與2;隨後3與4,4與5,5與6,6與7都不需要更換位置。所以第三趟結果為:[2,3,4,5,6,7,8,9]。

大家看出什麼了嗎?其實進行了很多無意義的比較,因為這些都不需要更換位置,而很多趟都會重複比較。根據氣泡排序思想,我們知道,有序序列長度,其實跟排序趟數相等,每一趟就是將當前亂序中的最大值下沉到陣列尾端。但其實序列真正有序的序列長度是大於當前排序趟數的。也就是說,只要我們找到了原序列中無序與有序的邊界,就可以避免再去比較有序序列。

其實最後一次交換的位置,就是無序序列與有序序列的邊界。

從例子中看:

第一趟最後一次交換的位置是元素5與2交換的位置,即陣列下標2的位置;

第二趟最後一次交換的位置是元素4與2交換的位置,即陣列下標1的位置;

第三趟最後一次交換的位置是元素3與2交換的位置,即陣列下標0的位置;

所以,只要我們記錄下當前趟最後一次交換的位置,在下一趟只比較到這個位置即可。

氣泡排序優化加強版:

    private static <T extends Comparable<? super T>> void bubbleSort(T[] nums) {
        if (null == nums || nums.length == 0) {
            throw new RuntimeException("陣列為null或長度為0");
        }

        T temp = null;
        int length = nums.length;
        boolean isOrdered = false;
        int lastExchangeIndex = 0;
        //當前趟無序的邊界
        int unorderedBorder = length - 1;
        for (int i = 0; i < length - 1; i++) {
            //每一趟開始前都假設已經有序
            isOrdered = true;
            for (int j = 0; j < unorderedBorder; j++) {
                if (nums[j].compareTo(nums[j + 1]) > 0) {
                    temp = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j + 1] = temp;
                    //如果出現有元素交換,則表明此躺沒有完成排序
                    isOrdered = false;
                    //記錄下最後一次交換元素的位置
                    lastExchangeIndex = j;
                }
            }
            unorderedBorder = lastExchangeIndex;
            if (isOrdered) {
                break;
            }
        }
    }
複製程式碼

其實,還可以進一步優化, 有興趣的可以去看看雞尾酒排序,我們已經很接近了。

三、總結

氣泡排序可以通過增加boolean標識是否已經排好序來進行優化;還可以記錄下最後一次交換元素的位置來進行優化,防止無意義的比較。氣泡排序是穩定排序,時間複雜度為O(n^2),空間複雜度為O(1)。

相關文章