最早擁有排序概念的機器出現在 1901 至 1904 年間由 Hollerith 發明出使用基數排序法的分類機,此機器系統包括打孔,制表等功能,1908 年分類機第一次應用于人口普查,并且在兩年內完成了所有的普查數據和歸檔。
Hollerith 在 1896 年創立的分類機公司的前身,為電腦制表記錄公司(CTR)。他在電腦制表記錄公司(CTR)曾擔任顧問工程師,直到1921年退休,而電腦制表記錄公司(CTR)在1924年正式改名為IBM。
---- 摘自《維基百科 - 插入排序》
期中已到,期末將至?!端惴ㄔO計與分析》的“預習”階段藉此開始~。在眾多的算法思想中,如果你沒有扎實的數據結構的功底,不知道棧與隊列,哈希表與二叉樹,不妨可以從排序算法開始練手??v觀各類排序算法,常見的、經典的排序算法將由此篇引出。
排序算法的輸出必須遵守的下列兩個原則:
- 輸出結果為遞增序列(遞增是針對所需的排序順序而言)
- 輸出結果是原輸入的一種排列、或是重組
十大經典的排序算法及其時間復雜度和穩定性如上表所示。判斷一個排序算法是否穩定是看在相等的兩個數據排序算法執行完成后是否會前后關系顛倒,如若顛倒,則稱該排序算法為不穩定,例如選擇排序和快速排序。
排序前:(4, 1) (3, 1) (3, 7)(5, 6)
排序后:(3, 1) (3, 7) (4, 1) (5, 6) (穩定,維持次序)
排序后:(3, 7) (3, 1) (4, 1) (5, 6) (不穩定,次序被改變)
00.交換兩個整數的值
接下來十個經典排序算法的詳細探討缺少不了交換兩個整數值的掌握,這里回顧一下其中三種方交換法:用臨時變量交換兩個整數的值(SwapTwo_1)、不用第三方變量交換兩個整數的值(SwapTwo_2)、使用位運算交換兩個整數的值(SwapTwo_3)。其中用臨時變量交換兩個整數的值效率最高,后倆者只適用于內置整型數據類型的交換,并不高效。
void SwapTwo_1 (int *a, int* b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}
void SwapTwo_2 (int *a, int *b) {
*a = *a + *b;
*b = *a - *b;
*a = *a - *b;
}
void SwapTwo_3 (int *a, int *b) {
*a ^= *b;
*b ^= *a;
*a ^= *b;
}
01. 冒泡排序(BubbleSort)
先不說公司面試筆試,大學實驗室的納新題里最常有的就是冒泡排序。冒泡排序重復地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。這個算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。由于它的簡潔,冒泡排序通常被用來對于程序設計入門的學生介紹算法的概念。
[圖片上傳失敗...(image-93185f-1513765803581)]](http://upload-images.jianshu.io/upload_images/2558748-990f8de3fbdbb50d.gif?imageMogr2/auto-orient/strip)
上圖可見,冒泡排序算法的運作如下:
- 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最后一對。這步做完后,最后的元素會是最大的數。
- 針對所有的元素重復以上的步驟,除了最后一個。
- 持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較。
通俗來講,整個冒泡排序就是通過兩次循環,外層循環將此輪最大(小)值固定到此輪尾部,內層循環“冒泡”比較相鄰的兩個元素并決定是否交換位置。
從上圖也可理解冒泡排序如何將每一輪最大(?。┲倒潭ǖ酱溯單膊浚何膊靠倿橛行驙顟B,前面無序狀態區根據大小規則冒泡向后方傳遞最值。
/*
* 冒泡排序。每次外循環將最值固定到數組最后
* @param: {int []} arr
* @param: {int} len
* @return null;
*/
void BubbleSort (int arr[], int len) {
int i, j, temp;
for (int i = 0; i < len - 1; i++) {
// 每趟 i 循環將最大(小)值固定到最后一位
for (int j = 0; j < len - 1 - i; j++) {
// 每趟 j 循環循環沒有被固定到后方的數字
if (arr[j] > arr[j + 1]) {
// arr[j] < arr[j + 1] 代表從小到大排序
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
更多十大經典排序算法請戳我的 Github 倉庫 @LeetCode-C
02. 選擇排序(SelectionSort)
選擇排序首先在未排序序列中找到最?。ù螅┰?,存放到排序序列的起始位置,然后,再從剩余未排序元素中繼續尋找最小(大)元素,然后放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
選擇排序的主要優點與數據移動有關。如果某個元素位于正確的最終位置上,則它不會被移動。選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,因此對n個元素的表進行排序總共進行至多n-1次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬于非常好的一種。
上圖左下角和右上角可以分別看做排序序列起始位置(已排序區)和排序序列末尾(未排序區),左下角一直穩定更新,但選擇排序不穩定,即排序后曾經相同的兩個元素的前后位置關系可能會發生顛倒。
/*
* 選擇排序。每次外循環選擇最值固定到數組開始
* @param: {int []} arr[]
* @param: {int} len
* @return null;
*/
void SelectionSort (int arr[], int len) {
int i, j, temp, min;
for (i = 0; i < len - 1; i++) {
min = i;
for (j = i + 1; j < len; j++) {
if (arr[j] < arr[min]) {
// 只需找到最小的值的位置后一次性替換
min = j;
}
}
temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
}
更多十大經典排序算法請戳我的 Github 倉庫 @LeetCode-C
03. 插入排序(InsertionSort)
插入排序的工作原理是通過構建有序序列,對于未排序數據,在已排序序列中從后向前掃描,找到相應位置并插入。插入排序在實現上,通常采用 in-place
排序(即只需用到O(1)的額外空間的排序),因而在從后向前掃描過程中,需要反復把已排序元素逐步向后挪位,為最新元素提供插入空間。
一般來說,插入排序都采用 in-place 在數組上實現。具體算法描述如下:
- 從第一個元素開始,該元素可以認為已經被排序
- 取出下一個元素,在已經排序的元素序列中從后向前掃描
- 如果該元素(已排序)大于新元素,將該元素移到下一位置
- 重復步驟 3,直到找到已排序的元素小于或者等于新元素的位置
- 將新元素插入到該位置后
- 重復步驟 2~5
如果比較操作的代價比交換操作大的話,可以采用二分查找法來減少比較操作的數目。該算法可以認為是插入排序的一個變種,稱為二分查找插入排序。這里先不做涉及。
/*
* 插入排序。前面有序的數字循環向后留給滿足條件的第一個無序數字
* @param: {int []} arr
* @param: {int} len
* @return null;
*/
void InsertionSort (int arr[], int len)
{
int i, j, temp;
for (i = 1; i < len; i++) {
// 與已排序的數逐一比較,大于 temp 時,該數向后移
temp = arr[i];
// 如果將賦值放到下面的for循環內, 會導致在第10行出現 j 未聲明的錯誤
j = i - 1;
for (; j >= 0 && arr[j] > temp; j--) {
// j 循環到-1時,由于短路求值,不會運算 arr[-1]
arr[j + 1] = arr[j];
}
arr[j + 1] = temp;
// 被排序數放到正確的位置
}
}
更多十大經典排序算法請戳我的 Github 倉庫 @LeetCode-C
04. 希爾排序(ShellSort)
希爾排序按其設計者希爾(Donald Shell)的名字命名,該算法由1959年公布。希爾排序也稱遞減增量排序算法,是插入排序的一種更高效的改進版本。希爾排序是非穩定排序算法。希爾排序是基于插入排序的以下兩點性質而提出改進方法的:
- 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率
- 插入排序一般來說是低效的,因為插入排序每次只能將數據移動一位
希爾排序通過將比較的全部元素分為幾個區域來提升插入排序的性能。這樣可以讓一個元素可以一次性地朝最終位置前進一大步。然后算法再取越來越小的步長進行排序,算法的最后一步就是普通的插入排序,但是到了這步,需排序的數據幾乎是已排好的了(此時插入排序較快)。
步長的選擇是希爾排序的重要部分。只要最終步長為1任何步長序列都可以工作。算法最開始以一定的步長進行排序。然后會繼續以一定步長進行排序,最終算法以步長為1進行排序。當步長為1時,算法變為插入排序,這就保證了數據一定會被排序。
已知的最好步長序列是由 Sedgewick 提出的(1, 5, 19, 41, 109,...),這項研究也表明“比較在希爾排序中是最主要的操作,而不是交換。”用這樣步長序列的希爾排序比插入排序要快,甚至在小數組中比快速排序和堆排序還快,但是在涉及大量數據時希爾排序還是比快速排序慢。
/*
* 希爾排序。
* @param: {int []} arr
* @param: {int} len
* @return null;
*/
void ShellSort(int arr[], int len) {
int gap, i, j;
int temp;
for (gap = len >> 1; gap > 0; gap >>= 1) {
// gap 為間隔,每次間隔折半
for (i = gap; i < len; i++) {
// 循環該輪數組后半段
temp = arr[i];
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
// 根據當前 grap 間隔和條件進行插入排序前的后移
arr[j + gap] = arr[j];
}
// 插入到當前位置
arr[j + gap] = temp;
}
}
}
更多十大經典排序算法請戳我的 Github 倉庫 @LeetCode-C
05. 歸并排序(MergeSort)
歸并排序是創建在歸并操作上的一種有效的排序算法,效率為 O(n log n)。1945年由約翰·馮·諾伊曼首次提出。該算法是采用分治法(Divide and Conquer)的一個非常典型的應用,且各層分治遞歸可以同時進行。
歸并操作(merge),也叫歸并算法,指的是將兩個已經排序的序列合并成一個序列的操作。歸并排序算法依賴歸并操作。
歸并排序用迭代法和遞歸法都可以實現,迭代法的算法步驟為:
- 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合并后的序列
- 設定兩個指針,最初位置分別為兩個已經排序序列的起始位置
- 比較兩個指針所指向的元素,選擇相對小的元素放入到合并空間,并移動指針到下一位置
- 重復步驟3直到某一指針到達序列尾
- 將另一序列剩下的所有元素直接復制到合并序列尾
遞歸法的算法步驟為:
- 將序列每相鄰兩個數字進行歸并操作,形成 floor(n/2) 個序列,排序后每個序列包含兩個元素
- 將上述序列再次歸并,形成 floor(n/4) 個序列,每個序列包含四個元素
- 重復步驟 2,直到所有元素排序完畢
/*
* 歸并排序。迭代版
* @param: {int []} arr
* @param: {int} len
* @return null;
*/
void MergeSort_1 (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 + seg) {
int low = start, mid = Min (start + seg, len), high = Min(start + seg + seg, 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);
}
int Min(int x, int y) {
return x < y ? x : y;
}
/*
* 歸并排序。遞歸版
* @param: {int []} arr
* @param: {const int} len
* @return null;
*/
void MergeSort_2 (int arr[], const int len) {
int reg[len];
merge_sort_recursive(arr, reg, 0, len - 1);
}
void merge_sort_recursive (int arr[], int reg[], int start, int end) {
if (start >= end)
return;
int len = end - start, mid = (len >> 1) + start;
int start1 = start, end1 = mid;
int start2 = mid + 1, end2 = end;
merge_sort_recursive(arr, reg, start1, end1);
merge_sort_recursive(arr, reg, start2, end2);
int k = start;
while (start1 <= end1 && start2 <= end2)
reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
while (start1 <= end1)
reg[k++] = arr[start1++];
while (start2 <= end2)
reg[k++] = arr[start2++];
for (k = start; k <= end; k++)
arr[k] = reg[k];
}
總結【上】
這篇文章“十大經典排序算法及其 C 語言實現【上】”引出了十大經典算法的前五個并用 C 語言實踐:冒泡排序、選擇排序、插入排序、希爾排序和歸并排序,并作出了充足的圖文解釋。即將推出的“十大經典排序算法及其 C 語言實現【下】”將對剩下五個經典算法快速排序、堆排序、計數排序、桶排序、基數排序作出完善,盡請期待~。
- Hello,我是韓亦樂,現任本科軟工男一枚。軟件工程專業的一路學習中,我有很多感悟,也享受持續分享的過程。如果想了解更多或能及時收到我的最新文章,歡迎訂閱我的個人微信號:韓亦樂。我的簡書個人主頁中,有我的訂閱號二維碼和 Github 主頁地址;我的知乎主頁 中也會堅持產出,歡迎關注。
- 本文內部編號經由我的 Github 相關倉庫統一管理;本文可能發布在多個平臺但僅在上述倉庫中長期維護;本文同時采用【知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議】進行許可。