[排序算法] 11. 基數排序詳解及採用一維數組實現優化(分配排序、算法優化、複雜度分析)

1. 基本思想

基數排序(radix sort)可以看作桶排序的擴展,它是一種多關鍵字排序算法。 如果記錄安照多個關鍵字排序,則依次按照這些關鍵字進行排序。例如撲克牌排序,撲克牌由數字面直和花色兩個關鍵字組成,可以先按照面值(2, 3,.,. 10, J,Q, K, A)排序,再按照花色排序。如果記錄按照一個數值型的關鍵字排序,可以把該關鍵字看作是由 d 位組成的多關鍵字排序,每一位的值取值範圍爲 [0,r), 其中 r 稱爲基數。例如,十進制數 268 由 3 位數組成,每一位的取值範圍爲 [0,10), 十進制數的基數 r 爲 10,同樣,二進制數的基數爲 2,英文字母的基數爲 26。 以下以十進制數的基數排序爲例。

基數排序與桶排序很類似,主要算法步驟如下:

  1. 求出待排序序列中最大關鍵字的位數 d,然後從低位到高位進行基數排序
  2. 按個位將關鍵字依次分配到桶中,然後將每個桶中的數據依次收集起來
  3. 按十位將關鍵字依次分配到桶中,然後將每個桶中的數據依次收集起來
  4. 依次下去, 直到 d 位處理完畢,得到一個有序的序列

舉個例子:假如有 10 個學生的成績,(68,75,54,70,83,48,80,12,75*,92)
,對該成績序列進行桶排序。需要進行以下步驟:

  1. 確定關鍵字
    待排序序列中最大關鍵字 92 爲兩位數,只需要兩趟基數排序即可
  2. 分配
    首先按照個位數,劃分爲 10 個桶 (0~9),將學生成績依次放入桶中,如下圖所示:
    在這裏插入圖片描述
  3. 收集
    將每個桶內的記錄依次收集起來,得到一個序列(70,80,12,92,83,54,75,75*,68,48)
  4. 分配
    再按照十位數,劃分爲10個桶(0~9), 將學生成績依次放入桶中,如下圖所示:
    在這裏插入圖片描述
  5. 收集
    將每個桶內的記錄依次收集起來,得到一個序列(12,48,54,68,70,75,75*,80,83,92)。待排序數據都是兩位數,只有兩個關鍵字,排序完畢,得到一個有序序列。

在此着重強調一個概念,依次分配和收集時爲什麼要“依次”放入和收集?如果不是“依次”會怎麼樣?

回答這個問題,依舊舉個例子:例如對(82,62,65,85)進行基數排序,首先按照個位劃分:在這裏插入圖片描述
收集桶中的數據,(62,82,85,65),再按照十位劃分到 6 號和 8 號桶中:
在這裏插入圖片描述
收集桶中的數據,(65,62,85,82),排序結束並不是一個有序的序列,爲什麼?

  • 以 62、65 爲例,首先按照個位劃分,62 在 2 號桶,65 在 5 號桶,2 號桶在 5 號桶的前面,那麼相同十位的情況下桶間是有序的,即2 號桶的元素肯定是小於 5 號桶的,那麼收集的時候就需要依次進行收集,不能隨意更改收集的方式。

如果不是按順序依次進行分配和收集則無法實現排序結果的正確性。那麼如何保證依次分配和收集呢?

  • 一個非常簡單的方法就是隊列,先進先出,依次進行。因此可以採用隊列保持桶中數據的進出順序,保證排序結果的正確性。也就是說,每一個桶內使用一個隊列存儲數據,可以使用順序隊列或鏈式隊列。

桶中的多個數據元素可以採用二維數組、鏈式存儲的方式都可以,主要保證“依次”屬性即可。下面着重學習一種一位數組的方式進行處理的做法及算法思想:

針對序列:(68,75,54,70,83,48,80,12,75*,92) 進行基數排序,首先按照個位數,,劃分 10 個桶(0~9),將學生成績依次放入桶中,個位是 0 的放入 0 號桶,個位是 2 的放入 2 號桶,等等。如圖所示:
在這裏插入圖片描述
重點:建立輔助數組爲–計數器數組,如圖所示。個位爲0的有兩個,count[0]=2
在這裏插入圖片描述
將計數器數組累加,從下標 1 開始,累加前一項,count[j] += count[j - 1],如圖所示:
在這裏插入圖片描述
累加的效果相當於分配存儲區間,例如,count[8]=10, 那麼 8 號桶的兩個數分配存儲空間下標爲 9,8; count[5]=8, 那麼 5 號桶的兩個數分配存儲空間下標爲7,6,因爲下標從 0 開始。如圖所示:
在這裏插入圖片描述
利用 count[] 數組,將桶中的數據收集到輔助數組 temp 中。序列 從後向前 處理,(68,75,54,70,83,48,80,12,75* 92),相當於一種 映射關係

  • 92 在 2 號桶,count[2]=4--count[2]=3, 將 92 存入temp[3],依次處理完畢後有:

在這裏插入圖片描述
temp[] 數組中的數據,按照十位數,劃分爲 10 個桶(0~9)
在這裏插入圖片描述
計數器數組,如圖所示。十位爲 7 的有 3 個,count[7]=3
在這裏插入圖片描述
將計數器數組累加,從下標 1 開始,累加前一項,count[j]+=countfj-1], 如圖所示。
在這裏插入圖片描述
利用 count[] 數組,將桶中的數據收集到輔助數組temp 中。序列從後向前處理,(70,80,12,92,83,54,75,75*,68,48)

  • 48 在 4 號桶,count[4]=2--count[4]=1, 將 48 存入temp[1]
    在這裏插入圖片描述

