常見的五類排序算法圖解和實現(多關鍵字排序:基數排序以及各個排序算法的總結)


基數排序思想

完全不同於以前的排序算法,可以說,基數排序也叫做多關鍵字排序,基數排序是一種借助“多關鍵字排序”的思想來實現“單關鍵字排序”的內部排序算法。

兩種方式:

1、最高位優先,先按照最高位排成若干子序列,再對子序列按照次高位排序

2、最低位優先:不必分子序列,每次排序全體元素都參與,不比較,而是通過分配+收集的方式。

多關鍵字排序

例:將下表所示的學生成績單按數學成績的等級由高到低排序,數學成績相同的學生再按英語成績的高低等級排序。

      

第一個關鍵字是數學成績,第二個關鍵字是英語成績,每個記錄最終的位置由兩個關鍵字決定。我們將它稱之為復合關鍵字,即多關鍵字排序是按照復合關鍵字的大小排序。

多關鍵字排序的方法:

n 個記錄的序列 {R1, R2, …, Rn} 對關鍵字 (Ki0, Ki1, …, Kid-1) 有序是指:對於序列中任意兩個記錄 Ri 和 Rj  (1≤i < j≤n) 都滿足下列(詞典)有序關系:(Ki0, Ki1, …, Kid-1) <  (Kj0, Kj1, …, Kjd-1) ,其中:K 0  被稱為 最主位關鍵字, Kd-1  被稱為最次位關鍵字。多關鍵字排序按照從最主位關鍵字到最次位關鍵字或從最次位關鍵字到最主位關鍵字的順序逐次排序,分兩種方法:

最高位優先法,簡稱 MSD 法:先按 k 0 排序分組,同一組中記錄,關鍵字 k 0 相等,再對各組按 k 1 排序分成子組,之后,對后面的關鍵字繼續這樣的排序分組,直到按最次位關鍵字  k d  對各子組排序后,再將各組連接起來,便得到一個有序序列。

最低位優先法,簡稱 LSD 法:先從 k d-1 開始排序,再對 k d-2 進行排序,依次重復,直到對 k 0 排序后便得到一個有序序列。

例:學生記錄含三個關鍵字:系別、班號和班內的序列號, 其中以系別為最主位關鍵字。

LSD的排序過程如下:

對 Ki (0≤i≤d -2)進行排序時,只能用穩定的排序方法。用 LSD 法進行的排序,在一定的條件下(即對 Ki 的不同值Ki+1 均取相同值),可通過若干次“分配”和“收集”來實現。

例:先將學生記錄按英語等級由高到低分成 A、B、C、D、E 五個組:

 

然后按從左向右,從上向下的順序將它們收集起來得到關鍵字序列:AA,EA,AB,BB,DB,CB,BC,CD 

再按數學成績由高到低分成 A、B、C、D、E 五個組:

按從上向下,從左向右的順序將其收集起來得到關鍵字序列:AA,AB,BB,BC,CB,CD,DB,EA 

可以看出,這個關鍵字序列已經是有序的了, 對每個關鍵字都是將整個序列按關鍵字分組,然后按順序收集,顯然LSD法,操作比較簡單。

MSD 與 LSD 的不同特點

MSD:必須將序列逐層分割成若干子序列,然后對各子序列分別排序。

LSD:不必分成子序列,對每個關鍵字都是整個序列參加排序;通過若干次分配與收集實現排序。

例:對於關鍵字序列 (101, 203, 567, 231, 478, 352)進行基數排序

可以將每個關鍵字 K 看成由三個單關鍵字組成,即 K= k1k2k3, 每個關鍵字的取值范圍為 0≤ki≤9,所以每個關鍵字可取值的數目為 10。通常將關鍵字取值的數目稱為基數,用 r 表示,在本例中 r =10。對於關鍵字序列(AB, BD, ED)可以將每個關鍵字看成是由二個單字母關鍵字組成的復合關鍵字,並且每個關鍵字的取值范圍為 “A~Z”,所以關鍵字的基數 r = 26。

