圖解排序算法之「冒泡排序」(詳細解析)

1. 基本思想

冒泡排序(Bubble Sort)是最基礎的排序算法之一,它的核心思想是:多次遍歷要排序的序列,在遍歷的過程中,當發現兩個相鄰的元素逆序,就交換這兩個元素的位置,直到某次遍歷不需要交換元素爲止。此時整個序列都不存在兩個元素逆序的情況,即滿足了順序要求。

爲了讓大家對冒泡排序有更加清晰的認識,我們以下面這組數據作爲例子來演示冒泡排序:

現在,我們需要對包含 8 個元素的序列 [1, 9, 2, 6, 0, 8, 1, 7] 進行升序(從小到大)排序。

按照冒泡排序的思想,我們需要遍歷這個序列,如果遍歷的過程發現相鄰元素中,左邊的元素大於右邊的元素時,就交換這兩個元素的位置。如果是左邊的元素小於所等於右邊的元素時,滿足從小到大的順序要求,元素位置維持不變。下面就是一趟遍歷的過程:

第一趟遍歷下來,9 作爲未排序區間中的最大元素,如同冒泡般上浮到了最右側,形成了已排序區間的第一個元素。

我們下面接着第二趟:

走完第二趟之後,我們可以看到 8 作爲未排序區間中的最大元素“上浮”到了未排序區間的最右側,形成了已排序區間的第二個元素。

其實看到這裏,大家都應該明白了,我們只要多再進行 727 - 2 趟遍歷,就能將已排序區間擴大到整個序列,完成冒泡排序的過程,使得序列中所有的元素都是有序的。這便是冒泡排序的過程,如果你還不懂的話,推薦你自己在紙上手動模擬一遍

聽說還有人分不清楚選擇排序和冒泡排序,這裏簡單說一下:選擇排序的一次遍歷僅是爲了在未排序的區間中選出最值元素,然後放到已排序區間中;而冒泡排序的一次遍歷是爲了讓較大的元素向未排序區間末端方向“上浮”,造成的結果不僅是最值元素被移到末端,還有過程中較大元素往“末端方向”移動,上面例子的第二趟遍歷就很好地體現了這一過程。

2. 代碼實現

冒泡排序的代碼非常簡單,直接上去就是兩層 for 循環,給人一種暴力的直視感。外層循環控制遍歷的趟數,內層循環控制控制每趟遍歷訪問區間的範圍。if 條件句用來判斷相鄰元素是否逆序,裏面的三行則是經典的元素交換語句。冒泡排序,一氣呵成~


public void bubbleSort(int[] arr, int n) {
    //控制遍歷的趟數
	for (int i = 0; i < n - 1; i++) {
        //控制遍歷區間的範圍
		for (int j = 0; j < n - i - 1; j++) {
            //判斷是否逆序
			if (arr[j] > arr[j + 1]) {
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
}

我們可以從上面代碼得出,冒泡排序的時間複雜度爲 O(n2)O(n^2)。由於在兩個元素相等的時候,並不會執行交換位置的操作,所以相等元素在排序前後相對順序是不變的,即冒泡排序是一種穩定排序。

3. 優化

雖然說冒泡排序的最壞時間複雜度是 O(n2)O(n^2),但是它的最佳時間複雜度應該是 O(n)O(n)。換句話說,上面提供的代碼其實還有很大的優化空間。因爲冒泡排序是一種基於比較的排序,我們在思考優化的時候,可以往減少比較次數的方向思考。

優化點1:其實這個優化點已經寫在了開頭的“基本思想”裏面,我們算法只需要執行到某趟遍歷不需要交換元素位置即可,因爲此時所有相鄰元素都滿足順序條件,不需要再繼續遍歷。

在代碼層面,我們只需要加上一個標誌變量,用於記錄在這一趟遍歷中是否發了元素交換,如果有則繼續下一趟遍歷,否則停止循環,完成排序。對應的優化版代碼如下:

public void bubbleSort(int[] arr, int n) {
	for (int i = 0; i < n - 1; i++) {
		boolean isSorted = true; // #1 添加標記
		for (int j = 0; j < n - i - 1; j++) {
			if (arr[j] > arr[j + 1]) {
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
				isSorted = false; // #2 發生了交換
			}
		}
		if (isSorted) {
			break;  // #3 如果沒有發生交換,則完成了排序
		}
	}
}

優化點2:這個優化點單純通過思考有點難想到,需要對排序過程進行觀察和模擬纔會感受到。其實,每趟遍歷的區間右邊界都是由上一趟遍歷最後一次發生交換的位置決定的。因爲如果這個位置之後都沒有發生交換,就說明之後的元素都是非逆序的,我們可以將有序區間的左邊界直接擴展到最後一次發生交換的下一個位置。

在代碼層面,我們可以將這兩個優化點合併在一起,詳情代碼如下:

public void bubbleSort(int[] arr, int n) {
	int lastIndex = n - 1;  // #1 有序區間的左邊界
	for (int i = 0; i < n - 1; i++) {
		int tempIndex = lastIndex; // #2 標記
        // #3 遍歷的右邊界變成了lastIndex
		for (int j = 0; j < lastIndex; j++) {
			if (arr[j] > arr[j + 1]) {
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
				tempIndex = j;  // #4 記錄發生交換的位置
			}
		}
		// #5 如果標記的值不變,則說明沒有發生交換
		if (tempIndex == lastIndex) {
			break;  
		} else {
			lastIndex = tempIndex;
		}
	}
}

至此,冒泡排序的優化就搞完了。因爲在大多數時候我們排序的數據並不都是在極端情況,加了優化的冒泡排序能在很多時候比同樣是 O(n2)O(n^2) 的普通選擇排序有更加優異的表現。


順手更新一篇水文,希望你也可以順手點贊呀~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章