如何分析一個排序算法?
分析一個排序算法的三要素:排序算法的執行效率、排序算法的內存消耗以及排序算法的穩定性。
排序算法的執行效率
對於排序算法執行效率的分析,一般是從以下三個方面來衡量:
- 最好情況、最壞情況、平均情況時間復雜度
- 時間復雜度的系數、常數、低階
- 比較次數和交換(或移動)次數
第1、2點在之前的復雜度分析中我們已經講過了,第3點會在這一節以及接下來的章節中詳細講解。
這一節和下一節講的都是基於比較的排序算法。基於比較的排序算法的執行過程,會涉及兩種操作,一種是元素比較大小,另一種是元素交換或移動。所以,我們如果在分析排序算法的執行效率的時候,應該把比較次數和交換(或移動)次數也考慮進去。
排序算法的內存消耗
算法的內存消耗可以通過空間復雜度來衡量,排序算法也不例外。不過,針對排序算法的空間復雜度,我們還引入了一個新的概念,原地排序(Sorted in place)。原地排序算法,就是特指空間復雜度為 O(1)的排序算法。我們今天所要講的三種排序算法:冒泡排序、插入排序和選擇排序,都是原地排序算法。
排序算法的穩定性
僅僅靠執行效率和內存消耗來衡量排序算法的好壞是不夠的。針對排序算法,我們還有一個重要的度量指標,穩定性。這個概念是說,如果待排序的序列中存在值相等的元素,經過排序之后,相等元素之間原有的先后順序不變。
比如我們有一組數據 2,9,3,4,8,3,按照大小排序之后就是 2,3,3,4,8,9。
這組數據里有兩個 3。經過某種排序算法排序之后,如果兩個 3 的前后順序沒有改變,那我們就把這種排序算法叫作穩定的排序算法;如果前后順序發生變化,那對應的排序算法就叫作不穩定的排序算法。
冒泡排序(Bubble Sort)
(1)基本思想
冒泡排序的基本思想就是:從無序序列頭部開始,進行兩兩比較,根據大小交換位置,直到最后將最大(小)的數據元素交換到了無序隊列的隊尾,從而成為有序序列的一部分;下一次繼續這個過程,直到所有數據元素都排好序。
算法的核心在於每次通過兩兩比較交換位置,選出剩余無序序列里最大(小)的數據元素放到隊尾。
(2)圖片示例
(以上兩點參考鏈接:https://blog.csdn.net/guoweimelon/article/details/50902597)
(3)代碼示例
1 // 冒泡排序,a 表示數組,n 表示數組大小 2 public void bubbleSort(int[] a, int n) { 3 if (n <= 1) { 4 return; 5 } 6 7 for (int i = 0; i < n; ++i) { 8 // 提前退出冒泡循環的標志位 9 boolean flag = false; 10 for (int j = 0; j < n - i - 1; ++j) { 11 if (a[j] > a[j+1]) { // 交換 12 int tmp = a[j]; 13 a[j] = a[j+1]; 14 a[j+1] = tmp; 15 flag = true; // 表示有數據交換 16 } 17 } 18 if (!flag) break; // 沒有數據交換,提前退出 19 } 20 }
結合上文提到的分析排序算法的三要素,我們可以得出以下結論:
- 冒泡排序是原地排序算法:因為冒泡的過程只涉及相鄰數據的交換操作,只需要常量級的臨時空間,所以它的空間復雜度為 O(1),是一個原地排序算法。
- 冒泡排序是穩定的排序算法:在冒泡排序中,只有當相鄰兩個元素大小不相等的時候,我們才做交換,相同大小的數據在排序前后不會改變順序,所以冒泡排序是穩定的排序算法。
- 冒泡排序的時間復雜度是 O(n^2):冒泡排序在要排序的數據都是有序的情況下,我們只需要進行一次冒泡排序就可以結束了,所以最好情況時間復雜度為 O(n)。而在要排序的數據剛好是倒序排列的情況下,則需要進行 n 次冒泡操作,所以最壞情況時間復雜度為 O(n^2)。
插入排序(Insertion Sort)
(1)基本思想
插入排序是一種簡單直觀的排序算法。它的基本思想是通過構建有序序列,對於未排序數據,在已排序序列中從后向前掃描,找到相應位置並插入。
(2)圖片示例
(圖片來自“極客時間”:https://time.geekbang.org/column/article/41802)
(3)代碼示例
1 // 插入排序,a 表示數組,n 表示數組大小 2 public void insertionSort(int[] a, int n) { 3 if (n <= 1) { 4 return; 5 } 6 7 for (int i = 1; i < n; ++i) { 8 int value = a[i]; 9 int j = i - 1; 10 // 查找插入的位置 11 for (; j >= 0; --j) { 12 if (a[j] > value) { 13 a[j+1] = a[j]; // 數據移動 14 } else { 15 break; 16 } 17 } 18 a[j+1] = value; // 插入數據 19 } 20 }
同樣,結合上文提到的分析排序算法的三要素,我們可以得出以下結論:
- 插入排序是原地排序算法:因為插入排序算法的運行並不需要額外的存儲空間,所以它的空間復雜度為 O(1),是一個原地排序算法。
- 插入排序是穩定的排序算法:在插入排序中,對於值相同的元素,我們可以選擇將后面出現的元素,插入到前面出現元素的后面,這樣就可以保持原有的前后順序不變,所以插入排序是穩定的排序算法。
- 插入排序的時間復雜度是 O(n^2):插入排序在要排序的數據都是有序的情況下,我們只需要比較一個數據就能確定插入的位置,所以最好情況時間復雜度為 O(n)。而在要排序的數據剛好是倒序排列的情況下,每次插入都相當於在數組的第一個位置插入新的數據,所以需要移動大量的數據,所以最壞情況時間復雜度為 O(n^2)。
選擇排序(Selection Sort)
(1)基本思想
選擇排序算法的實現思路有點類似插入排序,也分已排序區間和未排序區間。但是選擇排序每次會從未排序區間中找到最小的元素,將其放到已排序區間的末尾。
(2)圖片示例
(圖片來自“極客時間”:https://time.geekbang.org/column/article/41802)
(3)代碼示例
1 //選擇排序 2 public class SelectionSort { 3 public static void main(String[] args) { 4 int[] arr={1,3,2,45,65,33,12}; 5 System.out.println("交換之前:"); 6 for(int num:arr){ 7 System.out.print(num+" "); 8 } 9 //選擇排序的優化 10 for(int i = 0; i < arr.length - 1; i++) {// 做第i趟排序 11 int k = i; 12 for(int j = k + 1; j < arr.length; j++){// 選最小的記錄 13 if(arr[j] < arr[k]){ 14 k = j; //記下目前找到的最小值所在的位置 15 } 16 } 17 //在內層循環結束,也就是找到本輪循環的最小的數以后,再進行交換 18 if(i != k){ //交換a[i]和a[k] 19 int temp = arr[i]; 20 arr[i] = arr[k]; 21 arr[k] = temp; 22 } 23 } 24 System.out.println(); 25 System.out.println("交換后:"); 26 for(int num:arr){ 27 System.out.print(num+" "); 28 } 29 } 30 }
選擇排序空間復雜度也是 O(1),是一種原地排序算法。它的最好情況時間復雜度、最壞情況和平均情況時間復雜度都為 O(n^2)。
選擇排序不是穩定的排序算法,因為它每次都要找出剩余未排序元素中的最小值,並和前面的元素交換位置,這樣就破壞了穩定性。
內容小結
- 要想分析、評價一個排序算法,需要從執行效率、內存消耗和穩定性三個方面來看。
- 插入排序優於冒泡排序,冒泡排序優於選擇排序。