基數排序可用多關鍵字的LSD方法排序,即對待排序的記錄序列按復合關鍵字從低位到高位的順序交替地進行“分組”、“收集”,最終得到有序的記錄序列。在此我們將一次“分組”、“收集”稱為一趟。

對於由 d 位關鍵字組成的復合關鍵字,需要經過d 趟的“分配”與“收集”。 因此,若 d 值較大,基數排序的時間效率就會隨之降低。

鏈式的基數排序算法  

在計算機上實現基數排序時,為減少所需輔助存儲空間,應采用鏈表作存儲結構,即鏈式基數排序,具體作法為:

1、以靜態鏈表存儲待排記錄,並令表頭指針指向第一個記錄; 

2、“分配” 時,按當前“關鍵字位”所取值,將記錄分配到不同的 “鏈隊列” 中,每個隊列中記錄的 “關鍵字位” 相同;

3、“收集”時,按當前關鍵字位取值從小到大將各隊列首尾相鏈成一個鏈表; 

4、對每個關鍵字位均重復 2 和 3 兩步。 

例:鏈式基數排序,下面以靜態鏈表存儲待排記錄,並令表頭指針指向第一個記錄。

 

“分配” 時,按當前“關鍵字位”所取值,將記錄分配到不同的“鏈隊列”中,每個隊列中記錄的 “關鍵字位” 相同。 因為是 LSD,故從地位開始 ,也就是kd-1位開始,進行一趟分配:

然后xx9,xx3,xx0

