文章目錄
- 1)插入排序
-
- 2)希爾排序
-
- 3)選擇排序
-
- 4)冒泡排序
-
- 5)計數排序
-
- 6)基數排序
-
- 7)歸併排序
一、前言排序演算法也是面試中常常提及的內容,問的最多的應該是快速排序、堆排序。這些排序演算法很基礎,但是如果平時不怎麼寫代碼的話,面試的時候總會出現各種bug。雖然思想都知道,但是就是寫不出來。本文打算對各種排序演算法進行一個匯總,像插入排序、冒泡排序、選擇排序等就簡單的介紹,重點關注快速排序和堆排序。需要提到的一點就是:插入排序,冒泡排序,歸併排序,計數排序都是穩定的排序,而其他排序則都是不穩定的。二、簡單排序1)插入排序插入排序是很基本的排序,特別是在資料基本有序的情況下,插入排序的效能很高,基本可以達到O(N)。當然其最壞情況和平均情況時間複雜度都是O(N^2)。代碼如下:
void insert_sort(int x[], int n){ int i, j; for (i=1; i<n; i++) { /*迴圈不變式:x[0...i-1]有序。每次迭代開始前,x[0...i-1]有序, 迴圈結束後i=n,x[0...n-1]有序*/ int key = x[i]; for (j=i; j>0 && x[j-1]>key; j--) { x[j] = x[j-1]; } x[j] = key; }} 2)希爾排序
希爾排序內部調用插入排序來實現,通過對N/2,N/4...1階分別排序,最後得到整體的有序。
void shell_sort(int a[], int n){for (int gap=n/2; gap>0; gap /=2) {for (int 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 select_sort(int x[], int n){ int i, j, min, tmp; for (i=0; i<n-1; i++) { min = i; for (j=i+1; j<n; j++) { if (x[j] < x[min]) min = j; } if (min != i) tmp = x[i], x[i] = x[min], x[min] = tmp;//交換x[i]和x[min] }}
迴圈不變式:在外層迴圈執行前,x[0...i-1]包含a中最小的i個數,且有序。初始時,i=0,x[0...-1]為空白,顯然成立。每次執行完成後,x[0...i]包含x中最小的i+1個數,且有序。即第一次執行完成後,x[0...0]包含x最小的1個數,且有序。迴圈結束後,i=n-1,則x[0...n-2]包含x最小的n-1個數,且已經有序。所以整個數組有序。 4)冒泡排序冒泡排序時間複雜度跟選擇排序相同。其思想就是進行n-1趟排序,每次都是把最小的數上浮,像魚冒泡一樣。最壞情況為O(N^2)。代碼如下:
void bubble_sort(int x[], int n){ int tmp; for (int i=0; i<n; i++) { for (int j = n-1; j>=i+1; j--) { if (x[j] < x[j-1]) tmp = x[j], x[j] = x[j-1], x[j-1] = tmp; } } }
迴圈不變式:在迴圈開始迭代前,子數組x[0...i-1]包含了數組x[0..n-1]的i-1個最小值,且是排好序的。對冒泡排序的一個改進就是在每趟排序時判斷是否發生交換,如果一次交換都沒有發生,則數組已經有序,可以不用繼續剩下的趟數直接退出。改進後代碼如下:
void bubble_sort_better(int x[], int n){ int tmp; for (int i=0; i<n; i++) { int sorted = 1; for (int j = n-1; j>=i+1; j--) { if (x[j] < x[j-1]) { tmp = x[j], x[j] = x[j-1], x[j-1] = tmp; sorted = 0; } } if (sorted) return ; } } 5)計數排序
假定數組為A[0...n-1],數組中存在重複數字,數組中最大數字為k,建立兩個輔助數組B和C,B用於儲存排序後的結果,C用於儲存臨時值。時間複雜度為O(N),適用於數字範圍較小的數組。具體見代碼:
const int N = 5; //數字個數int b[N+1];void count_sort(int a[], int n, int k) //k為數組最大數字{ int c[k+1]; //輔助數組 for (int i=0; i<=k; i++) c[i] = 0; for (int j=0; j<n; j++) c[a[j]] = c[a[j]] + 1; //C[i]包含等於i的元素個數 for (int i=1; i<=k; i++) c[i] = c[i] + c[i-1]; //C[i]包含小於等於i的元素個數 for (int 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 (int j=n-1; j>=0; j--)改為for(int j=0; j<=n-1; j++)該代碼仍然正確,只是排序不再穩定。 6)基數排序基數排序的思想是對數字每一位分別排序(
注意這裡必須是穩定排序,比如計數排序等,否則會導致結果錯誤),最後得到整體排序。假定對N個數字進行排序,如果數字有d位,每一位可能的最大值為K,則每一位的穩定排序需要O(N+K)時間,總的需要O(d(N+K))時間,當d為常數,K=O(N)時,總的時間複雜度為O(N)。代碼省略。 7)歸併排序
歸併排序通過分治演算法,先排序好兩個子數組,然後將兩個子數組歸併。時間複雜度為O(NlgN)。代碼如下:
/*歸併排序代碼*/void merge_sort(int a[], int p, int r) {if (p < r) {int q = p+(r-p)/2;merge_sort(a, p, q);merge_sort(a, q + 1, r);merge(a, p, q, r);}}void merge(int a[], int start, int mid, int end) {int n1 = mid - start + 1;int n2 = end - mid;int left[n1], right[n2];int i, j;for (i = 0; i < n1; i++) /* left holds a[start..mid] */left[i] = a[start + i];for (j = 0; j < n2; j++) /* right holds a[mid+1..end] */right[j] = a[mid + 1 + j];i = j = 0;int k = start;while (i < n1 && j < n2) {if (left[i] < right[j])a[k++] = left[i++];elsea[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 merge_sort(int a[], int n){ int s=2,i; 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);}