編程論到極致,核心非代碼,即思想。
所以,真正的編程高手同時是思想獨到及富有智慧(注意與聰明區別)的人。
每一個算法都是一種智慧的凝聚或萃取,值得我們學習從而提高自己,開拓思路,更重要的是轉換思維角度。
其實,我們大多數人都活在“默認狀態”下。沒有發覺自己的獨特可設置選項-----思想。
言歸正傳(呵呵!恢復默認狀態),以下學習基數排序。
【1】基數排序
以前研究的各種排序算法,都是通過比較數據大小的方法對欲排數據序列進行排序處理過程,而基數排序卻不再相同。
那么,基數排序是采用怎樣策略進行數據排序的呢?
簡略概述:基數排序是通過“分配”和“收集”過程來實現排序。而這個思想該如何理解呢?請看以下例子:
(1)假設有欲排數據序列如下所示:
73 22 93 43 55 14 28 65 39 81
首先,根據每個數據個位數的數值,在遍歷數據時將它們各自分配到編號0至9的桶(個位數值與桶號一一對應)中。
分配結果(邏輯想象)如下圖所示:
分配結束后。接下來將所有桶中(由頂至底)所盛數據按照桶號由小到大依次重新收集串起來,得到如下仍然無序的數據序列:
81 22 73 93 43 14 55 65 28 39
接着,再進行一次分配,這次根據每個數據十位數的數值來分配(原理同上),分配結果(邏輯想象)如下圖所示:
分配結束后。接下來再將所有桶中(由頂至底)所盛的數據(原理同上)依次重新再收集串接起來,得到如下的數據序列:
14 22 28 39 43 55 65 73 81 93
至此,觀察可以看到:原無序數據序列已經變成有序的數據序列,即排序完畢。
如果排序的數據序列有三位數以上的數據,則重復進行以上的動作直至最高位數為止。
那么,到這里為止,你覺得自己是不是一個細心的人?不要不假思索的回答我。不論回答什么樣的問題,都要做到心比頭快,頭比嘴快。
仔細看看你對整個排序的過程中還有哪些疑惑?真看不到?覺得我做得很好?抑或前面沒看懂?
如果你看到這里真心沒有意識到或發現這個問題,那我告訴你:悄悄去找個牆角蹲下用小拇指畫圈圈(好好反省反省)。
追問:觀察原無序數據序列中73 93 43 三個數據的順序,在經過第一次(按照個位數值,它們三者應該是在同一個桶中)分配之后,
在桶中順序由底至頂應該為73 93 43(即就是裝的遲的在最上面,對應我們上面的邏輯想象應該是43 93 73),對吧?這個應該可以想明白吧?理論上應該是這樣的。
但是、但是、但是分配后很明顯在3號桶中三者的順序剛好相反。這點難道你沒有發現嗎?或者是發現了覺得不屑談及(算我貽笑大方)?
其實這個也正是基數排序穩定性的原因(分配時由末位向首位進行,即逆向遍歷),請看下文的詳細分析。
再思考一個問題:既然我們可以從最低位到最高位進行如此的分配收集,那么是否可以由最高位到最低位依次操作呢? 答案是完全可以的。
基於兩種不同的排序順序,我們將基數排序分為LSD(Least significant digital)或 MSD(Most significant digital),
LSD的排序方式由數值的最右邊(低位)開始,而MSD則相反,由數值的最左邊(高位)開始。
注意一點:LSD的基數排序適用於位數少的數列,如果位數多的話,使用MSD的效率會比較好。
MSD的方式與LSD相反,是由高位數為基底開始進行分配,但在分配之后並不馬上合並回一個數組中,而是在每個“桶子”中建立“子桶”,將每個桶子中的數值按照下一數位的值分配到“子桶”中。
在進行完最低位數的分配后再合並回單一的數組中。
(2)我們把撲克牌的排序看成由花色和面值兩個數據項組成的主關鍵字排序。
要求如下:
花色順序:梅花<方塊<紅心<黑桃
面值順序:2<3<4<...<10<J<Q<K<A
那么,若要將一副撲克牌排成下列次序:
梅花2,...,梅花A,方塊2,...,方塊A,紅心2,...,紅心A,黑桃2,...,黑桃A。
有兩種排序方法:
<1> 先按花色分成四堆,把各堆收集起來;然后對每堆按面值由小到大排列,再按花色從小到大按堆收疊起來。----稱為"最高位優先"(MSD)法。
<2> 先按面值由小到大排列成13堆,然后從小到大收集起來;再按花色不同分成四堆,最后順序收集起來。----稱為"最低位優先"(LSD)法。
【2】代碼實現
(1)MSD法實現
最高位優先法通常是一個遞歸的過程:
<1> 先根據最高位關鍵碼K1排序,得到若干對象組,對象組中每個對象都有相同關鍵碼K1。
<2> 再分別對每組中對象根據關鍵碼K2進行排序,按K2值的不同,再分成若干個更小的子組,每個子組中的對象具有相同的K1和K2值。
<3> 依此重復,直到對關鍵碼Kd完成排序為止。
<4> 最后,把所有子組中的對象依次連接起來,就得到一個有序的對象序列。
示例代碼如下:
1 #include <iostream>
2 #include <malloc.h>
3 using namespace std; 4
5 int getDigit(int x, int d) 6 { 7 int a[] = {1, 1, 10, 100}; // 因為待排數據最大數據也只是三位數,所以在此只需要到百位就滿足
8 return ((x / a[d]) % 10); // 確定桶號
9 } 10
11 void printArr(int ar[], int n) 12 { 13 for (int i = 0; i < n; ++i) 14 { 15 cout << ar[i] << " "; 16 } 17 cout << endl; 18 } 19
20 void msdRadixSort(int arr[], int begin, int end, int d) 21 { 22 const int radix = 10; 23 int count[radix], i, j; 24 // 置空
25 for (i = 0; i < radix; ++i) 26 { 27 count[i] = 0; 28 } 29 // 分配桶存儲空間
30 int *bucket = (int *)malloc((end - begin + 1) * sizeof(int)); 31 // 統計各桶需要裝的元素的個數
32 for (i = begin; i <= end; ++i) 33 { 34 count[getDigit(arr[i], d)]++; 35 } 36 // count[i]表示當前d位數值為i的桶底邊界索引,即count[1]表示當前d位數值為1的桶底邊界索引 37 // 或表示當前d位數值為(i+1)的桶頂索引,即count[1]表示當前d位數值為2的桶頂索引
38 for (i = 1; i < radix; ++i) 39 { 40 count[i] = count[i] + count[i - 1]; 41 } 42 // 必須從右向左掃描
43 for (i = end; i >= begin; --i) 44 { 45 j = getDigit(arr[i], d); // 求出關鍵碼第d位的數值。例如:576的第3位是((576 / 100) % 10) = 5
46 bucket[count[j] - 1] = arr[i]; // 放入對應的桶中,(count[j]-1)是當前d位數值為j的桶底索引
47 --count[j]; // 當前d位數值為j的桶底邊界索引減一
48 } 49 // 注意:執行至此,count[i]表示當前d位數值為i的桶頂索引 50 // 從各個桶中收集數據
51 for (i = begin, j = 0; i <= end; ++i, ++j) 52 { 53 arr[i] = bucket[j]; 54 } 55 // 釋放存儲空間
56 free(bucket); 57 // 對各個桶中的數據進行再排序
58 for (i = 0; i < radix; ++i) 59 { 60 int p1 = begin + count[i]; // 當前d位數值為i的桶頂索引
61 int p2 = 0; // 當前d位數值為i的桶底索引
62 if (i < radix - 1) 63 { 64 p2 = begin + count[i + 1] - 1; 65 } 66 else
67 { 68 p2 = end; 69 } 70 if (p1 < p2 && d > 1) 71 { 72 msdRadixSort(arr, p1, p2, d - 1); // 對桶遞歸調用,進行基數排序,數位降1
73 } 74 } 75 } 76
77 void main() 78 { 79 int ar[] = {20, 80, 90, 589, 998, 965, 852, 123, 456, 789}; 80 int len = sizeof(ar) / sizeof(int); 81 cout << "排序前數據如下:" << endl; 82 printArr(ar, len); 83 msdRadixSort(ar, 0, len - 1, 3); 84 cout << "排序后結果如下:" << endl; 85 printArr(ar, len); 86
87 system("pause"); 88 } 89
90 /*
91 排序前數據如下: 92 20 80 90 589 998 965 852 123 456 789 93 排序后結果如下: 94 20 80 90 123 456 589 789 852 965 998 95 請按任意鍵繼續. . . 96 */
(2)LSD法實現
最低位優先法,首先依據最低位關鍵碼Kd對所有對象進行一趟排序,
再依據次低位關鍵碼Kd-1對上一趟排序的結果再排序,
依次重復,直到依據關鍵碼K1最后一趟排序完成,就可以得到一個有序的序列。
使用這種排序方法對每一個關鍵碼進行排序時,不需要再分組,而是整個對象組。
示例代碼如下:
1 #include <iostream>
2 #include <malloc.h>
3 using namespace std; 4
5 int getDigit(int x, int d) 6 { 7 int a[] = {1, 1, 10, 100}; //最大三位數,所以這里只要百位就滿足了。
8 return (x / a[d]) % 10; 9 } 10
11 void printArr(int ar[], int n) 12 { 13 for (int i = 0; i < n; ++i) 14 { 15 cout << ar[i] << " "; 16 } 17 cout << endl; 18 } 19
20 void lsdRadixSort(int arr[], int begin, int end, int d) 21 { 22 const int radix = 10; 23 int count[radix], i, j; 24
25 // 開辟所有桶的空間
26 int *bucket = (int *)malloc((end - begin + 1) * sizeof(int)); 27
28 cout << "排序過程如下:" << endl; 29 // k == 1 表示個位數 30 // k == 2 表示十位數 31 // k == 3 表示百位數
32 for (int k = 1; k <= d; ++k) 33 { 34 // 置空
35 for (i = 0; i < radix; ++i) 36 { 37 count[i] = 0; 38 } 39 // 統計各個桶中所盛數據個數
40 for (i = begin; i <= end; ++i) 41 { 42 count[getDigit(arr[i], k)]++; 43 } 44 cout << ":: (k == " << k << ") ::" << endl; 45 printArr(count, radix); 46 // count[i]表示第k位數值為i的桶底邊界索引,即count[1]表示第k位數值為1的桶底邊界索引 47 // 或表示第k位數值為2的桶頂索引,即count[1]表示第k位數值為2的頂索引
48 for (i = 1; i < radix; ++i) 49 { 50 count[i] = count[i] + count[i - 1]; 51 } 52 printArr(count, radix); 53 // 把數據依次裝入桶(注意:裝入時的分配技巧)
54 for (i = end; i >= begin; --i) // 必須從右向左掃描
55 { 56 j = getDigit(arr[i], k); // 求出關鍵碼的第k位的數值, 例如:576的第3位是5
57 bucket[count[j] - 1] = arr[i]; // 放入對應的桶中,(count[j]-1)表示第k位數值為j的桶底索引
58 --count[j]; // 當前第k位數值為j的桶底邊界索引減一
59 } 60 // 注意:執行至此,count[i]表示當前第k位數值為i的桶頂索引
61 printArr(count, radix); 62
63 // 從各個桶中收集數據
64 for (i = begin, j = 0; i <= end; ++i, ++j) 65 { 66 arr[i] = bucket[j]; 67 } 68 printArr(arr, radix); 69 } 70
71 free(bucket); 72 } 73
74 void main() 75 { 76 int br[10] = {20, 80, 90, 589, 998, 965, 852, 123, 456, 789}; 77 cout << "原數據如下:" << endl; 78 printArr(br, 10); 79 lsdRadixSort(br, 0, 9, 3); 80 cout << "排序后數據如下:" << endl; 81 printArr(br, 10); 82
83 system("pause"); 84 } 85
86 /*
87 運行結果: 88 原數據如下: 89 20 80 90 589 998 965 852 123 456 789 90 排序過程如下: 91 :: (k == 1) :: 92 3 0 1 1 0 1 1 0 1 2 93 3 3 4 5 5 6 7 7 8 10 94 0 3 3 4 5 5 6 7 7 8 95 20 80 90 852 123 965 456 998 589 789 96 :: (k == 2) :: 97 0 0 2 0 0 2 1 0 3 2 98 0 0 2 2 2 4 5 5 8 10 99 0 0 0 2 2 2 4 5 5 8 100 20 123 852 456 965 80 589 789 90 998 101 :: (k == 3) :: 102 3 1 0 0 1 1 0 1 1 2 103 3 4 4 4 5 6 6 7 8 10 104 0 3 4 4 4 5 6 6 7 8 105 20 80 90 123 456 589 789 852 965 998 106 排序后數據如下: 107 20 80 90 123 456 589 789 852 965 998 108 請按任意鍵繼續. . . 109 */
注意:以上兩種方法,我們均使用數組模擬桶,關於數組模擬桶詳細講解請參考隨筆《桶排序》
【3】基數排序穩定性分析
基數排序是穩定性排序算法。那么,到底如何理解它所謂的穩定特性呢?
(1)示例分析
比如:我們有如下欲排數據序列:
注意:在原始數據序列中,兩個數據值相同(即12),而索引分別為1、4。
而所謂穩定性,即要求在排序完成后的有序數據序列中兩個相同值的索引仍然是后者大於前者(最末數據12的索引值 > 第二個數據12的索引值)。
下面按LSD邏輯進行演示:
第一次,按個位數值分配。結果如下圖所示:
然后,收集數據結果如下:
第二次,按十位數值分配。結果如下圖所示:
然后,收集數據結果如下:
注意:分配時是從欲排數據序列的末位開始進行,逐次分配至首位。
(2)結果分析:
原始數據序列中相同數據值12的索引分別為1、4。
由最終的排序結果可看出:在有序數據序列中相同值(12)的索引分別為1、2。
很明顯,原始數據序列中的最末位的12在有序數據序列中位置仍然居於原始數據序列中第二個數據值12之后(即按索引2 > 1)。
(3)如果從左向右掃描,同樣的示例,依據LSD方法的規則,我們可以看到如下的排序過程:
顯然,最終結果是無序的,這種掃描方式是錯誤的。所以,基數排序必須由后向前遍歷原始數據序列。
排序結束。相信一定一目了然。
Good Good Study, Day Day Up.
順序 選擇 循環 堅持 總結