這個系列是我多年前找工做時對數據結構和算法總結,其中有基礎部分,也有各大公司的經典的面試題,最先發布在CSDN。現整理爲一個系列給須要的朋友參考,若有錯誤,歡迎指正。本系列完整代碼地址在 這裏。html
排序算法也是面試中經常說起的內容,問的最多的應該是快速排序、堆排序。這些排序算法很基礎,可是若是平時不怎麼寫代碼的話,面試的時候總會出現各類bug。雖然思想都知道,可是就是寫不出來。本文打算對各類排序算法進行一個彙總,包括插入排序、冒泡排序、選擇排序、計數排序、歸併排序,基數排序、桶排序、快速排序等。快速排序比較重要,會單獨寫一篇,而堆排序見本系列的二叉堆那篇文章便可。git
須要提到的一點就是:插入排序,冒泡排序,歸併排序,計數排序都是穩定的排序,而其餘排序則是不穩定的。本文完整代碼在 這裏。github
插入排序是很基本的排序,特別是在數據基本有序的狀況下,插入排序的性能很高,最好狀況能夠達到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;
}
}
複製代碼
希爾排序內部調用插入排序來實現,經過對 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;
}
}
}
複製代碼
選擇排序的思想就是第i次選取第i小的元素放在位置i。好比第1次就選擇最小的元素放在位置0,第2次選擇第二小的元素放在位置1。選擇排序最好和最壞時間複雜度都爲 O(N^2)
。代碼以下:shell
/**
* 選擇排序
*/
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]
爲空,顯然成立。bash
每次執行完成後,a[0...i]
包含 a
中最小的 i+1
個數,且有序。即第一次執行完成後,a[0...0]
包含 a
最小的 1
個數,且有序。數據結構
循環結束後,i=n-1
,則 a[0...n-2]
包含 a
最小的 n-1
個數,且已經有序。因此整個數組有序。數據結構和算法
冒泡排序時間複雜度跟選擇排序相同。其思想就是進行 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 ;
}
}
複製代碼
假定數組爲 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++)
,該代碼仍然正確,只是排序再也不穩定。
歸併排序經過分治算法,先排序好兩個子數組,而後將兩個子數組歸併。時間複雜度爲 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);
}
複製代碼
基數排序的思想是對數字每一位分別排序(注意這裏必須是穩定排序,好比計數排序等,不然會致使結果錯誤),最後獲得總體排序。假定對 N
個數字進行排序,若是數字有 d
位,每一位可能的最大值爲 K
,則每一位的穩定排序須要 O(N+K)
時間,總的須要 O(d(N+K))
時間,當 d
爲常數,K=O(N)
時,總的時間複雜度爲O(N)。
而桶排序則是在輸入符合均勻分佈時,能夠以線性時間運行,桶排序的思想是把區間 [0,1)
劃分紅 N
個相同大小的子區間,將 N
個輸入均勻分佈到各個桶中,而後對各個桶的鏈表使用插入排序,最終依次列出全部桶的元素。
這兩種排序使用場景有限,代碼就略過了,更詳細能夠參考《算法導論》的第8章。