十大排序算法及C語言實現
0、總序
0.1 排序的概念
排序是計算機內經常進行的一種操作,其目的是將一組“無序”的記錄序列調整爲“有序”的記錄序列。分內部排序和外部排序,若整個排序過程不需要訪問外存便能完成,則稱此類排序問題爲內部排序。反之,若參加排序的記錄數量很大,整個序列的排序過程不可能在內存中完成,則稱此類排序問題爲外部排序。內部排序的過程是一個逐步擴大記錄的有序序列長度的過程。
0.2 穩定性概念
穩定排序:如果a原本在b前面,而a=b,排序之後a仍然在b的前面;
不穩定排序:如果a原本在b的前面,而a=b,排序之後a可能會出現在b的後面;
0.3 內部排序和外部排序
內部排序:排序不佔用外存
外部排序:數據過大,排序佔用外存
0.4 時空複雜度
時間複雜度:排序消耗的時間
空間複雜度:排序所需的內存
0.5 排序總結
1、冒泡排序
1.1 概念
冒泡排序(Bubble Sort)也是一種簡單直觀的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢"浮"到數列的頂端。
作爲最簡單的排序算法之一,冒泡排序給我的感覺就像 Abandon 在單詞書裏出現的感覺一樣,每次都在第一頁第一位,所以最熟悉。冒泡排序還有一種優化算法,就是立一個 flag,當在一趟序列遍歷中元素沒有發生交換,則證明該序列已經有序。但這種改進對於提升性能來
這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端(升序或降序排列),就如同碳酸飲料中二氧化碳的氣泡最終會上浮到頂端一樣,故名“冒泡排序”。
1.2 算法步驟
- 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
- 針對所有的元素重複以上的步驟,除了最後一個。
- 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
1.3 動畫演示
1.4 算法實現
#include <stdio.h>
void bubble_sort(int arr[], int len) {
int i, j, temp;
for (i = 0; i < len - 1; i++)
for (j = 0; j < len - 1 - i; j++)
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
int main() {
int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
int len = (int) sizeof(arr) / sizeof(*arr);
bubble_sort(arr, len);
int i;
for (i = 0; i < len; i++)
printf("%d ", arr[i]);
return 0;
}
2、選擇排序
2.1 概念
選擇排序是一種簡單直觀的排序算法,無論什麼數據進去都是 O(n²) 的時間複雜度。所以用到它的時候,數據規模越小越好。唯一的好處可能就是不佔用額外的內存空間了吧。
2.2 算法步驟
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。
- 重複第二步,直到所有元素均排序完畢。
2.3 動畫演示
2.4 算法實現
void swap(int *a,int *b){ //交換兩個變量
int temp = *a;
*a = *b;
*b = temp;
}
void selection_sort(int arr[], int len) {
int i,j;
for (i = 0 ; i < len - 1 ; i++) {
int min = i;
for (j = i + 1; j < len; j++) //訪問未排序的元素
if (arr[j] < arr[min]) //找到目前最小值
min = j; //記錄最小值
swap(&arr[min], &arr[i]); //做交換
}
}
3、插入排序
3.1 概念
插入排序的代碼實現雖然沒有冒泡排序和選擇排序那麼簡單粗暴,但它的原理應該是最容易理解的了,因爲只要打過撲克牌的人都應該能夠秒懂。插入排序是一種最簡單直觀的排序算法,它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。
插入排序和冒泡排序一樣,也有一種優化算法,叫做拆半插入。
3.2 算法步驟
- 將第一待排序序列第一個元素看做一個有序序列,把第二個元素到最後一個元素當成是未排序序列。
- 從頭到尾依次掃描未排序序列,將掃描到的每個元素插入有序序列的適當位置。(如果待插入的元素與有序序列中的某個元素相等,則將待插入元素插入到相等元素的後面。)
3.3 動畫演示
3.4 算法實現
void insertion_sort(int arr[], int len){
int i,j,key;
for (i=1;i<len;i++){
key = arr[i];
j=i-1;
while((j>=0) && (arr[j]>key)) {
arr[j+1] = arr[j];
j--;
}
arr[j+1] = key;
}
}
4、希爾排序
4.1 概念
希爾排序,也稱遞減增量排序算法,是插入排序的一種更高效的改進版本。但希爾排序是非穩定排序算法。
希爾排序是基於插入排序的以下兩點性質而提出改進方法的:
插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率; 但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位;
希爾排序的基本思想是:先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄"基本有序"時,再對全體記錄進行依次直接插入排序。
4.2 算法步驟
- 選擇一個增量序列
t1,t2,……,tk
,其中ti > tj, tk = 1
; - 按增量序列個數
k
,對序列進行 k 趟排序; - 每趟排序,根據對應的增量
ti
,將待排序列分割成若干長度爲m
的子序列,分別對各子表進行直接插入排序。僅增量因子爲1
時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。
4.3 動畫演示
4.4 算法實現
void shell_sort(int arr[], int len) {
int gap, i, j;
int temp;
for (gap = len >> 1; gap > 0; gap >>= 1)
for (i = gap; i < len; i++) {
temp = arr[i];
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)
arr[j + gap] = arr[j];
arr[j + gap] = temp;
}
}
5、歸併排序
5.1 概念
歸併排序(Merge sort)是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。
作爲一種典型的分而治之思想的算法應用,歸併排序的實現由兩種方法:
- 自上而下的遞歸(所有遞歸的方法都可以用迭代重寫,所以就有了第 2 種方法);
- 自下而上的迭代;
和選擇排序一樣,歸併排序的性能不受輸入數據的影響,但表現比選擇排序好的多,因爲始終都是
O(nlogn)
的時間複雜度。代價是需要額外的內存空間。
5.2 算法步驟
- 申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列;
- 設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置;
- 比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置;
- 重複步驟 3 直到某一指針達到序列尾;
- 將另一序列剩下的所有元素直接複製到合併序列尾
5.3 動畫演示
5.4 算法實現
int min(int x, int y) {
return x < y ? x : y;
}
void merge_sort(int arr[], int len) {
int *a = arr;
int *b = (int *) malloc(len * sizeof(int));
int seg, start;
for (seg = 1; seg < len; seg += seg) {
for (start = 0; start < len; start += seg * 2) {
int low = start, mid = min(start + seg, len), high = min(start + seg * 2, len);
int k = low;
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
while (start1 < end1 && start2 < end2)
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
while (start1 < end1)
b[k++] = a[start1++];
while (start2 < end2)
b[k++] = a[start2++];
}
int *temp = a;
a = b;
b = temp;
}
if (a != arr) {
int i;
for (i = 0; i < len; i++)
b[i] = a[i];
b = a;
}
free(b);
}
6、快速排序
6.1 概念
快速排序是由東尼·霍爾所發展的一種排序算法。在平均狀況下,排序
n
個項目要Ο(nlogn)
次比較。在最壞狀況下則需要Ο(n²)
次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他Ο(nlogn)
算法更快,因爲它的內部循環(inner loop)可以在大部分的架構上很有效率地被實現出來。快速排序使用分治法(Divide and conquer)策略來把一個串行(list)分爲兩個子串行(sub-lists)。
快速排序又是一種分而治之思想在排序算法上的典型應用。本質上來看,快速排序應該算是在冒泡排序基礎上的遞歸分治法。
快速排序的名字起的是簡單粗暴,因爲一聽到這個名字你就知道它存在的意義,就是快,而且效率高!它是處理大數據最快的排序算法之一了。雖然 Worst Case 的時間複雜度達到了
O(n²)
,但是人家就是優秀,在大多數情況下都比平均時間複雜度爲O(nlogn)
的排序算法表現要更好,可是這是爲什麼呢,我也不知道。好在我的強迫症又犯了,查了 N 多資料終於在《算法藝術與信息學競賽》上找到了滿意的答案:
快速排序的最壞運行情況是
O(n²)
,比如說順序數列的快排。但它的平攤期望時間是O(nlogn)
,且O(nlogn)
記號中隱含的常數因子很小,比複雜度穩定等於O(nlogn)
的歸併排序要小很多。所以,對絕大多數順序性較弱的隨機數列而言,快速排序總是優於歸併排序。
6.2 算法步驟
- 從數列中挑出一個元素,稱爲 “基準”(
pivot
); - 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱爲分區(
partition
)操作; - 遞歸地(
recursive
)把小於基準值元素的子數列和大於基準值元素的子數列排序;
6.3 動畫演示
6.4 算法實現
void quickSort() {
int arr[10] = {11, 7, 9, 3, 4, 6, 2, 8, 5, 3};
quick_sort(arr, 0, 9);
for (int i = 0; i < 10; i++)
printf("%d\t", arr[i]);
}
int partition(int arr[], int start, int end) {
int temp = arr[start];
int li = start, ri = end;
while (li < ri) {
while (li < ri && arr[ri] > temp)
ri--;
if (li < ri) {
arr[li] = arr[ri];
li++;
}
while (li < ri && arr[li] < temp)
li++;
if (li < ri) {
arr[ri] = arr[li];
ri--;
}
}
arr[li] = temp;
return li;
}
void quick_sort(int arr[], int start, int end) {
if (start < end) {
int index = partition(arr, start, end);
quick_sort(arr, start, index - 1);
quick_sort(arr, index + 1, end);
}
}
7、堆排序
7.1 概念
堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。堆排序可以說是一種利用堆的概念來排序的選擇排序。分爲兩種方法:
- 大頂堆:每個節點的值都大於或等於其子節點的值,在堆排序算法中用於升序排列;
- 小頂堆:每個節點的值都小於或等於其子節點的值,在堆排序算法中用於降序排列;
堆排序的平均時間複雜度爲
Ο(nlogn)
。
7.2 算法步驟
- 創建一個堆
H[0……n-1]
; - 把堆首(最大值)和堆尾互換;
- 把堆的尺寸縮小 1,並調用
shift_down(0)
,目的是把新的數組頂端數據調整到相應位置; - 重複步驟 2,直到堆的尺寸爲 1。
7.3 動畫演示
7.4 算法實現
void heapSort() {
int arr[] = {3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6};
int len = (int)sizeof(arr) / sizeof(*arr);
for (int i = len; i > 1; i--)
heap_Sort(arr, i); //建立堆 每次規模-1
for (int i = 0; i < len; i++)
printf("%d ", arr[i]);
return 0;
}
//構造一個大頂堆並將最大值換至最後一位
void heap_Sort(int arr[], int len) {
int dad = len / 2 - 1; //最後一個父節點
int son = 2 * dad + 1; //該父節點下的首個子節點
while (dad >= 0) {
//判斷是否有兩個子節點若有則在其中尋找最大子節點
if (son + 1 <= len - 1 && arr[son] < arr[son + 1])
son++;
if (arr[dad] < arr[son]) //若父節點小於子節點則交換位置
swap(&arr[dad], &arr[son]);
dad--; //回退到上一個父節點
son = 2 * dad + 1; //上一個父節點的首個子節點
}
swap(&arr[0], &arr[len - 1]);
}
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
8、計數排序
8.1 概念
計數排序是一個非基於比較的排序算法,該算法於1954年由 Harold H. Seward 提出。它的優勢在於在對一定範圍內的整數排序時,它的複雜度爲
Ο(n+k)
(其中k是整數的範圍),快於任何比較排序算法。當然這是一種犧牲空間換取時間的做法,而且當O(k)>O(nlog(n))
的時候其效率反而不如基於比較的排序(基於比較的排序的時間複雜度在理論上的下限是O(nlog(n))
, 如歸併排序,堆排序)
8.2 算法步驟
- 找出待排序的數組中最大和最小的元素
- 統計數組中每個值爲
i
的元素出現的次數,存入數組C
的第i項 - 對所有的計數累加(從
C
中的第一個元素開始,每一項和前一項相加) - 反向填充目標數組:將每個元素i放在新數組的第
C(i)
項,每放一個元素就將C(i)
減去1
8.3 動畫演示
8.4 算法實現
void countingSort() {
int arr[] = {3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6};
int len = (int)sizeof(arr) / sizeof(*arr);
counting_Sort(arr, len);
for (int i = 0; i < len; i++)
printf("%d ", arr[i]);
}
void counting_Sort(int arr[], int LEN) {
//尋找最大最小值
int max = arr[0], min = arr[0], i, j = 0;
for (i = 0; i < LEN; i++) {
if (arr[i] < min)
min = arr[i];
if (arr[i] > max)
max = arr[i];
}
//建立計數數組
int new_len = max - min + 1;
printf("%d\n", new_len);
int conunting_arr[new_len];
//計數
for (i = 0; i < new_len; i++) { //初始化
conunting_arr[i] = 0;
}
for (i = 0; i < LEN; i++) {
conunting_arr[arr[i] - min]++;
}
//根據計數結果進行排序
for (i = 0; i < new_len; i++) {
int index = conunting_arr[i];
while (index != 0) {
arr[j] = i + min;
index--;
j++;
}
}
}
9、桶排序
9.1 概念
桶排序是計數排序的升級版。它利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的確定。爲了使桶排序更加高效,我們需要做到這兩點:
- 在額外空間充足的情況下,儘量增大桶的數量
- 使用的映射函數能夠將輸入的 N 個數據均勻的分配到 K 個桶中
同時,對於桶中元素的排序,選擇何種比較排序算法對於性能的影響至關重要。
9.2 算法步驟
- 人爲設置一個BucketSize,作爲每個桶所能放置多少個不同數值;
- 遍歷輸入數據,並且把數據一個一個放到對應的桶裏去;
- 對每個不是空的桶進行排序,可以使用其它排序方法,也可以遞歸使用桶排序;
- 從不是空的桶裏把排好序的數據拼接起來。
9.3 動畫演示
9.4 算法實現
void bucketSort() {
int arr[] = {3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6};
int len = (int)sizeof(arr) / sizeof(*arr);
bucket_Sort(arr, len);
for (int i = 0; i < len; i++)
printf("%d ", arr[i]);
}
void bucket_sort(int arr[], int LEN) {
int bucket[5][6] = {0}, i, j, k, temp; //初始化桶,每個桶存放6個數據
//尋找最大最小值
int min = arr[0], max = arr[0];
for (i = 0; i < LEN; i++) {
if (arr[i] < min)
min = arr[i]; //0
if (arr[i] > max)
max = arr[i]; //9
}
//遍歷數組,將元素放到對應桶中
int index0 = 0, index1 = 0, index2 = 0, index3 = 0, index4 = 0;
for (i = 0; i < LEN; i++) {
if (arr[i] < min + (max - min + 1) / 5 * 1 && index0 < 7) {
bucket[0][index0] = arr[i];
index0++;
} else if (arr[i] < min + (max - min + 1) / 5 * 2 && (index1 < 7 || index0 >= 7)) {
bucket[1][index1] = arr[i];
index1++;
} else if (arr[i] < min + (max - min + 1) / 5 * 3 && (index2 < 7 || index1 >= 7)) {
bucket[2][index2] = arr[i];
index2++;
} else if (arr[i] < min + (max - min + 1) / 5 * 4 && (index3 < 7 || index2 >= 7)) {
bucket[3][index3] = arr[i];
index3++;
} else if (arr[i] < min + (max - min + 1) / 5 * 5 && (index4 < 7 || index3 >= 7)) {
bucket[4][index4] = arr[i];
index4++;
}
}
//在每個桶中使用冒泡排序
for (i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) { //從小到大
// 外循環爲排序趟數,len個數進行len-1趟
for (int k = 0; k < 5 - i; k++) {
// 內循環爲每趟比較的次數,第i趟比較len-i次,因爲第一次已經將最大的元素冒泡到最後一個位置了
if (bucket[i][k] > bucket[i][k + 1]) {
//相鄰元素比較,逆序則將交換位置
temp = bucket[i][k];
bucket[i][k] = bucket[i][k + 1];
bucket[i][k + 1] = temp;
}
}
}
}
//將桶中排序結果還原到原數組中
for (i = 0; i < 5; i++) {
for (j = 0; j < 6; j++) {
arr[i * 6 + j] = bucket[i][j];
}
}
}
10、基數排序
10.1 概念
基數排序(radix sort)屬於“分配式排序”(distribution sort),又稱“桶子法”(bucket sort)或bin sort,顧名思義,它是透過鍵值的部份資訊,將要排序的元素分配至某些“桶”中,藉以達到排序的作用,基數排序法是屬於穩定性的排序,其時間複雜度爲
O (nlogm)
,其中r
爲所採取的基數,而m
爲堆數,在某些時候,基數排序法的效率高於其它的穩定性排序法。
10.2 算法步驟
- 取得數組中的最大數,並取得位數;
arr
爲原始數組,從最低位開始取每個位組成radix
數組;- 對
radix
進行計數排序(利用計數排序適用於小範圍數的特點);
10.3 動畫演示
10.4 算法實現
void radixSort() {
int arr[] = {31, 25, 33, 40, 78, 26, 1, 52, 88, 63, 22, 44, 69, 42, 17, 10, 11, 28, 19, 47};
int LEN = (int)sizeof(arr) / sizeof(*arr);
int LEVEL = 0; //最大數的位數
//尋找最大值以確定位數
int max = arr[0], i;
for (i = 0; i < LEN; i++) {
if (arr[i] > max)
max = arr[i];
}
for (i = max; i > 0; i /= 10) {
LEVEL++;
}
// printf("%d",LEVEL);
for (i = 0; i < LEVEL; i++) {
radix_sort(arr, LEN, i);
for (int i = 0; i < LEN; i++)
printf("%d ", arr[i]);
printf("\n");
}
for (int i = 0; i < LEN; i++)
printf("%d ", arr[i]);
return 0;
}
void radix_sort(int arr[], int LEN, int level) {
int bucket[10] = {0}, temp[LEN], i, j;
int flag = pow(10, level); //用於確定當前比較的是什麼位
//獲取個位並進行第一次基數排序
//獲取個位存儲入桶
for (i = 0; i < LEN; i++) {
bucket[arr[i] / flag % 10]++;
}
bucket[0]--;
for (i = 1; i < 10; i++) {
bucket[i] += bucket[i - 1];
}
//根據桶結果將原數組進行第一次排序後複製到temp數組
//注意這裏必須要反向遍歷
for (i = LEN - 1; i >= 0; i--) {
temp[bucket[arr[i] / flag % 10]] = arr[i];
bucket[arr[i] / flag % 10]--;
}
//利用temp數組修改原數組
for (i = 0; i < LEN; i++) {
arr[i] = temp[i];
}
}
部分算法動畫轉載自:十大經典排序算法 | 菜鳥教程