本文介紹6種常見的排序算法,以及他們的原理,性能分析和c語言實現:
為了能夠條理清楚,本文所有的算法和解釋全部按照升序排序進行
首先准備一個元素無序的數組arr[],數組的長度為length,一個交換函數swap,
在main函數中實現排序函數的調用,並輸出排序結果:
void swap(int*x , int*y) {
int temp = *x; *x = *y; *y = temp; } int main() { int arr[] = { 1,8,5,7,4,6,2,3}; int length = sizeof(arr) / sizeof(int); sort(arr, length); for (int i = 0;i < length;i++) { printf("%d\n", arr[i]); } return 0; }
插入排序
第一次循環:

第二次循環:

第三次循環:


外層循環每執行一次就從無序區向有序區中插入一個數據arr[i]
里層循環控制插入的數據arr[i]與其前一個數據比較,如果比前一個數據小,就讓前一個數據后移1位
...不斷重復上述步驟,直到找到不比arr[i]小的數據arr[j],因為arr[j]后面的數據都后移了1位,所以直接將arr[i]放在空閑的arr[j+1]位置
c程序實現:
void CRsort(int arr[], int length) {
int temp; for (int i = 0;i < length;i++) { temp = arr[i]; for (int j = i - 1;j >= 0;j--) { if (arr[j] > temp) { arr[j + 1] = arr[j]; } else { arr[j + 1] = temp; break; } } } }
性能分析
穩定性 : 穩定
-->內層循環執行時,只有遇到大於arr[i]的才會后移,等於arr[i]的不會后移
時間復雜度 : (最壞n²,最好n,平均n²)
-->數據量為n的情況下,外層循環執行n次,內層循環最多執行n次,如果是數據是有序的內層循環只會執行1次
數據越有序,插入排序的執行效率就越高
空間復雜度: 1
-->程序沒有用到遞歸,臨時變量不占用存儲資源,因此空間復雜度為1
希爾排序
希爾排序又叫"縮小增量排序",是插入排序的一種改進排本:
插入排序優點是適合處理接近有序的元素,缺點是每次只能比較一個元素,希爾排序利用了這兩個特點:

區別於普通的插入排序,希爾排序有一個增量序列r,r的初始值一般取 int(length/2),也就是元素數量除以2向下取整
將所有相隔r個單位的元素組成一組,在組內完成排序
增量每次折半,直到最后一次排序時,增量為1,這時元素一定是有序的
最外層循環控制增量組,即每次循環的增量
里面的兩層循環就是一個最基本的插入排序,只不過每次加入有序組的不是arr[i+1],而是arr[i+增量]
c程序實現:
void ShellSort(int arr[], int length) { int r, temp, j; for (r = length / 2;r >= 1;r = r / 2) { for (int i = r;i < length;i++) { temp = arr[i]; j = i - r; while (j >= 0 && temp < arr[j]) { arr[j + r] = arr[j]; j = j - r; } arr[j + r] = temp; } } }
性能分析
穩定性 : 不穩定
-->每個元素都是在自己的排序組中排序,兩個值相同的元素出於不同的排序組中他們總體的相對位置可能會發生變化
時間復雜度 : (最壞n²,最好n,平均n^1.3)
-->希爾排序的分析是一個復雜的問題,以為它的時間是所取“增量”序列的函數,這涉及到一些數學上尚未解決的難題,摘自網上.
最好和普通插入排序相同,如果是數據是有序的內層循環只會執行1次
同樣擁有插入排序的特點:數據越有序,它的執行效率就越高
空間復雜度: 1
-->程序沒有用到遞歸,臨時變量不占用存儲資源,因此空間復雜度為1
冒泡排序
冒泡排序

