1. 程式人生 > 實用技巧 >交換排序演算法之氣泡排序

交換排序演算法之氣泡排序


八種排序演算法可以按照如圖分類,本文主要介紹氣泡排序。


交換排序

所謂交換,就是序列中任意兩個元素進行比較,根據比較結果來交換各自在序列中的位置,以此達到排序的目的。


氣泡排序

氣泡排序是一種簡單的交換排序演算法,以升序排序為例,其核心思想是:

  1. 從第一個元素開始,比較相鄰的兩個元素。如果第一個比第二個大,則進行交換。
  2. 輪到下一組相鄰元素,執行同樣的比較操作,再找下一組,直到沒有相鄰元素可比較為止,此時最後的元素應是最大的數。
  3. 除了每次排序得到的最後一個元素,對剩餘元素重複以上步驟,直到沒有任何一對元素需要比較為止。

用 Java 實現的氣泡排序如下

/**
 * 氣泡排序常規版
 *
 * <p>沒有經過任何優化操作的版本
 * @param arr 待排序的整型陣列
 * @return 比較次數
 */
public static int bubbleSortOpt1(int[] arr) {

    if (arr == null) {
        throw new NullPointerException();
    } else if (arr.length == 0) {
        return 0;
    }

    int temp;
    int count = 0;
    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]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
            count++;
        }
    }
    return count;
}

演算法優化

現在有個問題,假如待排序陣列是 2、1、3、4、5 這樣的情況,按照上述程式碼實現,第一次迴圈即可得出正確結果。但迴圈並不會停止,而是繼續執行,直到結束為止。顯然,之後的迴圈遍歷是沒有必要的。

為了解決這個問題,我們可以設定一個標誌位,用來表示當前次迴圈是否有交換,如果沒有,則說明當前陣列已經完全排序。

/**
 * 氣泡排序優化第一版
 *
 * <p>設定了一個標誌位,用來表示當前次迴圈是否有交換,如果沒有,則說明當前陣列已經完全排序
 * @param arr 待排序的整型陣列
 * @return 比較次數
 */
public static int bubbleSortOpt2(int[] arr) {

    if (arr == null) {
        throw new NullPointerException();
    } else if (arr.length == 0) {
        return 0;
    }

    int temp;
    int count = 0;
    for (int i = 0; i < arr.length - 1; i++) {
        int flag = 1;
        for (int j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                flag = 0;
            }
            count++;
        }
        // 沒有發生交換,排序已經完成
        if (flag == 1) {
            return count;
        }
    }
    return count;
}

演算法還可以再優化,比如 3、4、2、1、6、7、8 這個陣列,第一次迴圈後,變為 3、2、1、4、6、7、8 的順序,我們發現,1 之後的 4 、6、7、8 已經有序了,第二次迴圈就沒必要對後面這段再遍歷比較。

假設一次迴圈後陣列第 i 個元素後所有元素已經有序,優化目標就是下次迴圈不再花費時間遍歷已經有序的部分。關鍵在於如何定位 i 這個分界點,其實並不難,可以想象,由於 i 之前的元素是無序的,所以一定有交換髮生,而 i 之後的元素已經有序,不會發生交換,最後發生交換的地點,就是我們要找的分界點。

/**
 * 氣泡排序優化第二版
 *
 * <p>定位最後發生交換的分界點,之後的元素無需遍歷。
 * @param arr 待排序的整型陣列
 * @return 比較次數
 */
public static int bubbleSortOpt3(int[] arr) {

    if (arr == null) {
        throw new RuntimeException();
    } else if (arr.length == 0) {
        return 0;
    }

    int temp;
    int count = 0;
    int len = arr.length - 1;
    for (int i = 0; i < arr.length - 1; i++) {
        // 記錄最後一次交換位置
        int lastChange = 1;
        for (int j = 0; j < len; j++) {
            if (arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                // 每交換一次更新一次
                lastChange = j;
            }
            count++;
        }
        len = lastChange;
    }
    return count;
}

整合第一版和第二版,得到最終版本的冒泡演算法

/**
 * 氣泡排序優化最終版
 *
 * <p>整合第一版和第二版
 * @param arr 待排序的整型陣列
 * @return 比較次數
 */
public static int bubbleSortOpt4(int[] arr) {

    if (arr == null) {
        throw new RuntimeException();
    } else if (arr.length == 0) {
        return 0;
    }

    int temp;
    int count = 0;
    int len = arr.length - 1;
    for (int i = 0; i < arr.length - 1; i++) {
        // 記錄最後一次交換位置
        int lastChange = 1;
        int flag = 1;
        for (int j = 0; j < len; j++) {
            if (arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                // 每交換一次更新一次
                lastChange = j;
                flag = 0;
            }
            count++;
        }
        // 沒有發生交換,排序已經完成
        if (flag == 1) {
            return count;
        }
        len = lastChange;
    }
    return count;
}

各位可以自行設計測試資料,看看返回的比較次數大小是否減少。


效能分析

分析最優版本演算法,最好的情況是一開始已經排好序,那就沒必要交換了,直接返回,此時只走了外層的第一次迴圈,最優時間複雜度為 O(n)。

最壞的情況當然是陣列是逆序的,這樣裡外兩層迴圈都走了遍,最壞時間複雜度為 O( n^2 )。

空間複雜度則是用於交換的臨時變數,至於標誌位和其他的因演算法的不同實現有差異。