如需轉載,請標明地址
前言:各位小夥伴們,氣泡排序作為我入門程式設計第一個遇到的演算法,對我來說意義非凡。今天閒來重新拾起了這個演算法,發現它竟然還有這麼大的優化空間,驚訝。那我們就來優化一下它吧!寫這篇文章呢主要是想和在座的各位小夥伴分享一下我的優化歷程,二來還可以方便以後複習。廢話不多說。我們直接開始吧!
相比大家對氣泡排序法還是不陌生的,如果你是剛剛接觸程式設計也沒關係,請看我慢慢給你解答!
基礎比較好的小夥伴可以直接略過
什麼是氣泡排序?
氣泡排序(Bubble Sort)是一種較簡單的排序演算法。
通過比較兩個相鄰陣列元素來達到由大(xiao)到小(da)排序陣列的目的
我這麼說是不是能明白一點呢?不明白也沒關係,就讓我們一起來看程式碼吧!
int[] array = {
9, 8, 7, 6, 5, 4, 3, 2, 1, 0
};
複製程式碼
這是一個0-9的倒序排列的陣列,我們通過相鄰元素的下標比對然後互換來完成從小到大的排序,如圖:
這樣我們就完成了將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次。
但是優化到此為止好像還是缺了點什麼,如果陣列天生有一部分就是無需排序的,那我們又會浪費很多次的迴圈,這麼一想,第三步優化就有了方向。
第三步優化
如圖:
後面的 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 也可以直接問我。但是不要停止思考哦。說不定你就找出更好的優化方案了呢!