外層循環每次都將一個元素置入有序區
內層循環控制置入的元素:設置一個指針j,拿arr[j]的值與arr[j+1]進行比較:
如果arr[j]<arr[j+1],就將arr[j]的值與arr[j+1]交換,否則j++,直到無序區比較完畢
值得一提的是,冒泡排序可以進行這樣的優化:
設置一個狀態碼change,當有數據交換發生時change置1
如果一整次排序都沒有交換發生,那么這組數據就是有序的,可以直接結束循環
c程序實現:
int MPsort(int arr[], int length) {
int temp, change = 0; for (int i = 0;i < length;i++) { for (int j = 0;j < length - i - 1;j++) { if (arr[j] > arr[j + 1]) { swap(&arr[j],&arr[j+1]); change = 1; } } if (change == 0) { return 0; } } }
性能分析
穩定性 : 穩定
-->數據只有在大於或小於時才會交換,相等時不會交換,因此相同數據的相對位置不會發生改變
時間復雜度 : (最壞n²,最好n,平均n²)
-->在數據完全有序時,不會有數據交換,根據上面的優化處理,狀態碼change不會改變,外層循環執行一次就結束,時間復雜度為n+1,也就是n.
而如果是非常無序的數據,外層循環執行滿n次,時間復雜度就是n²
空間復雜度: log(2)(N)
-->程序沒有用到遞歸,臨時變量不占用存儲資源,因此空間復雜度為1
快速排序
快速排序是一種效率比較高的排序方法,這張圖片我認為介紹的很清楚,搬運來用一下,出處在文章下面:

c程序實現:
void KSSort(int arr[], int left, int right) {
if (left < right) { int key = arr[left]; int l = left; int r = right; while (l < r) { while (l < r && arr[r] >= key) { r--; } if (l < r) { arr[l] = arr[r]; l++; } else { break; } while (l < r && arr[l] <= key) { l++; } if (l < r) { arr[r] = arr[l]; r--; } else { break; } } arr[l] = key; KSSort(arr, left, l - 1); KSSort(arr, l + 1, right); } }
性能分析
穩定性 : 不穩定
--> 假設是穩定的,舉個反例:
5 | 3 1 2 | 9 7 8 9 | 4 6 3
這時遍歷unvisited部分 剛到了4 (array[8])
顯然4<5 ,這是4應該從 unvisited 部分去到 lower 部分。 因此 higher部分第一個元素 9 (array[4]) 和 4互換。變成了這樣:
5 | 3 1 2 4 | 7 8 9 9 | 6 3
時間復雜度 : (最壞n²,最好nlogn,平均nlogn)
-->快速排序最差的情況就是每次取到的基准數baes都是排序組的邊界值(不是最小的就是最大的),這時外層循環要遍歷n次才能將所有數據比較完,時間復雜度就是n²
這種情況多發生在排好序(或接近排好序)的數據中,要在這種數據中避免使用快速排序算法
最好的情況是基准數每次都能取到接近排序組的中位數,用最短的循環次數將程序完全分割,n個數據每次減半,也就是log(2)(N)次后數據被完全分割,算法的時間復雜度為
最差的情況不容易取到,平均時間復雜度取nlogn
空間復雜度: logn
-->在我這個程序里沒有用到遞歸,空間復雜度為1,當然也可以使用遞歸實現,遞歸log(2)(N)次,空間復雜度為logn
最后附一張各種排序算法比較圖
選擇排序
選擇排序是最簡單的排序方式,也是比較低效的一種排序方式:
第一次循環 :

第二次循環:

第三次循環:

外層循環每執行一次向有序區添加一個元素
內層循環遍歷到最大的元素,與剛剛填入有序區的元素交換
......直到數據全部加入有序區
c程序實現:
void XZsort(int arr[] , int length) { int check; for (int i = 0;i < length - 1;i++) { check = i; for (int j = i + 1;j < length;j++) { if (arr[j] < arr[check]) { check = j; } } if (i != check) { swap(&arr[i],&arr[check]); } } }
性能分析
穩定性 : 穩定
-->存在兩個相同的元素時,肯定是下標小的元素先進入有序區,而且在值相等的情況下有序區元素不會被替換
時間復雜度 : (最壞n²,最好n²,平均n²)
-->外層循環要執行n次,才能將n個數據全部加入有序區
無論數據是否有序,內層循環都要將所有的數據比較一遍,找到最小的元素
空間復雜度: 1
-->程序沒有用到遞歸,臨時變量不占用存儲資源,因此空間復雜度為1
堆排序
堆排序是選擇排序的改進版本,它比堆排序的性能要高很多.
要實現堆排序,首先要了解這幾個知識點:
大根堆:每個節點的值都大於他的左右子樹(小根堆反之)
完全二叉樹:除了最后一層之外的其他每一層都被完全填充,並且所有結點都保持向左對齊。
在n個節點的完全二叉樹中,葉子節點有(n+1)/2個,非葉子節點有(n-1)/2個
在堆中,下標為n的數的左子樹為2n+1,右子樹為2n+2
首先要能夠將一組無序的數排列成大根堆,大根堆的構造方法如下:


