人人都能學會的數據結構與算法
今天分享的是三種排序算法,在面試、實際編程中經常會碰到和使用到的,我會帶領大家從分析排序算法技巧上以及代碼實現上全面理解這一知識點的掌握。
一、如何分析一個「排序算法」
1. 執行效率
① 最好、最壞、平均時間複雜度
在分析算法的好壞時,要分別說出最好、最壞、平均時間複雜度的同時,也要說出最好、最壞時間複雜度對應排序的原始數據是什麼樣的。
② 複雜度係數、常數、低階
時間複雜度反應的是數據規模 n 很大的時候的一個增長趨勢,它表示的時候會忽略係數、常數、低階 ,小規模數據除外。
③ 比較次數和移動次數
基於比較的排序算法,在分析算法效率時,我們要考慮到元素的比較和元素的移動。
2. 內存消耗
算法的內存消耗可以通過空間複雜度來衡量,排序算法也不例外。我們引用一個名詞叫做「原地排序」,就是指特定空間複雜度是 O(1) 的排序算法。
3. 穩定性
如果待排序的序列中存在值相等的元素,經過排序之後,相等元素之間原有的先後順序不變,就叫做「穩定排序」。
二、冒泡排序
1、算法思想
每次冒泡對相鄰的兩個元素進行比較,看是否滿足大小關係,不滿足就進行互換,一次冒泡會讓至少一個元素移動到它應該在的位置。有 n 個數據,需要重複 n 次。
2、算法優化
當某次冒泡過程已經沒有數據交換時,說明已經達到完全有序,不用再執行後續的冒泡操作。
3、代碼實現
1// 冒泡排序,a 表示數組,n 表示數組大小 2public void bubbleSort(int[] a, int n) { 3 if (n <= 1) return; 4 5 for (int i = 0; i < n; ++i) { 6 // 提前退出冒泡循環的標誌位 7 boolean flag = false; 8 for (int j = 0; j < n - i - 1; ++j) { 9 if (a[j] > a[j+1]) { // 交換 10 int tmp = a[j]; 11 a[j] = a[j+1]; 12 a[j+1] = tmp; 13 flag = true; // 表示有數據交換 14 } 15 } 16 if (!flag) break; // 沒有數據交換,提前退出 17 } 18}
4、問題思考
①是否爲原地排序
冒泡的過程只涉及相鄰數據的交換操作,只需要常量級的臨時空間,所以它的空間複雜度爲 O(1),是一個原地排序算法。
②是否爲穩定排序
在冒泡排序中,只有交換纔可以改變兩個元素的前後順序。爲了保證冒泡排序算法的穩定性,當有相鄰的兩個元素大小相等的時候,我們不做交換,相同大小的數據在排序前後不會改變順序,所以冒泡排序是穩定的排序算法。
③最好、最壞以及平均時間複雜度
最好的情況是數據已經排好序,我們只進行一次冒泡排序就可以了,最好時間複雜度爲 O(n) 。最壞的情況是,要排序的數據剛好是倒序排列的,我們只進行 n 此冒泡操作,所以最壞的時間複雜度爲 O(n²),平均時間複雜度爲 O(n²)。
三、插入排序(Insertion Sort)
1、算法思想
我們將元素分爲兩個區間,未排序區間和已排序區間。我們要做的就是在未排序區間取出元素與已排序區間元素進行比較插入到適當位置,以此類推,直到未排序區間元素爲空爲止(順序爲從後向前比較)。
2、代碼實現
1// 插入排序,a 表示數組,n 表示數組大小(從小到大進行排序) 2public void insertionSort(int[] a, int n) { 3 //如果數組大小爲 1 直接返回 4 if (n <= 1) return; 5 //否則進行插入排序 6 for (int i = 1; i < n; ++i) { 7 int value = a[i]; 8 int j = i - 1; 9 // 查找插入的位置 10 for (; j >= 0; --j) { 11 if (a[j] > value) { 12 a[j+1] = a[j]; // 數據移動 13 } else { 14 break; 15 } 16 } 17 a[j+1] = value; // 插入數據 18 } 19}
3、問題思考
① 是否爲原地排序?
答:插入排序的運算並不需要額外的存儲空間,所以空間複雜度是 O(1),是一個原地排序算法。
② 是否爲穩定排序?
答:在插入排序中,對於值相同的元素,我們會將後邊出現的元素插入到前邊出現的元素的後邊,所以插入排序是穩定排序。
③ 最好、最壞、平均時間複雜度?
答:
最好的情況就是數據元素已經排好序,最好的時間複雜度爲 O(1) ,
如果數組是倒序的,每次插入都相當於在數組的第一個位置插入新的數據,需要移動大量的數據,最壞的時間複雜度是 O(n²)。
我們在數組中插入數據的平均時間複雜度爲 O(n),對於插入排序來說我們每次就相當於數組插入一個新的數據,循環執行n次插入數據,所以平均時間複雜度爲 O(n²)。
四、選擇排序
1、算法思想
和插入排序有點相似,將在未排序期間尋找到最小的數據,並將其放到已排好區間的元素的尾部。
2、問題思考
① 是否爲原地排序
因爲,數組中的兩個元素需要相互交換,需要用一個變量來存儲交換值,選擇排序的空間複雜度爲O(1),所以,是一種原地排序算法。
② 是否爲穩定排序
選擇排序每次都要找到剩餘未排序元素的最小值,並和前邊的元素交換位置,這樣破壞了穩定性。所以說,選擇排序是一種不穩定的排序算法。
③ 最好、最壞以及平均時間複雜度
選擇排序的最好情況就是已經是一組有序數據,最好的時間複雜度爲 O(1),最壞的情況就是 O(n²)。平均時間複雜度就是 O(n²)。
3、代碼實現
1// 選擇排序,a表示數組,n表示數組大小 2 public static void selectionSort(int[] a, int n) { 3 if (n <= 1) return; 4 for (int i = 0; i < n; ++i) { 5 // 查找最小值 6 int minIndex = i; 7 int minValue = a[i]; 8 for (int j = i; j < n; ++j) { 9 if (a[j] < minValue) { 10 minValue = a[j]; 11 for (int i = 0; i < n - 1; ++i) { 12 // 查找最小值 13 int minIndex = i; 14 for (int j = i + 1; j < n; ++j) { 15 if (a[j] < a[minIndex]) { 16 minIndex = j; 17 } 18 } 19 if (minIndex == i) 20 continue; 21 // 交換 22 int tmp = a[i]; 23 a[i] = a[minIndex]; 24 a[minIndex] = tmp; 25 } 26 }
五、實際應用中爲什麼插入排序應用最爲廣泛
冒泡排序不管怎麼優化,元素交換的次數是一個固定值,是原始數據的逆序度。插入排序是同樣的,不管怎麼優化,元素移動的次數也等於原始數據的逆序度。
從代碼實現上來看,冒泡排序的數據交換要比插入排序的數據移動要複雜,冒泡排序需要 3 個賦值操作,而插入排序只需要 1 個。
1冒泡排序中數據的交換操作: 2if (a[j] > a[j+1]) { // 交換 3 int tmp = a[j]; 4 a[j] = a[j+1]; 5 a[j+1] = tmp; 6 flag = true; 7} 8 9插入排序中數據的移動操作: 10if (a[j] > value) { 11 a[j+1] = a[j]; // 數據移動 12} else { 13 break; 14}
有興趣的小夥伴可以編幾個數據自己測試一下。
雖然冒泡排序和插入排序在在時間複雜度上是一樣的,都是 O(n²),我們希望把性能優化做到極致,首選插入排序。
六、重點掌握
今天重點掌握的內容是三種排序的「分析方法」,不必要死記硬背。另一個方面就是實際應用中用到最多就是「插入排序」。
七、擴展思考
上述小鹿講到的是用數組實現的排序,加入我們將數組換成鏈表,以上的分析方法是否還適合?以及最好、最壞、平均時間複雜度改變了沒有?