前言
基数排序的实现方法包括最高位优先法(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个值的辅助数组。
稳定性:基数排序属于稳定算法,因为当比较某一位数字的大小时遇到该位上数字大小一致的情况,基数排序总会将靠前的元素排在前面。