將排好序的輔助數組 temp[] 放回原數組即可。排序結果如圖所示。
在這裏插入圖片描述

2. 代碼實現

2.1 動態二維數組實現

// 基數排序(遞增)
int Maxbit(int array[], int size) {		// 求待排序序列最大元素位數
	int maxvalue = array[0], digits = 0;	// 初始化最大元素爲array[0],最大位數爲0
	for (int i = 1; i < size; i++) {	// 找到序列中最大元素
		if (array[i] > maxvalue)
			maxvalue = array[i];
	}
	while (maxvalue != 0) {		// 分解得到最大元素的位數
		digits++;
		maxvalue /= 10;
	}
	return digits;
}

int Bitnumber(int x, int bit) {		// 求x第bit位上的數字,例如238第2位上的數字爲3
	int temp = 1;
	for (int i = 1; i < bit; i++) {
		temp *= 10;
	}
	return (x / temp) % 10;
}

// 基數排序(遞增)
void RadixSort(int array[], int size) {
	int i, j, k, bit, maxbit;
	maxbit = Maxbit(array, size);	// 求最大元素位數
	cout << "最大元素位數爲:" << maxbit << "位 " << endl;
	int **B = new int *[10];	// 分配二維動態數組
	for (i = 0; i < 10; i++)
		B[i] = new int[size + 1];	// 每個桶都是size+1個空間,其中每個桶的第一個位置即B[0]第0位存放元素個數
	for (i = 0; i < 10; i++)
		B[i][0] = 0;	//統計第i個桶的元素個數
	// 從個位到高位,對不同的位數進行桶排序
	for (bit = 1; bit <= maxbit; bit++) {
		for (j = 0; j < size; j++) {	// 分配 
			int num = Bitnumber(array[j], bit);	// 取array[j]第bit位上的數字
			int index = ++B[num][0];
			B[num][index] = array[j];
		}
		for (i = 0, j = 0; i < 10; i++) {	// 收集
			for (k = 1; k <= B[i][0]; k++)
				array[j++] = B[i][k];
			B[i][0] = 0;	// 收集後元素個數置零
		}
	}
	for (int i = 0; i < 10; i++)
		delete[]B[i];
	delete B;
}

測試數據:int array[] = { 3, 9, 1, 4, 2, 8, 2, 7, 5, 3, 6, 11, 9, 4, 2, 5, 0, 6 };
在這裏插入圖片描述

2.2 一維數組實現

// 基數排序(遞增)
// 一維數組實現
const int maxn = 1000;
int a[maxn], size;
int maxbit(int array[], int size) { //輔助函數,求數據的最大位數
	int d = 1;//統計最大的位數
	int p = 10;
	for (int i = 0; i < size; ++i) {
		while (array[i] >= p) {
			p *= 10;
			++d;
		}
	}
	return d;
}

// 基數排序(遞增)
// 一維數組實現
void radixsort(int array[], int size) { 
	int d = maxbit(array, size); // 求最大位數 
	int *tmp = new int[size]; // 輔助數組 
	int *count = new int[10]; // 計數器
	int i, j, k;
	int radix = 1;
	for (i = 1; i <= d; i++) { // 進行d次排序
		for (j = 0; j < 10; ++j) {
			count[j] = 0; // 每次分配前清空計數器
		}
		for (j = 0; j < size; ++j) {
			k = (array[j] / radix) % 10; // 取出個位數,然後是十位數,... 
			count[k]++;  // 統計每個桶中的記錄數
		}
		for (j = 1; j < 10; ++j) {
			count[j] += count[j - 1]; // 將tmp中的位置依次分配給每個桶
		}
		for (j = size - 1; j >= 0; --j) { //將所有桶中記錄依次收集到tmp中
			k = (array[j] / radix) % 10;
			tmp[--count[k]] = array[j];
		}
		for (j = 0; j < size; j++) {	// 將臨時數組的內容複製到array中
			array[j] = tmp[j];
		}
		cout << "第" << i << "次排序結果:" << endl;
		for (int i = 0; i < size; ++i)
			cout << array[i] << "   ";
		cout << endl;
		radix = radix * 10;
	}
	delete[]tmp;
	delete[]count;
}

3. 性能分析

3.1 鏈式隊列性能分析

時間複雜度

基數排序需要進行 d 趟排序,每一趟排序包含分配和收集兩個操作,分配需要O(n)O(n)時間,收集操作如果使用順序隊列也需要O(n)O(n)時間,如果使用鏈式隊列則只需要
r 個鏈隊首展相連即可,需要O(r)O(r)時間,總的時間複雜度爲O(d(n+r))O(d(n+r))

空間複雜度

  • 如果使用順序隊列,需要 r 個大小爲 n 的隊列,空間複雜度爲O(m)O(m)。如果使用鏈式隊列,則需要額外的指針域,空間複雜度爲O(n+r)O(n+r)

排序穩定性

  • 穩定,基數排序時按關鍵字出現的順序依次進行的

3.2 一維數組實現

時間複雜度

基數排序需要進行 d 趟排序,每一趟排序包含分配和收集兩個操作,分配需要O(n)O(n)時間,收集操作使用一維數組需要O(n)O(n)時間,總的時間複雜度爲O(d×n)O(d{\times}n)

空間複雜度

  • 使用計數數組 count 的大小爲基數 r,輔助數組 temp 的大小爲 n,空間複雜度爲 O(n+r)O(n+r)

排序穩定性

  • 穩定,基數排序時按關鍵字出現的順序依次進行的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章