又遇到了 xx9,那么按照鏈式隊列的存儲方式,先進先出的入隊(類似一個桶,數據從上面進入,從下面露出

第一趟收集:按當前關鍵字位取值從小到大將各隊列首尾相鏈成一個鏈表;(從隊列的下面出去,先進先出

進行第二趟分配,kd-2位

進行第二題收集

進行第三趟分配,也就是 kd-3位。本例子是 k1位關鍵字

進行第三趟收集

序列按照多關鍵字從小到大的排序有序了

具體實現代碼如下:

  1 //鏈式隊列的節點結構,模擬桶
  2 struct Node
  3 {
  4     int data;//數據域
  5     Node *next;//指針域
  6 };
  7 
  8 //定義程序所需的特殊隊列
  9 class Queue
 10 {
 11 private:
 12     Node *front;//鏈式對列的頭指針
 13     Node *rear;//鏈隊的尾指針
 14     
 15 public:
 16     //構造函數,初始化隊列(帶頭結點的鏈式隊列)
 17     Queue()
 18     {
 19         //開始先構造一個空結點,沒有數據元素存儲
 20         Node *p = new Node;
 21         p->data = NULL;
 22         p->next = NULL;
 23         //開始是空鏈隊,首尾指針分別去指向隊頭結點
 24         front = p;
 25         rear = p;
 26     }
 27     //析構函數,銷毀鏈隊的結點占據的內存
 28     ~Queue()
 29     {
 30         //標記指針
 31         Node *p = front;
 32         //輔助的標記指針,作用是刪除結點
 33         Node *q;
 34         //循環遍歷整個隊列,直到標記指針 p 為 null
 35         while (p != NULL)
 36         {
 37             //比較常見的刪除結點內存的寫法
 38             q = p;
 39             //指向隊列的下一個結點
 40             p = p->next;
 41             //銷毀之
 42             delete q;
 43         }
 44     }
 45     //入隊方法,從尾進入,節點不存在,需要自行創建結點的方法
 46     void push(int e)
 47     {
 48         Node *p = new Node;
 49         p->data = e;
 50         //本結點作為了隊列的尾結點
 51         p->next = NULL;
 52         //然后連接結點到隊尾
 53         rear->next = p;
 54         //最后尾指針指向新的末位結點
 55         rear = p;
 56     }
 57     //入隊方法,尾進入,節點原來就存在的方法,不需要再新建結點和存儲結點的內容
 58     void push(Node *p)
 59     {
 60         //設置此結點為尾結點
 61         p->next = NULL;
 62         //鏈接結點
 63         rear->next = p;
 64         //尾指針指向新的尾結點
 65         rear = p;
 66     }
 67     //求數據元素的最大位數的方法,也就是求出需要分配和收集的次數
 68     int lengthData()
 69     {
 70         int length = 0;//保存數據元素的 最大位數
 71         int n = 0;   //單個數據元素具有的位數
 72         int d;      //用來存儲待比較的數據元素
 73         //指示指針
 74         Node *p = front->next;
 75         //遍歷
 76         while (p != NULL)
 77         {
 78             //取出結點的數據,也就是代比較的數據元素
 79             d = p->data;
 80             //如果 d 為正數,很重要的一個技巧,必須是 d 大於 0 的判斷
 81             while (d > 0)
 82             {
 83                 //數據位數分離算法
 84                 d /= 10;
 85                 //單個數據元素的位數存儲在此
 86                 n++;
 87             }
 88             //沿着鏈隊后移一個元素
 89             p = p->next;
 90             //找出數據元素的最大位數
 91             if (length < n)
 92             {
 93                 length = n;
 94             }
 95             //重新循環往復,n 設置為0
 96             n = 0;
 97         }
 98         //返回最終位數
 99         return length;
100     }
101     //判斷隊列是否為空
102     bool empty()
103     {
104         //隊頭指針和隊尾指針重合,說明空
105         if (front == rear)
106         {
107             return true;
108         }
109         //否則為不空
110         return false;
111     }
112     //清除隊列中的元素
113     void clear()
114     {
115         //直接把頭結點之后的鏈接斷開
116         front->next = NULL;
117         //設置尾指針指向頭結點即可,回到了構造函數初始化的情景
118         rear = front;
119     }
120     //輸出隊列中的元素,傳入引用參數比較好
121     void print(Queue &que)
122     {
123         //第一個結點是頭結點,next 才是第一個存儲元素的結點
124         Node *p = que.front->next;
125         //直到尾結點為止
126         while (p != NULL)
127         {
128             cout << p->data << " ";
129             //遍歷所有結點
130             p = p->next;
131         }
132     }
133     //基數排序過程
134     void RadixSort(Queue& que)
135     {
136         //聲明一個指針數組,該指針數組中存放十個指針,這十個指針需要分別指向十個隊列,這是模擬10個桶,因為是0-9的數字,取值范圍為10
137         Queue *arr[10];
138         //初始化這十個隊列
139         for (int i = 0; i < 10; i++)
140         {
141             //初始化建立頭結點
142             arr[i] = new Queue;
143         }
144         //取得待排序數據元素中的最大位數
145         int maxLen = que.lengthData();
146         //因為是 LSD 方式,從后到前,開始比較關鍵字,然后分配再收集,故開始設置數據分離算法中的除數為 1
147         int d = 1;
148         //將初始隊列中的元素分配到十個隊列中,maxlen 代表了需要分配和收集的次數
149         for(int i = 0; i < maxLen; i++)
150         {
151             Node *p = que.front->next;
152             //輔助指針 q
153             Node *q;
154             //余數為k,則存儲在arr[k]指向的鏈式隊列(桶)中
155             int k;
156             //遍歷原始序列
157             while (p != NULL)
158             {
159                 //重要的技巧,數據分離算法過程,最后勿忘模10,取余數,分離出需要的關鍵字位
160                 k = (p->data / d) % 10;
161                 q = p->next;
162                 //把本結點 p 加入對應的隊列中
163                 arr[k]->push(p);
164                 //指針后移,指向下一個結點
165                 p = q;
166             }
167             //清空原始隊列
168             que.clear();
169             //分配完畢,馬上將十個隊列中的數據收集到原始隊列中
170             for (int i = 0; i < 10; i++)
171             {
172                 if (!arr[i]->empty())
173                 {
174                     //從首節點開始遍歷,不是頭結點開始
175                     Node *p = arr[i]->front->next;
176                     //輔助指針 q
177                     Node *q;
178                     while (p != NULL)
179                     {
180                         q = p->next;
181                         //收集到原始隊列中,這就是為什么每次分配完畢,需要清除原始隊列
182                         que.push(p);
183                         p = q;
184                     }
185                 }
186             }
187             //一趟的分配收集完畢,最后要清空十個隊列
188             for (int i = 0; i < 10; i++)
189             {
190                 arr[i]->clear();
191             }
192             //進行下一趟的分配和收集
193             d *= 10;
194         }
195         //輸出隊列中排好序的元素
196         print(que);
197     }
198 };
199 
200 int main(void)
201 {
202     Queue oldque;
203     int i;
204     
205     cout << "輸入 int 類型的待排序的整數序列:輸入 ctrl+z 結束輸入,再回車即可" << endl;
206     //順序輸入元素
207     while (cin >> i)
208     {
209         oldque.push(i);
210     }
211     //基數排序
212     oldque.RadixSort(oldque);
213 
214     return 0;
215 }

輸入 int 類型的待排序的整數序列:輸入 ctrl+z 結束輸入,再回車即可

505 800 109 930 630 662 663 269 278 287 299 200 830 184 187 528 112 125 589

109 112 125 184 187 200 269 278 287 299 505 528 589 630 662 663 800 830 930

Program ended with exit code: 0

鏈式基數排序的時間復雜度和空間復雜度分析

假設:n —— 記錄數 , d —— 關鍵字數,  rd —— 關鍵字取值范圍(如十進制為10)

分配(每趟):T(n)=O(n) ,每次分配,分配的都是所有的關鍵字,故是 n

收集(每趟):T(n)=O(rd) ,收集的是桶里的數據,也就是關鍵字的取值范圍大小rd,是桶的數目

總的時間復雜度:因為一次完整的排序是分配+收集,也就是 n+rd ,而一共需要的排序趟數,恰恰就是關鍵字的數目 d,故T(n)=O(d(n+rd)) 

空間復雜度:S(n)=2rd 個隊列指針 + n 個指針域空間,因為一個桶本質是一個鏈式隊列,一共 rd 個桶,每個隊列有隊頭和隊尾兩個指針,就是2rd 個隊列指針。又原來的代拍序列是一個單鏈表,那么自然需要 n 個next指針控件。

排序小結

一、時間性能 

平均情況下,記住一個口訣:快些以 n log2 n 的速度歸隊

快=快速排序,些=希爾排序,歸=歸並排序,隊=堆排序

這四種排序算法,時間都是 n log2 n 的,除了這四個之外,其他的排序算法平均時間都為 n^2

記住一個特殊的排序算法:基數排序的時間復雜度

d(n+rd),其中 d 是分配和收集的趟數,n 是原始序列的數目(分配次數),rd 是桶的個數,也就是關鍵字最大位數(收集次數)

最壞的情況下,這四個中,快速排序為 n^2,其余的和平均時間相同,還是 n log2 n

二、空間復雜度

快速排序是 log2 n

歸並排序是 n

基數排序是 2rd

其余的都是1

又有口訣:直接插的好,為 n,起泡起的好為 n

三、穩定性

心情不穩定,快些選一堆好友來陪我

快=快速排序

些=希爾排序

選=簡單選擇排序

堆=堆排序

其余的排序算法都穩定

四、一趟排序,保證一個元素為最終位置的有兩類排序算法:交換類(冒泡和快速)排序和選擇類排序(簡單和堆)

五、元素比較次數和原始序列無關的算法:簡單選擇排序,折半插入排序

六、排序趟數和原序列有關的算法:交換類,其余類無關

七、借助於比較進行排序的算法,在最壞的時候,最好的時間復雜度為 n log2 n

八、堆排序和簡單選擇排序的時間復雜度和初始序列無關

注意,這些東西,不必要必須死記硬背,明白每個算法的類別和實現原理,在理解的基礎上,記憶,在腦海里推導演示排序的過程,幫助記憶。

 

歡迎關注

 

dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM