這個系列是我多年前找工作時對資料結構和演算法總結,其中有基礎部分,也有各大公司的經典的面試題,最早釋出在CSDN。現整理為一個系列給需要的朋友參考,如有錯誤,歡迎指正。本系列完整程式碼地址在 這裡。
0 概述
排序演算法也是面試中常常提及的內容,問的最多的應該是快速排序、堆排序。這些排序演算法很基礎,但是如果平時不怎麼寫程式碼的話,面試的時候總會出現各種bug。雖然思想都知道,但是就是寫不出來。本文打算對各種排序演算法進行一個彙總,包括插入排序、氣泡排序、選擇排序、計數排序、歸併排序,基數排序、桶排序、快速排序等。快速排序比較重要,會單獨寫一篇,而堆排序見本系列的二叉堆那篇文章即可。
需要提到的一點就是:插入排序,氣泡排序,歸併排序,計數排序都是穩定的排序,而其他排序則是不穩定的。本文完整程式碼在 這裡。
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章。
參考資料
- 《演算法導論》
- www.cnblogs.com/liushang041… (歸併排序非遞迴實現參考的這裡)