排序算法——繼“計數排序”後,我想到了盡善盡美的“基數排序”

前言

基數排序的實現方法包括最高位優先法(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個值的輔助數組。

穩定性:基數排序屬於穩定算法,因爲當比較某一位數字的大小時遇到該位上數字大小一致的情況,基數排序總會將靠前的元素排在前面。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章