...
外層循環從最后一個非葉子節點( length/2-1 )向根節點遍歷,每遍歷到一個數據,就執行下面操作:
設置一個根指針i,指向當前要操作樹的根節點
比較該樹的左右子樹的值,將biger指針指向較大的子樹
如果該子樹的值比根節點還大(arr[biger]>arr[i]),將該子樹的值與根節點交換,並將i指針指向該子樹,使之成為新的根節點
......遞歸執行上一步操作,直到有根節點不小於左右子樹為止
構造出大根堆之后,每次取整個堆的根節點(也就是第一個元素)存入有序區,將堆的最后一個元素作為根節點
剩下的元素繼續構造大根堆,直到數據完全存入有序區
c程序實現:
void MkHeap(int arr[], int i, int length) { int bigger = 2 * i + 1; int temp; if (bigger<length){ if (arr[bigger] < arr[bigger + 1]) { bigger++; } if (arr[i]<arr[bigger]){//如果子樹比爹樹大:把子樹的值與爹樹值交換,並讓該子樹成為新的爹樹 swap(&arr[i], &arr[bigger]); MkHeap(arr,bigger,length); } } } void HeapSort(int arr[], int length) { //從最后一個非葉子節點往根找 for (int i = length / 2 - 1;i >= 0;i--) { MkHeap(arr, i, length); } for (int j = length - 1;j > 0;j--) { swap(&arr[j],&arr[0]); MkHeap(arr, 0, j-1); } }
性能分析
穩定性 : 不穩定
-->先假設穩定,然后舉個反例:
{6,7,7}這棵樹左右子樹的值都是7,本來左子樹是應該是在前面的,但是6加入有序區之后右子樹的7會被提到最上面,這樣他們的順序就被調換了
時間復雜度 : (最壞nlogn,最好nlogn,平均nlogn <底數可以寫2也可以不寫,根據時間復雜度的性質無論底數是幾結果都是一樣的> )
-->根據性質,外層循環會執行n次(n代表排序的元素個數),將所有數據全部加入有序區.
而內層遞歸函數指針從0到n,每次增長前一個的2n+1,忽略掉常數1,也就是執行log(2)(N)次
因此為Nlog(2)(N)次,忽略掉底數2,時間復雜度為nlogn
插入/希爾排序適合接近有序的數據,而堆排序適合非常無序的數據,因為無論數據多么雜亂,希爾排序的時間復雜度都是nlogn
(不放心再說一下 : log(2)(N)是log以2為底n的對數 , 不是log2乘n , 因為底數打不出來只能這樣寫)
空間復雜度: log(2)(N)
-->這里程序遞歸了log(2)(N)次,每次遞歸都占用內存,因此空間復雜度為log(2)(N)
當然也可以使用循環的方式實現,但是那樣寫出來結構略顯混亂,不如遞歸方式清晰

不穩定算法口訣:快些選隊
參考資料(文中的部分圖片和思想來自以上材料):
1. 《新編數據結構習題與解析》
2. 文章
https://www.cnblogs.com/skywang12345/p/3603935.html
https://www.cnblogs.com/jingmoxukong/p/4302891.html
http://www.sohu.com/a/341037266_115128
https://www.toutiao.com/a6593273307280179715/?iid=6593273307280179715
3. 關於快速排序算法的穩定性? - 知遙其實是德魯伊的回答:https://www.zhihu.com/question/45929062/answer/262452296
