冒泡排序是入門級的算法,但也有一些有趣的玩法。一般來講,冒泡排序有三種寫法:java
public static void bubbleSort(int[] arr) { 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]) { // 若是左邊的數大於右邊的數,則交換,保證右邊的數字最大 swap(arr, j, j + 1); } } } } // 交換元素 private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
最外層的 for 循環每通過一輪,剩餘數字中的最大值就會被移動到當前輪次的最後一位,中途也會有一些相鄰的數字通過交換變得有序。總共比較次數是 (n-1)+(n-2)+(n-3)+…+1(n−1)+(n−2)+(n−3)+…+1。
這種寫法至關於相鄰的數字兩兩比較,而且規定:「誰大誰站右邊」。通過 n-1n−1 輪,數字就從小到大排序完成了。整個過程看起來就像一個個氣泡不斷上浮,這也是「冒泡排序法」名字的由來。算法
在第一種的基礎上改良而來數組
public static void bubbleSort(int[] arr) { // 初始時 swapped 爲 true,不然排序過程沒法啓動 boolean swapped = true; for (int i = 0; i < arr.length - 1; i++) { // 若是沒有發生過交換,說明剩餘部分已經有序,排序完成 if (!swapped) break; // 設置 swapped 爲 false,若是發生交換,則將其置爲 true swapped = false; for (int j = 0; j < arr.length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { // 若是左邊的數大於右邊的數,則交換,保證右邊的數字最大 swap(arr, j, j + 1); // 表示發生了交換 swapped = true; } } } } // 交換元素 private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
最外層的 for 循環每通過一輪,剩餘數字中的最大值仍然是被移動到當前輪次的最後一位。這種寫法相對於第一種寫法的優勢是:若是一輪比較中沒有發生過交換,則當即中止排序,由於此時剩餘數字必定已經有序了。app
比較少見,在第二種的基礎上進一步優化函數
public static void bubbleSort(int[] arr) { boolean swapped = true; // 最後一個沒有通過排序的元素的下標 int indexOfLastUnsortedElement = arr.length - 1; // 上次發生交換的位置 int swappedIndex = -1; while (swapped) { swapped = false; for (int i = 0; i < indexOfLastUnsortedElement; i++) { if (arr[i] > arr[i + 1]) { // 若是左邊的數大於右邊的數,則交換,保證右邊的數字最大 swap(arr, i, i + 1); // 表示發生了交換 swapped = true; // 更新交換的位置 swappedIndex = i; } } // 最後一個沒有通過排序的元素的下標就是最後一次發生交換的位置 indexOfLastUnsortedElement = swappedIndex; } } // 交換元素 private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
通過再一次的優化,代碼看起來就稍微有點複雜了。最外層的 while 循環每通過一輪,剩餘數字中的最大值仍然是被移動到當前輪次的最後一位。
在下一輪比較時,只需比較到上一輪比較中,最後一次發生交換的位置便可。由於後面的全部元素都沒有發生過交換,必然已經有序了。
當一輪比較中從頭至尾都沒有發生過交換,則表示整個列表已經有序,排序完成。性能
空間複雜度爲 O(1),時間複雜度爲 O(n^2),
第二種、第三種冒泡排序因爲通過優化,最好的狀況下只須要 O(n)的時間複雜度。
最好狀況:在數組已經有序的狀況下,只需遍歷一次,因爲沒有發生交換,排序結束。
最差狀況:數組順序爲逆序,每次比較都會發生交換。
但優化後的冒泡排序平均時間複雜度仍然是 O(n^2),因此這些優化對算法的性能並無質的提高。優化
選擇排序的思想是:雙重循環遍歷數組,每通過一輪比較,找到最小元素的下標,將其交換至首位。code
public static void selectionSort(int[] arr) { int minIndex; for (int i = 0; i < arr.length - 1; i++) { minIndex = i; for (int j = i + 1; j < arr.length; j++) { if (arr[minIndex] > arr[j]) { // 記錄最小值的下標 minIndex = j; } } // 將最小元素交換至首位 int temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; } }
冒泡排序和選擇排序有什麼異同?
相同點:對象
不一樣點:排序
假定在待排序的記錄序列中,存在多個具備相同的關鍵字的記錄,若通過排序,這些記錄的相對次序保持不變,即在原序列中,r[i] = r[j],且 r[i] 在 r[j] 以前,而在排序後的序列中,r[i] 仍在 r[j] 以前,則稱這種排序算法是穩定的;不然稱爲不穩定的。
因此,
冒泡排序中,只有左邊的數字大於右邊的數字時纔會發生交換,相等的數字之間不會發生交換,因此它是穩定的。
而選擇排序中,最小值和首位交換的過程可能會破壞穩定性。好比數列:[2, 2, 1],在選擇排序中第一次進行交換時,原數列中的兩個 2 的相對順序就被改變了,所以,咱們說選擇排序是不穩定的
排序算法的穩定性有什麼意義呢,其實它只在一種狀況下有意義:當要排序的內容是一個對象的多個屬性,且其本來的順序存在乎義時,若是咱們須要在二次排序後保持原有排序的意義,就須要使用到穩定性的算法。
舉個例子,若是咱們要對一組商品排序,商品存在兩個屬性:價格和銷量。當咱們按照價格從高到低排序後,要再按照銷量對其排序,這時,若是要保證銷量相同的商品仍保持價格從高到低的順序,就必須使用穩定性算法。
固然,算法的穩定性與具體的實現有關。在修改比較的條件後,穩定性排序算法可能會變成不穩定的。如冒泡算法中,若是將「左邊的數大於右邊的數,則交換」這個條件修改成「左邊的數大於或等於右邊的數,則交換」,冒泡算法就變得不穩定了。
一樣地,不穩定排序算法也能夠通過修改,達到穩定的效果。思考一下,選擇排序算法如何實現穩定排序呢?
實現的方式有不少種,這裏給出一種最簡單的思路:新開一個數組,將每輪找出的最小值依次添加到新數組中,選擇排序算法就變成穩定的了。
但若是將尋找最小值的比較條件由arr[minIndex] > arr[j]修改成arr[minIndex] >= arr[j],即便新開一個數組,選擇排序算法依舊是不穩定的。因此分析算法的穩定性時,須要結合具體的實現邏輯才能得出結論,咱們一般所說的算法穩定性是基於通常實現而言的。
既然每輪遍歷時找出了最小值,何不把最大值也順便找出來呢?這就是二元選擇排序的思想。
使用二元選擇排序,每輪選擇時記錄最小值和最大值,能夠把數組須要遍歷的範圍縮小一倍。
public static void selectionSort2(int[] arr) { int minIndex, maxIndex; // i 只須要遍歷一半 for (int i = 0; i < arr.length / 2; i++) { minIndex = i; maxIndex = i; for (int j = i + 1; j < arr.length - i; j++) { if (arr[minIndex] > arr[j]) { // 記錄最小值的下標 minIndex = j; } if (arr[maxIndex] < arr[j]) { // 記錄最大值的下標 maxIndex = j; } } // 若是 minIndex 和 maxIndex 都相等,那麼他們一定都等於 i,且後面的全部數字都與 arr[i] 相等,此時已經排序完成 if (minIndex == maxIndex) break; // 將最小元素交換至首位 int temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; // 若是最大值的下標恰好是 i,因爲 arr[i] 和 arr[minIndex] 已經交換了,因此這裏要更新 maxIndex 的值。 if (maxIndex == i) maxIndex = minIndex; // 將最大元素交換至末尾 int lastIndex = arr.length - 1 - i; temp = arr[lastIndex]; arr[lastIndex] = arr[maxIndex]; arr[maxIndex] = temp; } }
咱們使用 minIndex 記錄最小值的下標,maxIndex 記錄最大值的下標。每次遍歷後,將最小值交換到首位,最大值交換到末尾,就完成了排序。
因爲每一輪遍歷能夠排好兩個數字,因此最外層的遍歷只需遍歷一半便可。
二元選擇排序中有一句很重要的代碼,它位於交換最小值和交換最大值的代碼中間:
if (maxIndex == i) maxIndex = minIndex;
這行代碼的做用處理了一種特殊狀況:若是最大值的下標等於 i,也就是說 arr[i] 就是最大值,因爲 arr[i] 是當前遍歷輪次的首位,它已經和 arr[minIndex] 交換了,因此最大值的下標須要跟蹤到 arr[i] 最新的下標 minIndex。
在二元選擇排序算法中,數組須要遍歷的範圍縮小了一倍。那麼這樣可使選擇排序的效率提高一倍嗎?
從代碼能夠看出,雖然二元選擇排序最外層的遍歷範圍縮小了,但 for 循環內作的事情翻了一倍。也就是說二元選擇排序沒法將選擇排序的效率提高一倍。但實測會發現二元選擇排序的速度確實比選擇排序的速度快一點點,它的速度提高主要是由於兩點:
前文已經說到,選擇排序使用兩層循環,時間複雜度爲 O(n^2); 只使用有限個變量,空間複雜度 O(1)。二元選擇排序雖然比選擇排序要快,但治標不治本,二元選擇排序中作的優化沒法改變其時間複雜度,二元選擇排序的時間複雜度仍然是 O(n^2);只使用有限個變量,空間複雜度 O(1)。
插入排序的思想很是簡單,生活中有一個很常見的場景:在打撲克牌時,咱們一邊抓牌一邊給撲克牌排序,每次摸一張牌,就將它插入手上已有的牌中合適的位置,逐漸完成整個排序。
插入排序有兩種寫法:
public static void insertSort(int[] arr) { // 從第二個數開始,往前插入數字 for (int i = 1; i < arr.length; i++) { // j 記錄當前數字下標 int j = i; // 當前數字比前一個數字小,則將當前數字與前一個數字交換 while (j >= 1 && arr[j] < arr[j - 1]) { swap(arr, j, j - 1); // 更新當前數字下標 j--; } } } private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
當數字少於兩個時,不存在排序問題,固然也不須要插入,因此咱們直接從第二個數字開始往前插入。
整個過程就像是已經有一些數字坐成了一排,這時一個新的數字要加入,這個新加入的數字本來坐在這一排數字的最後一位,而後它不斷地與前面的數字比較,若是前面的數字比它大,它就和前面的數字交換位置。
能夠發現,在交換法插入排序中,每次交換數字時,swap 函數都會進行三次賦值操做。但實際上,新插入的這個數字並不必定適合與它交換的數字所在的位置。也就是說,它剛換到新的位置上不久,下一次比較後,若是又須要交換,它立刻又會被換到前一個數字的位置。
由此,咱們能夠想到一種優化方案:讓新插入的數字先進行比較,前面比它大的數字不斷向後移動,直到找到適合這個新數字的位置後,新數字只作一次插入操做便可。
這種方案咱們須要把新插入的數字暫存起來,代碼以下:
public static void insertSort(int[] arr) { // 從第二個數開始,往前插入數字 for (int i = 1; i < arr.length; i++) { int currentNumber = arr[i]; int j = i - 1; // 尋找插入位置的過程當中,不斷地將比 currentNumber 大的數字向後挪 while (j >= 0 && currentNumber < arr[j]) { arr[j + 1] = arr[j]; j--; } // 兩種狀況會跳出循環:1. 遇到一個小於或等於 currentNumber 的數字,跳出循環,currentNumber 就坐到它後面。 // 2. 已經走到數列頭部,仍然沒有遇到小於或等於 currentNumber 的數字,也會跳出循環,此時 j 等於 -1,currentNumber 就坐到數列頭部。 arr[j + 1] = currentNumber; } }
整個過程就像是已經有一些數字坐成了一排,這時一個新的數字要加入,因此這一排數字不斷地向後騰出位置,當新的數字找到本身合適的位置後,就能夠直接坐下了。重複此過程,直到排序結束。
分析可知,插入排序的過程不會破壞原有數組中相同關鍵字的相對次序,因此插入排序是一種穩定的排序算法。
插入排序過程須要兩層循環,時間複雜度爲 O(n^2);只須要常量級的臨時變量,空間複雜度爲 O(1)。
本章咱們介紹了三種基礎排序算法:冒泡排序、選擇排序、插入排序。
冒泡排序有兩種優化方式:
選擇排序能夠演變爲二元選擇排序:
插入排序有兩種寫法: