前言
基數排序的實現方法包括最高位優先法(Most Significant Digit first),簡稱MSD法;也包括最低位優先法(Least Significant Digit first),簡稱LSD法。本文介紹的是LSD法實現的基數排序,其中包含了計數排序的思想,如果不懂的同學請點擊鏈接先行學習計數排序。
概述
基數排序(radix sort)屬於“分配式排序”(distribution sort),又稱“桶子法”(bucket sort),是一種非比較線性時間排序算法。顧名思義,它是透過鍵值的部分訊息,將要排序的元素分配至某些“桶”內,藉以達到排序的作用。
在這我要給大家擴展一下,其實常用的三種非比較線性時間算法:計數排序,基數排序,桶排序都利用了桶的概念,都通過元素自身的值,計算哈希值,根據哈希值將元素存儲到相應的桶內,以此完成元素的排序,但是這三種算法對桶的使用方法上有明顯差異:
(1)上一回我們提到的計數排序,它是根據當前元素與最小值的差值作爲哈希值來選擇桶的,也就是說,每個桶只存儲單一鍵值。
(2)基數排序:就如接下來我們將要提到的,基數排序是根據鍵值的每一位上的數字來分配桶。(分配的過程由低位到高位,進行多次分配)
(3)桶排序:下一章我們將要學習的桶排序中每個桶存儲一定範圍的數值。
算法過程(實例分析)
假設原序列A中有一串數字如下所示:
vector<int> A = {1755, 27535, 5415, 22200, 18091, 18893, 13482, 4025, 586, 23372};
首先根據個位數的數值,在遍歷A過程中將它們分配至編號0到9號桶中:
0 22200
1 18091
2 13482 23372
3 18893
4
5 1755 27535 5415 4025
6 586
7
8
9
第二步:
接下來將這些桶子中的數值重新排列,按照0號桶優先的次序重新串接起來,成爲以下的數列:
A = {22200, 18091, 13482, 23372, 18893, 1755, 27535, 5415, 4025, 586};
接着按照十位數的數值,在遍歷A過程中將它們分配至編號0到9號桶中:
0 22200
1 5415
2 4025
3 27535
4
5 1755
6
7 23372
8 13482 586
9 18091 18893
第三步:
接下來將這些桶子中的數值重新排列,按照0號桶優先的次序重新串接起來,成爲以下的數列:
A = {22200, 5415, 4025, 27535, 1755, 23372, 13482, 586, 18091, 18893};
接着按照百位數的數值,在遍歷A過程中將它們分配至編號0到9號桶中:
0 4025 18091
1
2 22200
3 23372
4 5415 13482
5 27535 586
6
7 1755
8 18893
9
第四步:
接下來將這些桶子中的數值重新排列,按照0號桶優先的次序重新串接起來,成爲以下的數列:
A = {4025, 18091, 22200, 23372, 5415, 13482, 27535, 586, 1755, 18893};
接着按照千位數的數值,在遍歷A過程中將它們分配至編號0到9號桶中:
0 586
1 1755
2 22200
3 23372 13482
4 4025
5 5415
6
7 27535
8 18091 18893
9
第五步:
接下來將這些桶子中的數值重新排列,按照0號桶優先的次序重新串接起來,成爲以下的數列:
A = {586, 1755, 22200, 23372, 13482, 4025, 5415, 27535, 18091, 18893 };
接着按照萬位數的數值,在遍歷A過程中將它們分配至編號0到9號桶中:
0 586 1755 4025 5415
1 13482 18091 18893
2 22200 23372 27535
3
4
5
6
7
8
9
所以整個序列已經排序完畢了,結果如下
A = {586, 1755, 4025, 5415, 13482, 18091, 18893, 22200, 23372, 27535};
程序演示
/*返回當前序列中的最大位數*/
int max_bit(vector<int>& v)
{
int max = v[0];
for (size_t i = 1; i < v.size(); ++i)
{
if (v[i] > max)
max = v[i];
}
int p = 10;
int d = 1; //記錄當前的位數
while (max >= p)
{
max /= 10;
++d;
}
return d;
}
/*基數排序算法*/
void radix_sort(vector<int>& v)
{
int d = max_bit(v);
int radix = 1;
vector<int> temp(v.size());
vector<int> times(10);
for (size_t i = 0; i < d; ++i)
{
/*將times數組中的值清零*/
for (size_t j = 0; j < times.size(); ++j)
times[j] = 0;
/*接下來的過程類似計數排序*/
/*首先通過輔助數組times記錄當前位上數值爲0-9的數字分別有多少*/
for (size_t j = 0; j < v.size(); ++j)
++times[v[j] / radix % 10];
/*改變times中數值的意義*/
for (size_t j = 1; j < times.size(); ++j)
times[j] += times[j - 1];
/*倒序遍歷原序列v,將排序結果放到臨時數組temp中*/
for (int j = v.size() - 1; j >= 0; --j)
{
int k = v[j] / radix % 10; //取得當前位的數字
temp[times[k] - 1] = v[j];
--times[k];
}
/*將臨時數組temp中的值重新拷貝到v中*/
v.assign(temp.begin(), temp.end());
/*將基數乘以10*/
radix *= 10;
}
}
大家仔細看就會發現,其實最核心的部分用的就是類似於計數排序的思想,所以學習算法,“基礎”也是很重要的,有時候你瞭解並熟悉了一部分算法,可能別的算法對你來說也會更容易理解。
算法複雜度
基數排序的時間複雜度爲 O(d(n + k)),其中n爲原序列的元素個數,d爲最大值的最高位數,k爲同一位中數值的取值範圍(通常選十進制就是k = 10)。其中,一趟分配時間複雜度爲O(n),一趟收集時間複雜度爲O(k),共進行d趟分配和收集。
空間複雜度:O(n + k),用於拷貝的臨時數組和包含k個值的輔助數組。
穩定性:基數排序屬於穩定算法,因爲當比較某一位數字的大小時遇到該位上數字大小一致的情況,基數排序總會將靠前的元素排在前面。