一看就懂的氣泡排序和【3】步深度優化法

微光shimmer發表於2019-07-06
如需轉載,請標明地址

前言:各位小夥伴們,氣泡排序作為我入門程式設計第一個遇到的演算法,對我來說意義非凡。今天閒來重新拾起了這個演算法,發現它竟然還有這麼大的優化空間,驚訝。那我們就來優化一下它吧!寫這篇文章呢主要是想和在座的各位小夥伴分享一下我的優化歷程,二來還可以方便以後複習。廢話不多說。我們直接開始吧!


相比大家對氣泡排序法還是不陌生的,如果你是剛剛接觸程式設計也沒關係,請看我慢慢給你解答!

基礎比較好的小夥伴可以直接略過

什麼是氣泡排序?


氣泡排序(Bubble Sort)是一種較簡單的排序演算法。

通過比較兩個相鄰陣列元素來達到由大(xiao)到小(da)排序陣列的目的

我這麼說是不是能明白一點呢?不明白也沒關係,就讓我們一起來看程式碼吧!

int[] array = {
    9, 8, 7, 6, 5, 4, 3, 2, 1, 0
};
複製程式碼

這是一個0-9的倒序排列的陣列,我們通過相鄰元素的下標比對然後互換來完成從小到大的排序,如圖:

一看就懂的氣泡排序和【3】步深度優化法

一看就懂的氣泡排序和【3】步深度優化法

一看就懂的氣泡排序和【3】步深度優化法

一看就懂的氣泡排序和【3】步深度優化法

一看就懂的氣泡排序和【3】步深度優化法

一看就懂的氣泡排序和【3】步深度優化法

一看就懂的氣泡排序和【3】步深度優化法

一看就懂的氣泡排序和【3】步深度優化法

一看就懂的氣泡排序和【3】步深度優化法

一看就懂的氣泡排序和【3】步深度優化法

這樣我們就完成了將9放到了陣列的最後。完成了一次排序。


怎麼樣,你是不是能明白了呢?

說到這裡,那我們如何用程式碼來實現呢?

int[] array = {
    9, 8, 7, 6, 5, 4, 3, 2, 1, 0
};
// 一次遍歷,將相對最大的數放到陣列底部
for (int j = 0; j < array.length - 1; j++) {
    if (array[j] > array[j + 1]) {
        int max = array[j];
        array[j] = array[j + 1];
        array[j + 1] = max;
    }
}
複製程式碼

輸出的結果: [8, 7, 6, 5, 4, 3, 2, 1, 0, 9]


那怎麼完成所有元素的排序呢?

那就太好辦了!再加一個迴圈吧!

int[] array = {
    9, 8, 7, 6, 5, 4, 3, 2, 1, 0
};
int max = 0;

// 一次遍歷,在倒序情況下最少遍歷的次數
for (int i = 0; i < arrays.length - 1; i++) {
    // 二次遍歷,將相對最大的數放到陣列底部
    for (int j = 0; j < array.length - 1; j++) {
        if (array[j] > array[j + 1]) {
            max = array[j];
            array[j] = array[j + 1];
            array[j + 1] = max;
        }
    }
}
複製程式碼

輸出結果: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


做到這裡,我們發現了,這樣的寫法並不完美,有很多紕漏。大大影響了程式的效能。那我們應該怎麼去優化呢?

我們的目的:

  • 增加迴圈效率
  • 減少無用的迴圈遍歷和判斷

最有用的辦法就是觀察演算法的有效遍歷數和實際遍歷數


第一步優化

那我們就想辦法先為程式減少一些迴圈吧!

我發現在執行二次遍歷時,程式越執行到後面,所做的排序就越少,因為陣列後面的元素都已排序完成,無需再進行迴圈判斷

這樣一想,我們的優化方案就有了!

一次遍歷的計數(i)就相當於我們陣列已經排好元素的個數

減去(i),就可以減少迴圈次數

int[] array = {
    9, 8, 7, 6, 5, 4, 3, 2, 1, 0
};
// 程式有效執行的次數
int runCount = 0;
// 一共遍歷的次數
int allCount = 0;
int max = 0;

// 一次遍歷,在倒序情況下最少遍歷的次數
for (int i = 0; i < array.length - 1; i++) {
    // 二次遍歷,將相對最大的數放到陣列底部
    for (int j = 0; j < array.length - 1 - i; j++) {
            if (array[j] > array[j + 1]) {
                max = array[j];
                array[j] = array[j + 1];
                array[j + 1] = max;
                runCount += 1;
        }
        allCount += 1;
    }
}
System.out.println("runCount = " + runCount);
System.out.println("allCount = " + allCount);
System.out.println(Arrays.toString(array));
複製程式碼

輸出結果:

