本文轉載於 SegmentFault 社區
作者:FiTeen
https://mp.weixin.qq.com/s/qEc9cJv6wooTZybaUMS2jA
排序算法是程序員必備的基礎知識,弄明白它們的原理和實現很有必要。本文中將通過非常細節的動畫展示出算法的原理,配合代碼更容易理解。
概述
由於待排序的元素數量不同,使得排序過程中涉及的存儲器不同,可將排序方法分為兩類:一類是內部排序,指的是待排序列存放在計算機隨機存儲器中進行的排序過程;另一類是外部排序,指的是待排序的元素的數量很大,以致內存一次不能容納全部記錄,在排序過程中尚需對外存進行訪問的排序過程。
我們可以將常見的內部排序算法可以分成兩類:
比較類排序:通過比較來決定元素間的相對次序,時間復雜度為 O(nlogn)~O(n²)。屬於比較類的有:
非比較類排序:不通過比較來決定元素間的相對次序,其時間復雜度可以突破 O(nlogn),以線性時間運行。屬於非比較類的有:
名次解釋:
時間/空間復雜度:描述一個算法執行時間/占用空間與數據規模的增長關系
n:待排序列的個數
r:“桶”的個數(上面的三種非比較類排序都是基於“桶”的思想實現的)
d:待排序列的最高位數
In-place:原地算法,指的是占用常用內存,不占用額外內存。空間復雜度為 O(1) 的都可以認為是原地算法
Out-place:非原地算法,占用額外內存
穩定性:假設待排序列中兩元素相等,排序前后這兩個相等元素的相對位置不變,則認為是穩定的。
一、冒泡排序
冒泡排序(Bubble Sort),顧名思義,就是指越小的元素會經由交換慢慢“浮”到數列的頂端。
算法原理
1. 從左到右,依次比較相鄰的元素大小,更大的元素交換到右邊;
2. 從第一組相鄰元素比較到最后一組相鄰元素,這一步結束最后一個元素必然是參與比較的元素中最大的元素;
3. 按照大的居右原則,重新從左到后比較,前一輪中得到的最后一個元素不參與比較,得出新一輪的最大元素;
4. 按照上述規則,每一輪結束會減少一個元素參與比較,直到沒有任何一組元素需要比較。
動圖演示
代碼實現
1 void bubble_sort(int arr[], int n) { 2 int i, j; 3 for (i = 0; i < n - 1; i++) { 4 for (j = 0; j < n - i - 1; j++) { 5 if (arr[j] > arr[j + 1]) { 6 swap(arr, j, j+1); 7 } 8 } 9 } 10 }
另一種更好的實現:(子函數形式請繼續往下閱讀原文作者的代碼)
1 int a[10]={10,3,23,45,12,67,44,22,33,18}; 2 n=10; 3 4 for(i=1;i<n;i++) 5 { 6 flag=1; 7 for(j=0;j<n-i;j++) 8 { 9 if(a[j]>a[j+1]) 10 { 11 flag=0; 12 t=a[j]; a[j]=a[j+1]; a[j+1]=t; 13 } 14 } 15 if(flag==1) break; 16 }
算法分析
冒泡排序屬於交換排序,是穩定排序,平均時間復雜度為 O(n²),空間復雜度為 O(1)。
但是我們常看到冒泡排序的最優時間復雜度是 O(n),那要如何優化呢?
我們可以用一個 flag 參數記錄新一輪的排序中元素是否做過交換,如果沒有,說明前面參與比較過的元素已經是正序,那就沒必要再從頭比較了。代碼實現如下:
1 void bubble_sort_quicker(int arr[], int n) { 2 int i, j, flag; 3 for (i = 0; i < n - 1; i++) { 4 flag = 0; 5 for (j = 0; j < n - i - 1; j++) { 6 if (arr[j] > arr[j + 1]) { 7 swap(arr, j, j+1); 8 flag = 1; 9 } 10 } 11 if (!flag) return; 12 } 13 }
二、快速排序
快速排序(Quick Sort),是冒泡排序的改進版,之所以“快速”,是因為使用了分治法。它也屬於交換排序,通過元素之間的位置交換來達到排序的目的。
基本思想
在序列中隨機挑選一個元素作基准,將小於基准的元素放在基准之前,大於基准的元素放在基准之后,再分別對小數區與大數區進行排序。
一趟快速排序的具體做法是:
1. 設兩個指針 i 和 j,分別指向序列的頭部和尾部;
2. 先從 j 所指的位置向前搜索,找到第一個比基准小的值,把它與基准交換位置;
3. 再從 i 所指的位置向后搜索,找到第一個比基准大的值,把它與基准交換位置;
4. 重復 2、3 兩步,直到 i = j。
仔細研究一下上述算法我們會發現,在排序過程中,對基准的移動其實是多余的,因為只有一趟排序結束時,也就是 i = j 的位置才是基准的最終位置。
由此可以優化一下算法:
1. 設兩個指針 i 和 j,分別指向序列的頭部和尾部;
2. 先從 j 所指的位置向前搜索,找到第一個比基准小的數值后停下來,再從 i 所指的位置向后搜索,找到第一個比基准大的數值后停下來,把 i 和 j 指向的兩個值交換位置;
3. 重復步驟 2,直到 i = j,最后將相遇點指向的值與基准交換位置。
動圖演示
代碼實現
這里取序列的第一個元素為基准。
1 /* 選取序列的第一個元素作為基准 */ 2 int select_pivot(int arr[], int low) { 3 return arr[low]; 4 } 5 6 void quick_sort(int arr[], int low, int high) { 7 int i, j, pivot; 8 if (low >= high) return; 9 pivot = select_pivot(arr, low); 10 i = low; 11 j = high; 12 while (i != j) { 13 while (arr[j] >= pivot && i < j) j--; 14 while (arr[i] <= pivot && i < j) i++; 15 if (i < j) swap(arr, i, j); 16 } 17 arr[low] = arr[i]; 18 arr[i] = pivot; 19 quick_sort(arr, low, i - 1); 20 quick_sort(arr, i + 1, high); 21 }
算法分析
快速排序是不穩定排序,它的平均時間復雜度為 O(nlogn),平均空間復雜度為 O(logn)。
快速排序中,基准的選取非常重要,它將影響排序的效率。舉個例子,假如序列本身順序隨機,快速排序是所有同數量級時間復雜度的排序算法中平均性能最好的,但如果序列本身已經有序或基本有序,直接選取固定位置,例如第一個元素作為基准,會使快速排序就會淪為冒泡排序,時間復雜度為 O(n^2)。為了避免發生這種情況,引入下面兩種獲取基准的方法:
隨機選取
就是選取序列中的任意一個數為基准的值。
1 /* 隨機選擇基准的位置,區間在 low 和 high 之間 */ 2 int select_pivot_random(int arr[], int low, int high) { 3 srand((unsigned)time(NULL)); 4 int pivot = rand()%(high - low) + low; 5 swap(arr, pivot, low); 6 7 return arr[low]; 8 }
三者取中
就是取起始位置、中間位置、末尾位置指向的元素,對這三個元素排序后取中間數作為基准。
1 /* 取起始位置、中間位置、末尾位置指向的元素三者的中間值作為基准 */ 2 int select_pivot_median_of_three(int arr[], int low, int high) { 3 // 計算數組中間的元素的下標 4 int mid = low + ((high - low) >> 1); 5 // 排序,使 arr[mid] <= arr[low] <= arr[high] 6 if (arr[mid] > arr[high]) swap(arr, mid, high); 7 if (arr[low] > arr[high]) swap(arr, low, high); 8 if (arr[mid] > arr[low]) swap(arr, low, mid); 9 // 使用 low 位置的元素作為基准 10 return arr[low]; 11 }
經驗證明,三者取中的規則可以大大改善快速排序在最壞情況下的性能。
三、插入排序
直接插入排序(Straight Insertion Sort),是一種簡單直觀的排序算法,它的基本操作是不斷地將尚未排好序的數插入到已經排好序的部分,好比打撲克牌時一張張抓牌的動作。
在冒泡排序中,經過每一輪的排序處理后,序列后端的數是排好序的;而對於插入排序來說,經過每一輪的排序處理后,序列前端的數都是排好序的。
基本思想
先將第一個元素視為一個有序子序列,然后從第二個元素起逐個進行插入,直至整個序列變成元素非遞減有序序列為止。如果待插入的元素與有序序列中的某個元素相等,則將待插入元素插入大相等元素的后面。整個排序過程進行 n-1 趟插入。
動圖演示
代碼實現
1 void insertion_sort(int arr[], int n) { 2 int i, j, temp; 3 for (i = 1; i < n; i++) { 4 temp = arr[i]; 5 for (j = i; j > 0 && arr[j - 1] > temp; j--) 6 arr[j] = arr[j - 1]; 7 arr[j] = temp; 8 } 9 }
算法分析
插入排序是穩定排序,平均時間復雜度為 O(n²),空間復雜度為 O(1)。
四、希爾排序
希爾排序(Shell's Sort)是第一個突破 O(n²) 的排序算法,是直接插入排序的改進版,又稱“縮小增量排序”(Diminishing Increment Sort)。它與直接插入排序不同之處在於,它會優先比較距離較遠的元素。
基本思想
先將整個待排序列分割成若干個字序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行一次直接插入排序。
子序列的構成不是簡單地“逐段分割”,將相隔某個增量的記錄組成一個子序列,讓增量逐趟縮短,直到增量為 1 為止。
動圖演示
代碼實現
增量序列可以有各種取法,例如上面動圖所示,增量序列滿足 [n / 2, n / 2 / 2, ..., 1],n 是序列本身的長度,這也是一種比較流行的增量序列定義方式。這時希爾排序的算法可以通過下面的代碼實現:
1 void shell_sort_split_half(int arr[], int n) { 2 int i, j, dk, temp; 3 for (dk = n >> 1; dk > 0; dk = dk >> 1) { 4 for (i = dk; i < n; i++) { 5 temp = arr[i]; 6 for (j = i - dk; j >= 0 && arr[j] > temp; j -= dk) 7 arr[j + dk] = arr[j]; 8 arr[j + dk] = temp; 9 } 10 } 11 }
增量序列也可以有其它的定義方式,那么希爾排序的實現可以歸納成這樣:
1 void shell_insert(int arr[], int n, int dk) { 2 int i, j, temp; 3 for (i = dk; i < n; i += dk) { 4 temp = arr[i]; 5 j = i - dk; 6 while (j >= 0 && temp < arr[j]) { 7 arr[j + dk] = arr[j]; 8 j -= dk; 9 } 10 arr[j + dk] = temp; 11 } 12 } 13 14 void shell_sort(int arr[], int n, int dlta[], int t) { 15 int k; 16 for (k = 0; k < t; ++k) { 17 // 一趟增量為 dlta[k] 的插入排序 18 shell_insert(arr, n, dlta[k]); 19 } 20 }
算法分析
希爾排序是不穩定排序,它的分析是一個復雜的問題,因為它的運行時間依賴於增量序列的選擇,它的平均時間復雜度為 O(n^1.3),最好情況是 O(n),最差情況是 O(n²)。空間復雜度為 O(1)。
五、選擇排序
選擇排序(Selection Sort)是一種簡單直觀的排序算法。它的基本思想就是,每一趟 n-i+1(i=1,2,...,n-1) 個記錄中選取關鍵字最小的記錄作為有序序列的第 i 個記錄。
算法步驟
簡單選擇排序:
1. 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;2. 在剩余未排序元素中繼續尋找最小(大)元素,放到已排序序列的末尾;3. 重復步驟 2,直到所有元素排序完畢。
動圖演示
代碼實現
1 void selection_sort(int arr[], int n) { 2 int i, j; 3 for (i = 0; i < n - 1; i++) { 4 int min = i; 5 for (j = i + 1; j < n; j++) { 6 if (arr[j] < arr[min]) 7 min = j; 8 } 9 swap(arr, min, i); 10 } 11 }
算法分析
選擇排序是不穩定排序,時間復雜度固定為 O(n²),因此它不適用於數據規模較大的序列。不過它也有優點,就是不占用額外的內存空間。
六、堆排序
堆排序(Heap Sort)是指利用堆這種數據結構所設計的一種排序算法。堆的特點:
1. 一顆完全二叉樹(也就是會所生成節點的順序是:從上往下、從左往右)
2. 每一個節點必須滿足父節點的值不大於/不小於子節點的值
基本思想
實現堆排序需要解決兩個問題:
1. 如何將一個無序序列構建成堆?
2. 如何在輸出堆頂元素后,調整剩余元素成為一個新的堆?
以升序為例,算法實現的思路為:
1. 建立一個 build_heap 函數,將數組 tree[0,...n-1] 建立成堆,n 表示數組長度。函數里需要維護的是所有節點的父節點,最后一個子節點下標為 n-1,那么它對應的父節點下標就是 (n-1-1)/2。
2. 構建完一次堆后,最大元素就會被存放在根節點 tree[0]。將 tree[0] 與最后一個元素交換,每一輪通過這種不斷將最大元素后移的方式,來實現排序。
3. 而交換后新的根節點可能不滿足堆的特點了,因此需要一個調整函數 heapify 來對剩余的數組元素進行最大堆性質的維護。如果 tree[i] 表示其中的某個節點,那么 tree[2*i+1] 是左孩子,tree[2*i+2] 是右孩子,選出三者中的最大元素的下標,存放於 max 值中,若 max 不等於 i,則將最大元素交換到 i 下標的位置。但是,此時以 tree[max] 為根節點的子樹可能不滿足堆的性質,需要遞歸調用自身。
動圖演示
代碼實現
1 void heapify(int tree[], int n, int i) { 2 // n 表示序列長度,i 表示父節點下標 3 if (i >= n) return; 4 // 左側子節點下標 5 int left = 2 * i + 1; 6 // 右側子節點下標 7 int right = 2 * i + 2; 8 int max = i; 9 if (left < n && tree[left] > tree[max]) max = left; 10 if (right < n && tree[right] > tree[max]) max = right; 11 if (max != i) { 12 swap(tree, max, i); 13 heapify(tree, n, max); 14 } 15 } 16 17 void build_heap(int tree[], int n) { 18 // 樹最后一個節點的下標 19 int last_node = n - 1; 20 // 最后一個節點對應的父節點下標 21 int parent = (last_node - 1) / 2; 22 int i; 23 for (i = parent; i >= 0; i--) { 24 heapify(tree, n, i); 25 } 26 } 27 28 void heap_sort(int tree[], int n) { 29 build_heap(tree, n); 30 int i; 31 for (i = n - 1; i >= 0; i--) { 32 // 將堆頂元素與最后一個元素交換 33 swap(tree, i, 0); 34 // 調整成大頂堆 35 heapify(tree, i, 0); 36 } 37 }
算法分析
堆排序是不穩定排序,適合數據量較大的序列,它的平均時間復雜度為 Ο(nlogn),空間復雜度為 O(1)。
此外,堆排序僅需一個記錄大小供交換用的輔助存儲空間。
七、歸並排序
歸並排序(Merge Sort)是建立在歸並操作上的一種排序算法。它和快速排序一樣,采用了分治法。
基本思想
歸並的含義是將兩個或兩個以上的有序表組合成一個新的有序表。也就是說,從幾個數據段中逐個選出最小的元素移入新數據段的末尾,使之有序。
那么歸並排序的算法我們可以這樣理解:
假如初始序列含有 n 個記錄,則可以看成是 n 個有序的子序列,每個子序列的長度為 1。然后兩兩歸並,得到 n/2 個長度為2或1的有序子序列;再兩兩歸並,……,如此重復,直到得到一個長度為 n 的有序序列為止,這種排序方法稱為 二路歸並排序,下文介紹的也是這種排序方式。
動圖演示
代碼實現
1 /* 將 arr[L..M] 和 arr[M+1..R] 歸並 */ 2 void merge(int arr[], int L, int M, int R) { 3 int LEFT_SIZE = M - L + 1; 4 int RIGHT_SIZE = R - M; 5 int left[LEFT_SIZE]; 6 int right[RIGHT_SIZE]; 7 int i, j, k; 8 // 以 M 為分割線,把原數組分成左右子數組 9 for (i = L; i <= M; i++) left[i - L] = arr[i]; 10 for (i = M + 1; i <= R; i++) right[i - M - 1] = arr[i]; 11 // 再合並成一個有序數組(從兩個序列中選出最小值依次插入) 12 i = 0; j = 0; k = L; 13 while (i < LEFT_SIZE && j < RIGHT_SIZE) arr[k++] = left[i] < right[j] ? left[i++] : right[j++]; 14 while (i < LEFT_SIZE) arr[k++] = left[i++]; 15 while (j < RIGHT_SIZE) arr[k++] = right[j++]; 16 } 17 18 void merge_sort(int arr[], int L, int R) { 19 if (L == R) return; 20 // 將 arr[L..R] 平分為 arr[L..M] 和 arr[M+1..R] 21 int M = (L + R) / 2; 22 // 分別遞歸地將子序列排序為有序數列 23 merge_sort(arr, L, M); 24 merge_sort(arr, M + 1, R); 25 // 將兩個排序后的子序列再歸並到 arr 26 merge(arr, L, M, R); 27 }
算法分析
歸並排序是穩定排序,它和選擇排序一樣,性能不受輸入數據的影響,但表現比選擇排序更好,它的時間復雜度始終為 O(nlogn),但它需要額外的內存空間,空間復雜度為 O(n)。
八、桶排序
桶排序(Bucket sort)是計數排序的升級版。它利用了函數的映射關系,高效與否的關鍵就在於這個映射函數的確定。
桶排序的工作的原理:假設輸入數據服從均勻分布,將數據分到有限數量的桶里,每個桶再分別排序(也有可能是使用別的排序算法或是以遞歸方式繼續用桶排序進行排序)。
算法步驟
1. 設置固定數量的空桶;
2. 把數據放在對應的桶內;
3. 分別對每個非空桶內數據進行排序;
4. 拼接非空的桶內數據,得到最終的結果。
動圖演示
代碼實現
1 void bucket_sort(int arr[], int n, int r) { 2 if (arr == NULL || r < 1) return; 3 4 // 根據最大/最小元素和桶數量,計算出每個桶對應的元素范圍 5 int max = arr[0], min = arr[0]; 6 int i, j; 7 for (i = 1; i < n; i++) { 8 if (max < arr[i]) max = arr[i]; 9 if (min > arr[i]) min = arr[i]; 10 } 11 int range = (max - min + 1) / r + 1; 12 13 // 建立桶對應的二維數組,一個桶里最多可能出現 n 個元素 14 int buckets[r][n]; 15 memset(buckets, 0, sizeof(buckets)); 16 int counts[r]; 17 memset(counts, 0, sizeof(counts)); 18 for (i = 0; i < n; i++) { 19 int k = (arr[i] - min) / range; 20 buckets[k][counts[k]++] = arr[i]; 21 } 22 23 int index = 0; 24 for (i = 0; i < r; i++) { 25 // 分別對每個非空桶內數據進行排序,比如計數排序 26 if (counts[i] == 0) continue; 27 counting_sort(buckets[i], counts[i]); 28 // 拼接非空的桶內數據,得到最終的結果 29 for (j = 0; j < counts[i]; j++) { 30 arr[index++] = buckets[i][j]; 31 } 32 } 33 }
算法分析
桶排序是穩定排序,但僅限於桶排序本身,假如桶內排序采用了快速排序之類的非穩定排序,那么就是不穩定的。
時間復雜度
桶排序的時間復雜度可以這樣看:
- n 次循環,每個數據裝入桶
- r 次循環,每個桶中的數據進行排序(每個桶中平均有 n/r 個數據)
假如桶內排序用的是選擇排序這類時間復雜度較高的排序,整個桶排序的時間復雜度就是 O(n)+O(n²),視作 O(n²),這是最差的情況;
假如桶內排序用的是比較先進的排序算法,時間復雜度為 O(nlogn),那么整個桶排序的時間復雜度為 O(n)+O(r*(n/r)*log(n/r))=O(n+nlog(n/r))。
k=nlog(n/r),桶排序的平均時間復雜度為 O(n+k)。當 r 接近於 n 時,k 趨近於 0,這時桶排序的時間復雜度是最優的,就可以認為是 O(n)。
也就是說如果數據被分配到同一個桶中,排序效率最低;但如果數據可以均勻分配到每一個桶中,時間效率最高,可以線性時間運行。但同樣地,桶越多,空間就越大。
空間復雜度
占用額外內存,需要創建 r 個桶的額外空間,以及 n 個元素的額外空間,所以桶排序的空間復雜度為 O(n+r)。
九、計數排序
計數排序(Counting Sort)是一種非比較性質的排序算法,利用了桶的思想。它的核心在於將輸入的數據值轉化為鍵存儲在額外開辟的輔助空間中,也就是說這個輔助空間的長度取決於待排序列中的數據范圍。
如何轉化成桶思想來理解呢?
我們設立 r 個桶,桶的鍵值分別對應從序列最小值升序到最大值的所有數值。接着,按照鍵值,依次把元素放進對應的桶中,然后統計出每個桶中分別有多少元素,再通過對桶內數據的計算,即可確定每一個元素最終的位置。
算法步驟
1. 找出待排序列中最大值 max 和最小值 min,算出序列的數據范圍 r = max - min + 1,申請輔助空間 C[r];
2. 遍歷待排序列,統計序列中每個值為 i 的元素出現的次數,記錄在輔助空間的第 i 位;
3. 對輔助空間內的數據進行計算(從空間中的第一個元素開始,每一項和前一項相加),以確定值為 i 的元素在數組中出現的位置;
4. 反向填充目標數組:將每個元素 i 放在目標數組的第 C[i] 位,每放一個元素就將 C[i] 減 1,直到 C 中所有值都是 0
動圖演示
代碼實現
1 void counting_sort(int arr[], int n) { 2 if (arr == NULL) return; 3 // 定義輔助空間並初始化 4 int max = arr[0], min = arr[0]; 5 int i; 6 for (i = 1; i < n; i++) { 7 if (max < arr[i]) max = arr[i]; 8 if (min > arr[i]) min = arr[i]; 9 } 10 int r = max - min + 1; 11 int C[r]; 12 memset(C, 0, sizeof(C)); 13 // 定義目標數組 14 int R[n]; 15 // 統計每個元素出現的次數 16 for (i = 0; i < n; i++) C[arr[i] - min]++; 17 // 對輔助空間內數據進行計算 18 for (i = 1; i < r; i++) C[i] += C[i - 1]; 19 // 反向填充目標數組 20 for (i = n - 1; i >= 0; i--) R[--C[arr[i] - min]] = arr[i]; 21 // 目標數組里的結果重新賦值給 arr 22 for (i = 0; i < n; i++) arr[i] = R[i]; 23 }
算法分析
計數排序屬於非交換排序,是穩定排序,適合數據范圍不顯著大於數據數量的序列。
時間復雜度
它的時間復雜度是線性的,為 O(n+r),r 表示待排序列中的數據范圍,也就是桶的個數。可以這樣理解:將 n 個數據依次放進對應的桶中,再從 r 個桶中把數據按順序取出來。
空間復雜度
占用額外內存,還需要 r 個桶,因此空間復雜度是 O(n+r),計數排序快於任何比較排序算法,但這是通過犧牲空間換取時間來實現的。
十、基數排序
基數排序(Radix Sort)是非比較型排序算法,它和計數排序、桶排序一樣,利用了“桶”的概念。基數排序不需要進行記錄關鍵字間的比較,是一種借助多關鍵字排序的思想對單邏輯關鍵字進行排序的方法。比如數字100,它的個位、十位、百位就是不同的關鍵字。
那么,對於一組亂序的數字,基數排序的實現原理就是將整數按位數(關鍵字)切割成不同的數字,然后按每個位數分別比較。對於關鍵字的選擇,有最高位優先法(MSD法)和最低位優先法(LSD法)兩種方式。MSD 必須將序列先逐層分割成若干子序列,然后再對各子序列進行排序;而 LSD 進行排序時,不必分成子序列,對每個關鍵字都是整個序列參加排序。
算法步驟
以 LSD 法為例:
1. 將所有待比較數值(非負整數)統一為同樣的數位長度,數位不足的數值前面補零2. 從最低位(個位)開始,依次進行一次排序
3. 從最低位排序一直到最高位排序完成以后, 數列就變成一個有序序列
如果要支持負數參加排序,可以將序列中所有的值加上一個常數,使這些值都成為非負數,排好序后,所有的值再減去這個常數。
動圖演示
代碼實現
1 // 基數,范圍0~9 2 #define RADIX 10 3 4 void radix_sort(int arr[], int n) { 5 // 獲取最大值和最小值 6 int max = arr[0], min = arr[0]; 7 int i, j, l; 8 for (i = 1; i < n; i++) { 9 if (max < arr[i]) max = arr[i]; 10 if (min > arr[i]) min = arr[i]; 11 } 12 // 假如序列中有負數,所有數加上一個常數,使序列中所有值變成正數 13 if (min < 0) { 14 for (i = 0; i < n; i++) arr[i] -= min; 15 max -= min; 16 } 17 // 獲取最大值位數 18 int d = 0; 19 while (max > 0) { 20 max /= RADIX; 21 d ++; 22 } 23 int queue[RADIX][n]; 24 memset(queue, 0, sizeof(queue)); 25 int count[RADIX] = {0}; 26 for (i = 0; i < d; i++) { 27 // 分配數據 28 for (j = 0; j < n; j++) { 29 int key = arr[j] % (int)pow(RADIX, i + 1) / (int)pow(RADIX, i); 30 queue[key][count[key]++] = arr[j]; 31 } 32 // 收集數據 33 int c = 0; 34 for (j = 0; j < RADIX; j++) { 35 for (l = 0; l < count[j]; l++) { 36 arr[c++] = queue[j][l]; 37 queue[j][l] = 0; 38 } 39 count[j] = 0; 40 } 41 } 42 // 假如序列中有負數,收集排序結果時再減去前面加上的常數 43 if (min < 0) { 44 for (i = 0; i < n; i++) arr[i] += min; 45 } 46 }
算法分析
基數排序是穩定排序,適用於關鍵字取值范圍固定的排序。
時間復雜度
基數排序可以看作是若干次“分配”和“收集”的過程。假設給定 n 個數,它的最高位數是 d,基數(也就是桶的個數)為 r,那么可以這樣理解:共進行 d 趟排序,每趟排序都要對 n 個數據進行分配,再從 r 個桶中收集回來。所以算法的時間復雜度為 O(d(n+r)),在整數的排序中,r = 10,因此可以簡化成 O(dn),是線性階的排序。
空間復雜度
占用額外內存,需要創建 r 個桶的額外空間,以及 n 個元素的額外空間,所以基數排序的空間復雜度為 O(n+r)。
計數排序 & 桶排序 & 基數排序
這三種排序算法都利用了桶的概念,但對桶的使用方法上有明顯差異:
桶排序:每個桶存儲一定范圍的數值,適用於元素盡可能分布均勻的排序;
計數排序:每個桶只存儲單一鍵值,適用於最大值和最小值盡可能接近的排序;
基數排序:根據鍵值的每位數字來分配桶,適用於非負整數間的排序,且最大值和最小值盡可能接近。