資料結構和演算法面試題系列—排序演算法之基礎排序
排序演算法也是面試中常常提及的內容,問的最多的應該是快速排序、堆排序。這些排序演算法很基礎,但是如果平時不怎麼寫程式碼的話,面試的時候總會出現各種bug。雖然思想都知道,但是就是寫不出來。本文打算對各種排序演算法進行一個彙總,包括插入排序、氣泡排序、選擇排序、計數排序、歸併排序,基數排序、桶排序、快速排序等。快速排序比較重要,會單獨寫一篇,而堆排序見本系列的二叉堆那篇文章即可。
需要提到的一點就是:插入排序,氣泡排序,歸併排序,計數排序都是穩定的排序,而其他排序則是不穩定的。本文完整程式碼在 ofollow,noindex">這裡 。
1 插入排序
插入排序是很基本的排序,特別是在資料基本有序的情況下,插入排序的效能很高,最好情況可以達到 O(N)
,其最壞情況和平均情況時間複雜度都是 O(N^2)
。程式碼如下:
/** * 插入排序 */ void insertSort(int a[], int n) { int i, j; for (i = 1; i < n; i++) { /* * 迴圈不變式:a[0...i-1]有序。每次迭代開始前,a[0...i-1]有序, * 迴圈結束後i=n,a[0...n-1]有序 * */ int key = a[i]; for (j = i; j > 0 && a[j-1] > key; j--) { a[j] = a[j-1]; } a[j] = key; } } 複製程式碼
2 希爾排序
希爾排序內部呼叫插入排序來實現,通過對 N/2,N/4...1
階分別排序,最後得到整體的有序。
/** * 希爾排序 */ void shellSort(int a[], int n) { int gap; for (gap = n/2; gap > 0; gap /= 2) { int i; for (i = gap; i < n; i++) { int key = a[i], j; for (j = i; j >= gap && key < a[j-gap]; j -= gap) { a[j] = a[j-gap]; } a[j] = key; } } } 複製程式碼
3 選擇排序
選擇排序的思想就是第i次選取第i小的元素放在位置i。比如第1次就選擇最小的元素放在位置0,第2次選擇第二小的元素放在位置1。選擇排序最好和最壞時間複雜度都為 O(N^2)
。程式碼如下:
/** * 選擇排序 */ void selectSort(int a[], int n) { int i, j, min, tmp; for (i = 0; i < n-1; i++) { min = i; for (j = i+1; j < n; j++) { if (a[j] < a[min]) min = j; } if (min != i) tmp = a[i], a[i] = a[min], a[min] = tmp; //交換a[i]和a[min] } } 複製程式碼
迴圈不變式:在外層迴圈執行前, a[0...i-1]
包含 a
中最小的 i
個數,且有序。
-
初始時,
i=0
,a[0...-1]
為空,顯然成立。 -
每次執行完成後,
a[0...i]
包含a
中最小的i+1
個數,且有序。即第一次執行完成後,a[0...0]
包含a
最小的1
個數,且有序。 -
迴圈結束後,
i=n-1
,則a[0...n-2]
包含a
最小的n-1
個數,且已經有序。所以整個陣列有序。
4 氣泡排序
氣泡排序時間複雜度跟選擇排序相同。其思想就是進行 n-1
趟排序,每次都是把最小的數上浮,像魚冒泡一樣。最壞情況為 O(N^2)
。程式碼如下:
/** * 氣泡排序-經典版 */ void bubbleSort(int a[], int n) { int i, j, tmp; for (i = 0; i < n; i++) { for (j = n-1; j >= i+1; j--) { if (a[j] < a[j-1]) tmp = a[j], a[j] = a[j-1], a[j-1] = tmp; } } } 複製程式碼
迴圈不變式:在迴圈開始迭代前,子陣列 a[0...i-1]
包含了陣列 a[0..n-1]
的 i-1
個最小值,且是排好序的。
對氣泡排序的一個改進就是在每趟排序時判斷是否發生交換,如果一次交換都沒有發生,則陣列已經有序,可以不用繼續剩下的趟數直接退出。改進後代碼如下:
/** * 氣泡排序-優化版 */ void betterBubbleSort(int a[], int n) { int tmp, i, j; for (i = 0; i < n; i++) { int sorted = 1; for (j = n-1; j >= i+1; j--) { if (a[j] < a[j-1]) { tmp = a[j], a[j] = a[j-1], a[j-1] = tmp; sorted = 0; } } if (sorted) return ; } } 複製程式碼
5 計數排序
假定陣列為 a[0...n-1]
,陣列中存在重複數字,陣列中最大數字為k,建立兩個輔助陣列 b[]
和 c[]
, b[]
用於儲存排序後的結果, c[]
用於儲存臨時值。時間複雜度為 O(N)
,適用於數字範圍較小的陣列。

計數排序原理如上圖所示,程式碼如下:
/** * 計數排序 */ void countingSort(int a[], int n) { int i, j; int *b = (int *)malloc(sizeof(int) * n); int k = maxOfIntArray(a, n); // 求陣列最大元素 int *c = (int *)malloc(sizeof(int) * (k+1));//輔助陣列 for (i = 0; i <= k; i++) c[i] = 0; for (j = 0; j < n; j++) c[a[j]] = c[a[j]] + 1; //c[i]包含等於i的元素個數 for (i = 1; i <= k; i++) c[i] = c[i] + c[i-1];//c[i]包含小於等於i的元素個數 for (j = n-1; j >= 0; j--) {// 賦值語句 b[c[a[j]]-1] = a[j]; //結果存在b[0...n-1]中 c[a[j]] = c[a[j]] - 1; } /*方便測試程式碼,這一步賦值不是必須的*/ for (i = 0; i < n; i++) { a[i] = b[i]; } free(b); free(c); } 複製程式碼
擴充套件:如果程式碼中的給陣列 b[]
賦值語句 for (j=n-1; j>=0; j--)
改為 for(j=0; j<=n-1; j++)
,該程式碼仍然正確,只是排序不再穩定。
6 歸併排序
歸併排序通過分治演算法,先排序好兩個子陣列,然後將兩個子陣列歸併。時間複雜度為 O(NlgN)
。程式碼如下:
/* * 歸併排序-遞迴 * */ void mergeSort(int a[], int l, int u) { if (l < u) { int m = l + (u-l)/2; mergeSort(a, l, m); mergeSort(a, m + 1, u); merge(a, l, m, u); } } /** * 歸併排序合併函式 */ void merge(int a[], int l, int m, int u) { int n1 = m - l + 1; int n2 = u - m; int left[n1], right[n2]; int i, j; for (i = 0; i < n1; i++) /* left holds a[l..m] */ left[i] = a[l + i]; for (j = 0; j < n2; j++) /* right holds a[m+1..u] */ right[j] = a[m + 1 + j]; i = j = 0; int k = l; while (i < n1 && j < n2) { if (left[i] < right[j]) a[k++] = left[i++]; else a[k++] = right[j++]; } while (i < n1) /* left[] is not exhausted */ a[k++] = left[i++]; while (j < n2) /* right[] is not exhausted */ a[k++] = right[j++]; } 複製程式碼
擴充套件:歸併排序的非遞迴實現怎麼做?
歸併排序的非遞迴實現其實是最自然的方式,先兩兩合併,而後再四四合並等,就是從底向上的一個過程。程式碼如下:
/** * 歸併排序-非遞迴 */ void mergeSortIter(int a[], int n) { int i, s=2; while (s <= n) { i = 0; while (i+s <= n){ merge(a, i, i+s/2-1, i+s-1); i += s; } //處理末尾殘餘部分 merge(a, i, i+s/2-1, n-1); s*=2; } //最後再從頭到尾處理一遍 merge(a, 0, s/2-1, n-1); } 複製程式碼
7 基數排序、桶排序
基數排序的思想是對數字每一位分別排序(注意這裡必須是穩定排序,比如計數排序等,否則會導致結果錯誤),最後得到整體排序。假定對 N
個數字進行排序,如果數字有 d
位,每一位可能的最大值為 K
,則每一位的穩定排序需要 O(N+K)
時間,總的需要 O(d(N+K))
時間,當 d
為常數, K=O(N)
時,總的時間複雜度為O(N)。

而桶排序則是在輸入符合均勻分佈時,可以以線性時間執行,桶排序的思想是把區間 [0,1)
劃分成 N
個相同大小的子區間,將 N
個輸入均勻分佈到各個桶中,然後對各個桶的連結串列使用插入排序,最終依次列出所有桶的元素。

這兩種排序使用場景有限,程式碼就略過了,更詳細可以參考《演算法導論》的第8章。