runCount = 45
allCount = 45
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
複製程式碼

從結果來看,有效遍歷和實際遍歷次數相同。

但是這是在極端情況下(完全倒序),我們拿到的陣列大多數情況都是無序散亂的。這樣的優化明顯不能滿足我們的要求。這又為第二次優化提供了思路...


第二步優化

往往散亂的陣列實際所需的遍歷次數是遠小於極端情況(完全倒序)的,然而我們程式還是會進行迴圈遍歷.

那我們不如做個判斷,判斷它是否需要進行實際遍歷,如果不需要了。那陣列肯定是排序完成了!那我們就可以跳出迴圈了。

這樣一想,我們的優化方案又有了!

我們用無序陣列進行測試

int[] array = {
    3, 6, 2, 7, 9, 5, 0, 1, 4, 8
};
// 程式有效執行的次數
int runCount = 0;
// 一共遍歷的次數
int allCount = 0;
int max = 0;
// flag判斷排序是否完成 true-完成;false-未完成
boolean flag;

// 一次遍歷,在倒序情況下最少遍歷的次數
for (int i = 0; i < array.length - 1; i++) {
    // 每次迴圈重置flag為true
    flag = true;
    // 二次遍歷,將相對最大的數放到陣列底部
    for (int j = 0; j < array.length - 1 - i; j++) {
            if (array[j] > array[j + 1]) {
                max = array[j];
                array[j] = array[j + 1];
                array[j + 1] = max;
                runCount += 1;
                // 進入迴圈表示陣列未排序完成,需再次迴圈
                flag = false;
        }
        allCount += 1;
    }
    // 如果已經完成排序,則跳出迴圈
    if (flag) {
        break;
    }
    
}
System.out.println("runCount = " + runCount);
System.out.println("allCount = " + allCount);
System.out.println(Arrays.toString(array));
複製程式碼

輸出結果:

runCount = 22
allCount = 42
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
複製程式碼

未加判斷輸出結果:

runCount = 22
allCount = 45
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
複製程式碼

從結果可以看出來,比未加判斷的實際遍歷次數少了3次。

但是優化到此為止好像還是缺了點什麼,如果陣列天生有一部分就是無需排序的,那我們又會浪費很多次的迴圈,這麼一想,第三步優化就有了方向。


第三步優化

如圖:

一看就懂的氣泡排序和【3】步深度優化法

後面的 5 6 7 8 9 本來就是排序完成的,那按照我們的程式碼還要去對後面的程式碼進行迴圈遍歷,那樣是很不科學的!

這樣一想,我們的優化方案就完美了!

我用幾個變數來動態記錄陣列所需遍歷的次數就可以解決問題了。

int[] array = {
    3, 6, 2, 7, 9, 5, 0, 1, 4, 8
};
// 程式有效執行的次數
int runCount = 0;
// 一共遍歷的次數
int allCount = 0;
int max = 0;
// flag判斷排序是否完成 true-完成;false-未完成
boolean flag;
// 無序陣列迴圈邊界,預設為陣列長度array.length - 1
int sortBorder = array.length - 1;
//  記錄陣列最後進行排序的位置
int lastChange = 0;

// 一次遍歷,在倒序情況下最少遍歷的次數
for (int i = 0; i < array.length - 1; i++) {
    // 每次迴圈重置flag為true
    flag = true;
    // 二次遍歷,將相對最大的數放到陣列底部
    for (int j = 0; j < sortBorder; j++) {
            if (array[j] > array[j + 1]) {
                max = array[j];
                array[j] = array[j + 1];
                array[j + 1] = max;
                runCount += 1;
                // 進入迴圈表示陣列未排序完成,需再次迴圈
                flag = false;
                // 記錄陣列最後進行排序的位置
                lastChange = j;
        }
        allCount += 1;
    }
    // 動態設定無序陣列迴圈邊界
    sortBorder = lastChange;
    // 如果已經完成排序,則跳出迴圈
    if (flag) {
        break;
    }
}
System.out.println("runCount = " + runCount);
System.out.println("allCount = " + allCount);
System.out.println(Arrays.toString(array));
複製程式碼

輸出結果:

runCount = 22
allCount = 35
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
複製程式碼

優化到這一步我們基本的需求就已經完成了,有效遍歷和實際遍歷次數已經相當接近了。

有的小夥伴會問了:怎麼還是多出來 13 次啊?

我覺得在目前看來多於的次數對於有效遍歷提供了一定的幫助,所以並不是完全無效的。

不懂的小夥伴可以複製程式碼進行 bebug 也可以直接問我。但是不要停止思考哦。說不定你就找出更好的優化方案了呢!

最後還是感謝各位小夥伴能夠看到最後。文章有什麼出錯的地方歡迎指出改正。

相關文章