圖解氣泡排序及演算法優化(Java實現)

衍方發表於2020-09-11

冒牌排序

基本思想

定義:氣泡排序的英文是bubblesort,它是一種基礎的交換排序

原理:每次比較兩個相鄰的元素,將較大的元素交換至右端 (升序排序)

思路:相鄰的元素兩兩比較,當一個元素大於右側相鄰元素時,交換它們的位置;當一個元素小於或等於右側相鄰元素時,位置不變

案例分析

1、初始的無序數列 {5,8,6,3,9,2,1,7},希望對其升序排序

2、按照思路分析:

在經過第一輪交換後,最大的數 9 冒泡到了最右邊

到此為止,所有元素都是有序的了,這就是氣泡排序的整體思路。
3、氣泡排序是一種穩定排序,值相等的元素並不會打亂原本的順序。由於該排序演算法的每一輪都要遍歷所有元素,總共遍歷(元素數量-1)輪,所以平均時間複雜度是O(n2)。

程式碼實現

第 1 版程式碼

public static void bubbleSort(int[] arr){
    if (arr == null || arr.length == 0) return;
    for (int i = 0; i < arr.length - 1; i++) {
        for (int j = 0; j < arr.length - 1 -i; j++) {
            int tmp = 0;
            //升序排序>,降序排序<
            if (arr[j] > arr[j + 1]){
                tmp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = tmp;
            }
        }
    }
}

使用雙迴圈進行排序。外部迴圈控制所有的回合,內部迴圈實現每一輪的冒泡處理,先進行元素比較,再進行元素交換。

第 2 版程式碼

仍以無序數列 {5,8,6,3,9,2,1,7}為例,我們發現在第 6 輪的時候,數列已經是有序了,但氣泡排序仍然進行了第7輪,可以做一個小優化,在外層迴圈設定一個哨兵標記isSorted,預設有序,內層迴圈如果發生交換,則仍為無序

public static void bubbleSort(int[] arr){
    if (arr == null || arr.length == 0) return;
    for (int i = 0; i < arr.length - 1; i++) {
        //是否已經有序的標記,預設有序
        boolean isSorted = true;
        for (int j = 0; j < arr.length - 1 -i; j++) {
            int tmp = 0;
            //升序排序>,降序排序<
            if (arr[j] > arr[j + 1]){
                tmp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = tmp;
                //發生元素交換,序列仍是無序狀態
                isSorted = false;
            }
        }
        if (isSorted){
            break;
        }
    }
}

isSorted作為標記。如果在本輪排序中,元素有交換,則說明數列無序;如果沒有元素交換,則說明數列已然有序,然後直接跳出大迴圈。

第 3 版程式碼

以新的無序數列 {3,4,2,1,6,7,8,9}為例,發現前半部分是無序的,而後半部分[6 ,7 ,8 ,9]是有序區間

如果以上面的第2版程式碼執行,會發現只有前半部分的比較是有意義的,而後半部分的有序區間的比較是無意義的

怎麼避免這種情況?那麼可以在每一輪的排序後,記錄下來最後一次元素交換的位置,該位置即為無序數列的邊界,再往後就是有序區

public static void bubbleSort(int[] arr){
    if (arr == null || arr.length == 0) return;
    //記錄記錄下來最後一次元素交換的位置
    int lastExchangeIndex = 0;
    //無序數列的邊界,每次比較只需要比到這裡為止
    int sortBorder = arr.length-1;
    for (int i = 0; i < arr.length - 1; i++) {
        //是否已經有序的標記,預設有序
        boolean isSorted = true;
        for (int j = 0; j < sortBorder; j++) {
            int tmp = 0;
            //升序排序>,降序排序<
            if (arr[j] > arr[j + 1]){
                tmp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = tmp;
                //發生元素交換,序列仍是無序狀態
                isSorted = false;
                //更新為最後一次交換元素的位置
                lastExchangeIndex = j;
            }
        }
        //更新無序數列的邊界
        sortBorder = lastExchangeIndex;
        if (isSorted){
            break;
        }
    }
}

在第3版程式碼中,sortBorder就是無序數列的邊界。在每一輪排序過程中,處於sortBorder之後的元素就不需要再進行比較了,肯定是有序的

演算法升級

分析

冒泡演算法的每一輪都是從左到右來比較元素,進行單向的位置交換的,是單向的

以新的無序數列 {2,3,4,5,6,7,8,1}為例,按照氣泡排序的演算法,排序過程如下:

事實上,前面的[2,3,4,5,6,7,8]已經是有序了,只有元素1的位置不正確,卻要進行7輪交換。可以將演算法從單向交換改為雙向交換,排序過程就像鐘擺一樣,第1輪從左到右,第2輪從右到左,第3輪再從左到右……,這就是雞尾酒排序.

雞尾酒排序

圖解雞尾酒排序:

經過2輪交換(雖然實際上已經有序,但是流程並沒有結束),進入第3輪交換從左到右進行,1和2比較,位置不變;2和3比較,位置不變;3和4比較,位置不變……6和7比較,位置不變。
沒有元素位置進行交換,證明已經有序,排序結束。

public static void cockTailSort(int[] arr){
    if (arr == null || arr.length == 0) return;
    // 記錄右側最後一次交換的位置
    int lastRightExchangeIndex = 0;
    // 記錄左側最後一次交換的位置
    int lastLeftExchangeIndex = 0;
    // 無序數列的右邊界,每次比較只需要比到這裡為止
    int rightSortBorder = arr.length - 1;
    // 無序數列的左邊界,每次比較只需要比到這裡為止
    int leftSortBorder = 0;

    //i設定為1,代表從第1輪開始
    for (int i = 1; i < arr.length; i++) {
        boolean isSorted = true;
        //奇數,從左到右
        if (i % 2 != 0) {
            for (int j = leftSortBorder; j < rightSortBorder; j++) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    //發生元素交換,序列仍是無序狀態
                    isSorted = false;
                    //更新為右側最後一次交換元素的位置
                    lastRightExchangeIndex = j;
                }
            }
        } else {
            //偶數,從右到左
            for (int j = rightSortBorder; j > leftSortBorder; j--) {
                if (arr[j] < arr[j - 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j - 1];
                    arr[j - 1] = temp;
                    //發生元素交換,序列仍是無序狀態
                    isSorted = false;
                    //更新為左側最後一次交換元素的位置
                    lastLeftExchangeIndex = j;
                }
            }
        }
        //更新無序數列的左邊界
        leftSortBorder = lastLeftExchangeIndex;
        //更新無序數列的右邊界
        rightSortBorder = lastRightExchangeIndex;
        if (isSorted) {
            break;
        }
    }

}

優缺點:雞尾酒排序的優點是能夠在特定條件下,減少排序的回合數;而缺點也很明顯,就是程式碼量幾乎增加了1倍。

應用場景:無序數列中大部分元素已經有序

參考圖書:《漫畫演算法—小灰的演算法之旅》

歡迎關注我的掘金

相關文章