桶排序、计数排序和基数排序这三种算法的时间复杂度都为 O ( n ) O(n) O(n),因此,它们也被叫作线性排序(Linear Sort)。之所以能做到线性,是因为这三个算法是非基于比较的排序算法,都不涉及元素之间的比较操作。
1. 桶排序(Bucket Sort)
1.1. 桶排序原理
- 桶排序,顾名思义,要用到“桶”。核心思想是将要排序的数据分到几个有序的桶里,每个桶的数据再单独进行排序。桶内排完序后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
1.2. 桶排序的时间复杂度分析
- 如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶内就有 k = n m k = \frac{n}{m} k=mn 个元素。对每个桶内的数据进行快速排序,时间复杂度为 O ( k l o g k ) O(klogk) O(klogk)。m 个桶排序时间复杂度就为 O ( m ∗ k l o g k ) = O ( n ∗ l o g n m ) O(m * klogk) = O(n * log\frac{n}{m}) O(m∗klogk)=O(n∗logmn)。当桶的个数接近数据个数时, O ( l o g n m ) O(log\frac{n}{m}) O(logmn) 就是一个非常小的数,这个时候通排序的时间复杂度接近于 O ( n ) O(n) O(n)。
1.3. 桶排序的适用条件
桶排序看起来很优秀,但事实上,桶排序对排序数据的要求是非常苛刻的。
- 首先,要排序的数据需要很容易就能划分为 m 个桶,并且桶与桶之间有着天然的大小顺序。
- 其次,数据在各个桶之间的分布是比较均匀的。
- 桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据比较大而内存有限,无法将数据全部加载到内存中去。
1.4. 一个桶排序的实例
假如我们有 10 GB 的订单数据需要按照金额进行排序,但内存只有几百 MB ,这时候该怎么办呢?
- 我们先扫描一遍文件,确定订单金额的数据范围。
- 如果扫描后发现订单金额处于 1 万元到 10 万元之间,我们将所有订单按照金额划分到 100 个桶内,第一个桶数据范围为[1, 1000],第二个桶数据范围为[1001, 2000]…,每个桶对应一个文件,同时将文件按照金额范围的大小顺序编号命名(如00、01、02…99)。
- 如果订单金额分布均匀,则每个文件包含大约 100 MB 的数据,我们可以将每个小文件读入到内存中,进行快速排序。然后,再按顺序从各个小文件读取数据,写入到另外一个文件,即是排序好的数据了。
- 如果订单分布不均,某一范围内数据特别多无法一次读入内存,则可以继续对此区间再进行划分,直到所有的文件都可以读入内存为止。
2. 计数排序(Counting Sort)
2.1. 计数排序算法实现
- 计数排序可以看作是桶排序的一种特殊情况。当要排序的数据所处的范围并不大时,比如最大值为 K,这时候,我们可以把数据分为 K 个桶,每个桶内的数据都是相同的,省掉了桶内排序的时间。
- 假设高考分数的范围为 [0, 750],我们可以将考生的分数划分到 751 个桶内,然后再依次从桶内取出数据,就可以实现对考生成绩的排序了。因为只涉及到扫描遍历操作,因此时间复杂度为 O ( n ) O(n) O(n)。
但这个排序算法为什么叫计数排序呢,这是由计数排序算法的实现方法来决定的,我们来看一个简单的例子。
- 假设有 8 个考生,他们的分数范围为 [0, 5]。这 8 个考生的成绩我们放在一个数组中,A[8] = {2, 5, 3, 0, 2, 3, 0, 3}。
- 我们用大小为 6 的数组代表 6 个桶来统计考生的成绩分布情况,其中下标表示考生的分数,数组内的值表示这个分数的考生个数。我们遍历一遍数组后,就可以得到 C[6] = {2, 0, 2, 3, 0, 1,},得 0 分的共有 2 人,得 3 分的共有 3 人。
- 如下所示,成绩为 3 的考生共有 3 个,小于 3 分的考生共有 4 个,所以在排好序的数据 R[8] 中,3 的位置应该为 4,5 和 6 。
- 而我们怎么得到这个位置呢?只需要对 C[6] 数组按顺序累计求和即可,这时候,C[6] 数组中的每个值就都表示小于等于它的值的个数了。
- 接下来,我们从后到前依次扫描数组 A[8]。当扫描到 3 时,我们取出 C[3] 的值 7,说明小于等于 3 的个数为 7 个,那么 3 就应该放在数组 R[8] 的第 7 个位置,也就是下标为 6 的地方。当我们再次遇到 3 的时候,这时候小于等于 3 的元素个数就少了一个,也就是我们要把 C[3] 相应地减去 1 。
- 之所以要从后到前依次扫描数组,是因为这样之前相同的元素就仍然会保持相同的顺序,可以保证排序算法的稳定性。
- 当我们扫描完整个数组 A[8] 时,数组 R[8] 中的数据也就从小到大排好序了,其详细过程可参考下图。
- 代码实现
// 假设数组中存储的都是非负整数
void Counting_Sort(int data[], int n)
{
if (n <= 1)
{
return;
}
// 寻找数组的最大值
int max = data[0];
for (int i = 1; i < n; i++)
{
if (data[i] > max)
{
max = data[i];
}
}
// 定义一个计数数组, 统计每个元素的个数
int c[max+1] = {0};
for (int i = 0; i < n; i++)
{
c[data[i]]++;
}
// 对计数数组累计求和
for (int i = 1; i <= max; i++)
{
c[i] = c[i] + c[i-1];
}
// 临时存放排好序的数据
int r[n] = {0};
// 倒序遍历数组,将元素放入正确的位置
for (int i = n-1; i >= 0; i--)
{
r[c[data[i]] - 1] = data[i];
c[data[i]]--;
}
for (int i = 0; i < n; i++)
{
data[i] = r[i];
}
}
通过上面分析不难得知, c[data[i]--]
即为结果的对应索引值,这也是为什么有 r[c[data[i]] - 1] = data[i];
然后 c[data[i]]--;
则同理是为了将排过序的值索引去掉。
2.2. 计数排序的适用范围
- 计数排序只适用于数据范围不大的场景中,如果数据范围 K 比排序的数据 n 大很多,就不适合用计数排序了。
- 计数排序能给非负整数排序,如果数据是其他类型的,需要将其在不改变相对大小的情况下,转化为非负整数。比如数据有一位小数,我们需要将数据都乘以 10;数据范围为 [-1000, 1000],我们需要对每个数据加 1000。
3. 基数排序(Radix Sort)
假设要对 10 万个手机号码进行排序,显然桶排序和计数排序都不太适合,那怎样才能做到时间复杂度为 O(n) 呢?
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
3.1. 基数排序原理
- 手机号码有这样的规律,假设要比较两个手机号码 a, b 的大小,如果在前面几位中,a 手机号码已经比 b大了,那后面几位就不用看了。
- 借助 稳定排序算法,我们可以这么实现。从手机号码的最后一位开始,分别按照每一位的数字对手机号码进行排序,依次往前进行,经过 11 次排序之后,手机号码就都有序了。
- 下面是一个字符串的排序实例,和手机号码类似。
- 根据每一位的排序,我们可以用刚才的桶排序或者计数排序来实现,它们的时间复杂度可以做到 O(n)。如果排序的数据有 K位,则总的时间复杂度为 O(K * n),当 K 不大时,基数排序的时间复杂度就近似为 O(n)。
- 有时候,要排序的数据并不都是等长的,比如我们要对英文单词进行排序。这时候,我们可以把所有单词都补足到相同长度,位数不够的在后面补 ’0‘,所有字母的 ASCII 码都大于 ‘0’,因此不会影响原有的大小顺序。
- 基数排序需要数据可以分割出独立的位出来,而且位之间有递进的关系。除此之外,每一位的数据范围都不能太大,要可以用线性排序算法来进行排序。
3.2 算法思想
基本思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
算法步骤:
- 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
- 从最低位开始,依次进行一次排序。
- 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序的方式可以采用 LSD(Least significant digital)或 MSD(Most significant digital),LSD 的排序方式由键值的最右边开始,而 MSD 则相反,由键值的最左边开始。
不妨通过一个具体的实例来展示一下基数排序是如何进行的。 设有一个初始序列为: R {50, 123, 543, 187, 49, 30, 0, 2, 11, 100}。
我们知道,任何一个阿拉伯数,它的各个位数上的基数都是以 0~9 来表示的,所以我们不妨把 0~9 视为 10 个桶。
我们先根据序列的个位数的数字来进行分类,将其分到指定的桶中。例如:R[0] = 50,个位数上是 0,将这个数存入编号为 0 的桶中。
分类后,我们在从各个桶中,将这些数按照从编号 0 到编号 9 的顺序依次将所有数取出来。这时,得到的序列就是个位数上呈递增趋势的序列。
按照个位数排序: {50, 30, 0, 100, 11, 2, 123, 543, 187, 49}。
接下来,可以对十位数、百位数也按照这种方法进行排序,最后就能得到排序完成的序列。
动态效果示意图:
3.3 代码
C++:
#include <iostream>
#include <vector>
using namespace std;
// 求出数组中最大数的位数的函数
int MaxBit(vector<int> input){
// 数组最大值
int max_data = input[0];
for (int i = 1; i < input.size(); i++){
if (input[i] > max_data){
max_data = input[i];
}
}
// 数组最大值的位数
int bits_num = 0;
while (max_data){
bits_num++;
max_data /= 10;
}
return bits_num;
}
// 取数xxx上的第d位数字
int digit(int num, int d){
int pow = 1;
while (--d > 0){
pow *= 10;
}
return num / pow % 10;
}
// 基数排序
vector<int> RadixSort(vector<int> input, int n){
// 临时数组,用来存放排序过程中的数据
vector<int> bucket(n);
// 位记数器,从第0个元素到第9个元素依次用来记录当前比较位是0的有多少个...是9的有多少个数
vector<int> count(10);
// 从低位往高位循环
for (int d = 1; d <= MaxBit(input); d++){
// 计数器清0
for (int i = 0; i < 10; i++){
count[i] = 0;
}
// 统计各个桶中的个数
for (int i = 0; i < n; i++){
count[digit(input[i],d)]++;
}
/*
* 比如某次经过上面统计后结果为:[0, 2, 3, 3, 0, 0, 0, 0, 0, 0]则经过下面计算后 结果为: [0, 2,
* 5, 8, 8, 8, 8, 8, 8, 8]但实质上只有如下[0, 2, 5, 8, 0, 0, 0, 0, 0, 0]中
* 非零数才用到,因为其他位不存在,它们分别表示如下:2表示比较位为1的元素可以存放在索引为1、0的
* 位置,5表示比较位为2的元素可以存放在4、3、2三个(5-2=3)位置,8表示比较位为3的元素可以存放在
* 7、6、5三个(8-5=3)位置
*/
for (int i = 1; i < 10; i++){
count[i] += count[i - 1];
}
/*
* 注,这里只能从数组后往前循环,因为排序时还需保持以前的已排序好的顺序,不应该打
* 乱原来已排好的序,如果从前往后处理,则会把原来在前面会摆到后面去,因为在处理某个
* 元素的位置时,位记数器是从大到到小(count[digit(arr[i], d)]--)的方式来处
* 理的,即先存放索引大的元素,再存放索引小的元素,所以需从最后一个元素开始处理。
* 如有这样的一个序列[212,213,312],如果按照从第一个元素开始循环的话,经过第一轮
* 后(个位)排序后,得到这样一个序列[312,212,213],第一次好像没什么问题,但问题会
* 从第二轮开始出现,第二轮排序后,会得到[213,212,312],这样个位为3的元素本应该
* 放在最后,但经过第二轮后却排在了前面了,所以出现了问题
*/
for (int i = n - 1; i >= 0; i--){
int k = digit(input[i], d);
bucket[count[k] - 1] = input[i];
count[k]--;
}
// 临时数组复制到 input 中
for (int i = 0; i < n; i++){
input[i] = bucket[i];
}
}
return input;
}
void main(){
int arr[] = { 50, 123, 543, 187, 49, 30, 0, 2, 11, 100 };
vector<int> test(arr, arr + sizeof(arr) / sizeof(arr[0]));
cout << "排序前:";
for (int i = 0; i < test.size(); i++){
cout << test[i] << " ";
}
cout << endl;
vector<int> result = test;
result = RadixSort(result, result.size());
cout << "排序后:";
for (int i = 0; i < result.size(); i++){
cout << result[i] << " ";
}
cout << endl;
system("pause");
}
运行结果如下图所示:
Python:
# -*- coding:utf-8 -*-
def RadixSort(input_list):
'''
函数说明:基数排序(升序)
Author:
www.cuijiahua.com
Parameters:
input_list - 待排序列表
Returns:
sorted_list - 升序排序好的列表
'''
def MaxBit(input_list):
'''
函数说明:求出数组中最大数的位数的函数
Author:
www.cuijiahua.com
Parameters:
input_list - 待排序列表
Returns:
bits-num - 位数
'''
max_data = max(input_list)
bits_num = 0
while max_data:
bits_num += 1
max_data //= 10
return bits_num
def digit(num, d):
'''
函数说明:取数xxx上的第d位数字
Author:
www.cuijiahua.com
Parameters:
num - 待操作的数
d - 第d位的数
Returns:
取数结果
'''
p = 1
while d > 1:
d -= 1
p *= 10
return num // p % 10
if len(input_list) == 0:
return []
sorted_list = input_list
length = len(sorted_list)
bucket = [0] * length
for d in range(1, MaxBit(sorted_list) + 1):
count = [0] * 10
for i in range(0, length):
count[digit(sorted_list[i], d)] += 1
for i in range(1, 10):
count[i] += count[i - 1]
for i in range(0, length)[::-1]:
k = digit(sorted_list[i], d)
bucket[count[k] - 1] = sorted_list[i]
count[k] -= 1
for i in range(0, length):
sorted_list[i] = bucket[i]
return sorted_list
if __name__ == '__main__':
input_list = [50, 123, 543, 187, 49, 30, 0, 2, 11, 100]
print('排序前:', input_list)
sorted_list = RadixSort(input_list)
print('排序后:', sorted_list)
基数排序是在计数排序的基础上引入位结构,将其扩展到可处理任意多位的比较,不再局限到只有一位进制数之内(如0-9),当要比较的位数不同时,通过前面补0或其他方式补充为位数相同再进行计数排序。
4. 算法分析
4.1 基数排序的性能
其中,d 代表数组元素最高为位数,n 代表元素个数。
4.2 时间复杂度
这个时间复杂度比较好计算:count * length;其中 count 为数组元素最高位数,length为元素个数;所以时间复杂度:O(n * d)
4.3 空间复杂度
空间复杂度是使用了两个临时的数组:10 + length;所以空间复杂度:O(n)。
4.4 算法稳定性
在基数排序过程中,每次都是将当前位数上相同数值的元素统一“装桶”,并不需要交换位置。所以基数排序是稳定的算法。
本站整理自:
-
http://www.cnblogs.com/jingmoxukong/p/4311237.html
-
https://www.cnblogs.com/chunguang/p/5892768.html
-
https://61mon.com/index.php/archives/204/
-
https://yq.aliyun.com/articles/11331
-
https://zhuanlan.zhihu.com/p/47227151