排序算法(8) -- 桶排序、计数排序和基数排序

桶排序、计数排序和基数排序这三种算法的时间复杂度都为 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(mklogk)=O(nlogmn)。当桶的个数接近数据个数时, 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] 数组中的每个值就都表示小于等于它的值的个数了。

img

  • 接下来,我们从后到前依次扫描数组 A[8]。当扫描到 3 时,我们取出 C[3] 的值 7,说明小于等于 3 的个数为 7 个,那么 3 就应该放在数组 R[8] 的第 7 个位置,也就是下标为 6 的地方。当我们再次遇到 3 的时候,这时候小于等于 3 的元素个数就少了一个,也就是我们要把 C[3] 相应地减去 1 。
  • 之所以要从后到前依次扫描数组,是因为这样之前相同的元素就仍然会保持相同的顺序,可以保证排序算法的稳定性
  • 当我们扫描完整个数组 A[8] 时,数组 R[8] 中的数据也就从小到大排好序了,其详细过程可参考下图。

img

  • 代码实现
// 假设数组中存储的都是非负整数
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}。

接下来,可以对十位数、百位数也按照这种方法进行排序,最后就能得到排序完成的序列。

动态效果示意图:

排序(8):基数排序

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");
}

运行结果如下图所示:

排序(8):基数排序

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 算法稳定性

在基数排序过程中,每次都是将当前位数上相同数值的元素统一“装桶”,并不需要交换位置。所以基数排序是稳定的算法。

本站整理自:

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值