计数排序、基数排序和桶排序
计数排序
不同于基于比较和交换的排序,计数排序是基于统计词频的排序,计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中,这就要求了待排元素必须是有确定范围的整数。
计数排序的过程如下📺
计数排序步骤:
- 确定数组元素的范围,即为 最大值和最小值
- 开辟对应范围长度的数组
countArr[]
,遍历统计待排元素设为num
- 对于每次出现的
num
,将countArr[num]++
- 此时按序按次输出
countArr[]
,即排好序
桶排序
桶排序如其名,工作原理是 将待排元素分到有限数量的桶里(桶的划分是有序的),在每个桶内进行排序,最后按顺序输出得到有序序列。
示例如下:
步骤如下:
- 设置一个定量的数组当作空桶
- 遍历输入数据,并且把数据一个一个放到对应的桶里去
- 对每个不是空的桶进行排序
- 从不是空的桶里把排好序的数据拼接起来
基数排序 🔥🔥🔥
概述与演示
基数排序是基于 关键字各位 的大小来进行排序的,有两种方法:
最高位优先 MSD
:按关键字权重 递减 依次 逐层 划分成若干更小的子序列,最后将所有子序列依次连接成一个有序序列最低位优先 LSD
:按关键字权重 递增 依次进行排序
基数排序演示📺 LSD
传统的算法步骤
-
设待排元素 基数 为
r
,例如十进制数 1024 基数为 10,则分配r
个桶子 -
以
LSD
为例,假设待排序列为:73, 22, 93, 43, 55, 14, 28, 65, 39, 81
-
基数为10,桶号为
0~9
,,从最低位优先,首先根据 个位 数值,分配到对应桶子中:0 1 2 3 4 5 6 7 8 9 81 22 73、93、43 14 55、65 28 39 -
将桶子中数值重新组成序列:
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
-
按 十位 数值,分配到对应桶子中:
0 1 2 3 4 5 6 7 8 9 14 22、28 39 43 55 65 73 81 93 -
将桶中数值重新组成序列:
14, 22, 28, 39, 43, 55, 65, 73, 81, 93
,序列已有序
传统步骤算法分析
- 空间复杂度:使用额外辅助存储空间为
radix
,且可重复使用,空间复杂度O(N)
- 时间复杂度:由步骤可知,排序共需进行
digist
(即最大数字的 位数) 趟分配到桶中 和 从桶中收集再组成,分配需要O(N)
,收集需要O(radix)
,所以总时间复杂度为:O(digist * (N + radix))
,且时间复杂度稳定 - 稳定性:✔️ 按位分配过程是有序进行的,收集过程同样有序,这决定了整体排序是稳定的
优化的算法步骤
还是以 LSD
, 待排序列为:🔰 73, 22, 93, 43, 55, 14, 28, 65, 39, 81
🔰 为例:
-
假设待排序列最大值有
digit
位,例中 93 有 2 位,那么digist
决定了 分配和收集(入桶和出桶)的 次数 -
基数
radix
依然是 10,表示 十进制 -
先对待排元素进行 计数排序,令计数数组为
count[]
,则在第一轮(个位),count[]
如下:0 1 2 3 4 5 6 7 8 9 0 1 1 3 1 2 0 0 1 1 表示含义:在当前位
d
位(此时为个位,d = 1
)值是count[j]
的元素有多少个比如:个位是 3 的数字有 3 个
73、93、43
;个位是 5 的数字有 2 个55、65
-
对
count[]
做如下累加count[j] += count[j - 1]
变形:0 1 2 3 4 5 6 7 8 9 0 1 2 5 6 8 8 8 9 10 表示含义:在当前位
d
位(此时为个位,d = 1
)值小于或等于 <=
count[j]
的元素有多少个比如:个位值小于等于 3 的数字有 5个
81、22、73、93、43
注意
💥 :先把 计数排序统计词频得到count[]
操作 当作是 分配操作 -
构建与待排序列等规模的辅助数组
bucket[]
, 此时 从右往左 遍历待排序列:- 倒数第一个数 81,个位为 1,
count[1]
只有 1 个,按个位比较是最小的,若收集
的话 ,应该排在最前面,即buctet[1 - 1] = 81
,然后再count[1] --
(因为相当于已经收集了一个元素) - 倒数第二个数 39,个位为 9,
count[9]
有 10 个,说明 39 按个位比较排在第 10 位,若收集的话,应该是bucket[10 - 1] = 39
,然后再count[9] --
- 倒数第三个数 65,个位为 5,
count[5]
有 8 个,说明 65 按个位比较排在 第 8 位,若收集的话,应该是bucket[8 - 1] = 65
,然后再count[5] --
- ………………
- 倒数第一个数 81,个位为 1,
-
上步操作执行完,得到的
bucket[]
已经是按照个位排序是有序的:81, 22, 73, 93, 43, 14, 55, 65, 28, 39
-
将
bucket[]
元素覆盖到待排序列, 再对 十位d = 2
进行上述操作,得到的bucket[]
就是最终结果了
步骤分析
通过以上优化的步骤,我们可以得知,通过基数排序得到词频 count[]
,和从右往左遍历保证了 先入桶的元素先出桶,即保证了分配和收集的有序性,count[j]++
可作为 入桶操作,count[j]--
可作为出桶操作
优化基数排序算法实现代码
// 求数组中最大元素的位数
int maxBits(int arr[],int size) {
int max = *max_element(arr, arr + size);
int bits = 0;
while (max != 0) {
bits++;
max /= 10;
}
return bits;
}
// 求数字num的某位d上的值,超出最大位数返回0
int getDigist(int num, int d) {
return (num / (int)pow(10, d - 1)) % 10;
}
// 基数排序 !!!
void radixSort(int arr[], int size) {
int *bucket = new int[size]; // 辅助数组,暂存每轮排完序的序列
int digist = maxBits(arr, size);
int i = 0, j = 0;
for (int d = 1; d <= digist; d++) { //共有digist轮分配和收集
int count[10]{ 0 }; // count[10] 计数排序数组 0~9,必须保证每次入桶前 count[]计数是0
// 分配操作1--计数排序
for (i = 0; i< size; i++) {
j = getDigist(arr[i], d); //令 j 为 arr[i] 的第 d 位的值,取值0~9
count[j]++;
}
//分配操作2--累加前缀
for (i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 收集操作,出桶
for (i = size - 1; i >= 0; i--) {
j = getDigist(arr[i], d);
bucket[count[j] - 1] = arr[i]; // 存储到bucket[]中
count[j]--; // 出桶
}
//覆盖
for (i = 0; i < size; i++) {
arr[i] = bucket[i];
}
}
delete[] bucket;
}
所有非基于比较的排序,都要根据数据状况来排序,所以应用场景比较狭隘