【数据结构与算法】(17):计数排序和基数排序详解

news2024/9/20 20:31:44

🤡博客主页:Code_文晓

🥰本文专栏:数据结构与算法

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


✨✨💜💛想要学习更多数据结构与算法点击专栏链接查看💛💜✨✨ 


        前面我们学习了许多种类的排序,这次我们学习一种不同思想的排序种类——桶思想排序。桶排序有什么不同吗?如果说前面的排序主要是通过关键字的比较和记录的移动,而桶思想的排序往往并不需要进行关键字的比较。如果大家还不了解桶排序的思想,推荐大家一定要先快速看一下这篇很简单的文章《桶除了能装饭还能排序?》

        这节课我们将学习一下桶思想排序中的计数排序基数排序算法,让我们开始这次的学习旅程吧。


1. 计数排序 

        计数排序是通过计数而不是比较来进行排序的。算法比较简单,适合于待排序记录数量多但排序关键字的范围比较小的整数数字排序情形。  

        计数排序,这种排序算法是利用数组下标来确定元素的正确位置的。 假设数组中有10个整数,取值范围为0~10,要求用最快的速度把这10个整数从小到大进行排序。 可以根据这有限的范围,建立一个长度为11的数组。数组下标从0到10,元素初始值全为0。

        假设数组数据为:{ 9,1,2,7,8,1,3,6,5,3 } 

        下面就开始遍历这个无序的随机数列,每一个整数按照其值对号入座,同时,对应数组下标的元素进行 加1操作 例如第1个整数是9,那么数组下标为9的元素加1

最终,当数列遍历完毕时,数组的状态如下: 

        该数组中每一个下标位置的值代表数列中对应整数出现的次数 直接遍历数组,输出数组元素的下标值,元素的值是几,就输出几次,0不输出。 则顺序输出是:1、1、2、3、3、5、6、7、8、9

        计数排序如果起始数不是从0开始,比如分数排序: 95,94,91,98,99,90,99,93,91,92 数组起始数为90,这样数组前面的位置就浪费了。又或者说,如果遇到要排序的数字是负数呢?

        所以为了解决这个问题,也不要紧,我们确定要排序的整数数字的最大值最小值从而确定出计数数组定义多大合适。当然,在根据计数数组来输出排序结果时,计数数组下标为0的元素代表的应该是待排序关键字中的最小值,而不再是0本身。比如,对-10到10之间的元素排序,定义的计数数组大小应该是21(最大值-最小值+1)。而计数数组下标为0的元素代表的应该是-10。

        有了上述讲解之后,下面就是计数排序的代码实现:

// 计数排序
void CountSort(int* myarray, int length)
{
	// 找出数组的最小值和最大值
	int min = myarray[0], max = myarray[0];
	for (int i = 0; i < length; i++)
	{
		if (myarray[i] < min)
		{
			min = myarray[i];
		}

		if (myarray[i] > max)
		{
			max = myarray[i];
		}
	}

	int range = max - min + 1; // 计数数组的范围
	int* countA = (int*)malloc(sizeof(int) * range); // 计数数组
	memset(countA, 0, sizeof(int) * range); // 初始化计数数组元素都为0

	// 计数
	for (int i = 0; i < length; i++)
	{
		countA[myarray[i] - min]++;
	}

	// 排序	
	int k = 0;
	for (int j = 0; j < range; j++)
	{
		while (countA[j]--)
		{
			myarray[k++] = j + min;
		}
	}

	// 释放计数数组
	free(countA);
}

计数排序算法效率分析 

        计数排序算法的时间复杂度方面,因为要扫描待排序的 length个元素,还需要用到辅助的计数数组来进行计数统计工作,这里用 range代表计数数组的大小,所以计数排序算法的时间复杂度为O(length + range),当然因为range值取值范围比较小,可能远远小于legnth值,所以也可以把计数排序算法的时间复杂度看成是O(legnth)。因为用到了计数数组,所以,空间复杂度是O(range)。

前面曾经强调过,计数排序的适用场合是:

  1. 待排序记录数量多。

  2. 排序关键字的范围比较集中(范围小)。

  3. 整数数字排序。

此时用计数排序可能比其他排序算法要快得多。但如果不满足这样的场合,则要慎用这样的排序,以免造成排序效率过差。


2. 基数排序 

2.1 什么是基数排序 

        以往的排序主要是通过关键字的比较和记录的移动来进行。而基数排序是一种不同以往的排序方式,它并不需要进行关键字的比较。

        基数排序要进行多趟排序,每趟排序都要经历“分配”和“收集”两个步骤,当然,每趟排序也都会基于上一趟排序的成果再次排序。

        有这么一组数字 {516,231,445,323,299,2,18,67,42,102,164,755,687,437} 。现在希望对这组数字进行从小到大的排序。观察一下这组数字,最大的数字也就是3位(个位、十位、百位),所以为了更清晰地说明算法,可以把这组数字都扩展成3位的,比如 {516,231,445,323,299,002,018,067,042,102,164,755,687,437} 。

        将关键字拆分成d组(上面范围每个数字都是3位,所以将要拆分成3组,即d=3),然后按关键字位的权重递增的次序(个位、十位、百位)来做d趟的“分配”和“收集”动作。

        因为“个位”、“十位”、“百位”数字取值都是0~9(10个数字)之间,所以建立10个辅助队列(桶)B0~B9来保存个位、十位、百位信息。

第一趟处理取权重最低的即“个位”进行“分配”和“收集”两个动作,如图1所示。

  • 分配:以“个位”数字进行分配,将指定数字放到辅助队列B0~B9中,比如对于数字516,其个位数字是6,所以放入到B6中。其余数字也是如此处理。对于个位数字重复的,在相应的辅助队列中从上到下依次放置。

  • 收集:依次从B0~B9辅助队列中把相关的数字从上到下、从左到右收集并排列起来。

这样就得到了按“个位”递增排序的数字序列。

第二趟处理取“十位”进行“分配”和“收集”两个动作,第二趟处理会基于第一趟处理的成果进行,如图2所示。

  • 分配:以“十位”数字进行分配,将指定数字放到辅助队列B0~B9中,比如对于数字231,其十位数字是3,所以放入到B3中。其余数字也是如此处理。对于十位数字重复的,在相应的辅助队列中从上到下依次放置。不难发现,在相同的队列中,个位数越小越是在队头位置。

  • 收集:依次从B0~B9辅助队列中把相关的数字从上到下、从左到右收集并排列起来。

这样就得到了按“十位”递增排序的数字序列。对于“十位”数字相同的,“个位”数字按递增排序。

第三趟处理取“百位”进行“分配”和“收集”两个动作,第三趟处理会基于第二趟处理的成果进行,如图3所示。

  • 分配:以“百位”数字进行分配,将指定数字放到辅助队列B0~B9中,比如对于数字002,其百位数字是0,所以放入到B0中。其余数字也是如此处理。对于百位数字重复的,在相应的辅助队列中从上到下依次放置。

  • 收集:依次从B0~B9辅助队列中把相关的数字从上到下、从左到右收集并排列起来。

这样就得到了按“百位”递增排序的数字序列。对于“百位”数字相同的,“十位”数字按递增排序。如果“百位”和“十位”数字都相同,则会按“个位”递增排序。

实现代码如下。

//基数排序
template<typename T>
void RadixSort(T myarray[], int length)
{
	if (length <= 1) //不超过1个元素的数组,没必要排序
		return;

	T* pResult = new T[length]; //新数组,用于保存每趟排序的结果

	//借用C++标准库中的list容器保存必要的信息,当然也可以用自己写的链表来保存数据
	std::list<T *> mylist[10]; //#include <list>  ,注意list中的<>里的数据类型

	//3,意味着分别取得个位、十位、百位 数字
	for (int i = 0; i < 3; ++i) //为简化代码,假设已经知道待排序数字最大不超过3位,所以这里就直接写i < 3了
	{
		//(1)分配
		for (int j = 0; j < length; ++j)
		{
			//根据i值来决定取得某个数字的个位、十位、百位
			int tmpi = i;
			T tmpvalue = myarray[j];
			T lastvalue;   //取得的个位、十位、百位数字保存在这里
			while (tmpi >= 0)
			{
				lastvalue = tmpvalue % 10;
				tmpvalue /= 10;
				tmpi--;
			} //end while

			mylist[lastvalue].push_back(&myarray[j]);  //在list尾部插入元素
		} //end for j

		//(2)收集
		int idx = 0;
		for (int k = 0; k < 10; ++k)
		{
			for (auto iter = mylist[k].begin(); iter != mylist[k].end(); ++iter)
			{
				pResult[idx] = *( * (iter));
				idx++;
			} //end iter
			mylist[k].clear(); //清空mylist,为下次向其中存数据做准备
		} //end for k

		//(3)把数据拷贝回myarray
		for (int m = 0; m < length; ++m)
		{
			myarray[m] = pResult[m];
		}//end for m
	} //end for i

	delete[] pResult;
	return;
}

在main主函数中,代码应该是这样的。

int arr[] = { 516, 231, 445, 323, 299, 2, 18, 67, 42, 102, 164, 755, 687, 437 };
int length = sizeof(arr) / sizeof(arr[0]);   //数组中元素个数
RadixSort(arr, length);//对数组元素进行基数排序
cout <<"基数排序结果为:";
for (int i = 0; i < length; ++i)
{
	cout << arr[i] <<"";
}
cout << endl; //换行

下面是代码的执行结果。

2.2 基数排序算法效率分析

基数排序算法时间复杂度分析:假设算法进行了d趟的分配和收集,每趟分配要扫描待排序的n个元素,所以每一趟分配的时间复杂度是O(n)。此外还需要用到多个辅助队列进行分配完后的数据收集工作,假设用到的是k个辅助队列,所以每趟收集的时间复杂度是O(k)。所以总的时间复杂度是O(d(n+k))。

从代码可以看到,基数排序需要一些辅助空间来保存数据。

比如这段代码行。

T* pResult = new T[length];
std::list<T *> mylist[10];

这段代码用到的队列数组有k(10)个,所以空间复杂度是O(n+k)。此外,基数排序算法是稳定的,你只要结合代码,拿两个相同的数字画一画或稍微想想就可以得出结论。  

2.3 基数排序算法的应用

前面实现的算法代码是针对一系列数字进行从小到大的排序。当然,基数排序还有许多适用场合。

举个例子,某学校有5000名学生,他们的出生日期有详细记录,要求将学生按照年龄从小到大排序。

要完成这个需求,根据学生的出生日期来确定排序次序是非常合适的,可以把每个学生的出生日期拆解为年、月、日三部分。已知学生的出生日期在1990~2010年之间,月份自然是在1~12之间,日期在1~31之间。

从权重的角度来看,年>月>日,所以,排序的时候应该是先按照日来排序,再按照月来排序,最后按照年来排序。

考虑到按照年龄从小到大排序,所以日这一项应该从大到小排序(日这个数字越大的人年纪越小)。

看一看第一趟先对日这一项进行排序,如图4所示。

第二趟要针对月这一项进行排序,这一项也应该从大到小(月这个数字越大的人年纪越小),如图5所示。

第三趟要针对年这一项进行排序,这一项也应该从大到小(年这个数字越大的人年纪越小),如图6所示。

经过上述三趟的分配和收集操作,就可以得到学生按照年龄递增的排序。从这个范例中可以看到,每趟处理所采用的辅助队列大小是可以不同的。

根据前面的分析,基数排序算法的时间复杂度是O(d(n+k))。里面的字母都是什么意思呢?

  • d表示趟数,这里是3。

  • n是5000,因为参与年龄排序的学生是5000名。

  • k是每趟排序用到的辅助队列大小,第一趟是31,第二趟是12,第三趟是21,这里按最大值31计算。

  • 把d、n、k代入时间复杂度公式O(d(n+k)) ≈ O(15093)。相比于一些其他时间复杂度的排序算法比如O($n^{2}$) ≈ O(25000000)或者O(n$log_{2}^{n}$) ≈ O(60000)来说,基数排序算法的时间复杂度表现非常好。

理解基数排序的应用之后,我们尝试总结一下它的适用场合。

  • 记录中要排序的关键字可以很方便地拆分成几组,比如上述范例的年、月、日是三组。组数当然也不能太大,因为每多一组就代表多一趟分配和收集处理。

  • 每组关键字的取值范围也不应该太大,比如上述范例年月日的取值范围都不算大。否则算法需要的辅助空间也会太大导致空间复杂度过高。

  • 待排序记录数量多多益善,记录数量多意味着要排序的元素数量n较多。如果待排序记录很少,则没有必要用基数排序,基数排序毕竟需要不少的辅助空间,杀鸡焉用牛刀。


3. 常见排序算法复杂度及稳定性汇总 

        学习到这里,如果你觉得本篇文章对你有一点帮助的话,希望您能点个赞或评论支持一下~  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1527958.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

智慧城市与数字孪生:科技融合助力城市可持续发展

随着信息技术的迅猛发展&#xff0c;智慧城市和数字孪生作为现代城市发展的重要理念和技术手段&#xff0c;正日益受到广泛关注。智慧城市通过集成应用先进的信息通信技术&#xff0c;实现城市管理、服务、运行的智能化&#xff0c;而数字孪生则是利用数字化手段对物理城市进行…

面试笔记——Redis(使用场景、面临问题、缓存穿透)

Redis的使用场景 Redis&#xff08;Remote Dictionary Server&#xff09;是一个内存数据结构存储系统&#xff0c;它以快速、高效的特性闻名&#xff0c;并且它支持多种数据结构&#xff0c;包括字符串、哈希表、列表、集合、有序集合等。它主要用于以下场景&#xff1a; 缓…

libmodbus编译为64位动态库

通用方法&#xff0c;记录一下&#xff0c;以便后续参考。 Step 1. 下载libmodbus源码 GitHub - stephane/libmodbus: A Modbus library for Linux, Mac OS, FreeBSD and Windows Step 2. 生成配置文件 进入libmodbus-master\src\win32目录&#xff0c;在该目录下打开终端&am…

【火猫TV】DOTA2 BB队员称:队伍非常具有凝聚力

1、近日BB战队队员Nightfall接受采访时表示战队自从去年获得梦幻联赛S20亚军以来&#xff0c;就非常具有凝聚力。 “我认为战队自从去年获得梦幻联赛S20亚军以来&#xff0c;战队就非常具有凝聚力。从那一刻开始&#xff0c;我们这群人不再只是几个选手组建起来的松散组织&…

移动云COCA架构实现算力跃升,探索人工智能新未来

近期&#xff0c;随着OpenAI正式发布首款文生视频模型Sora&#xff0c;标志着人工智能大模型在视频生成领域有了重大飞跃。Sora模型不仅能够生成逼真的视频内容&#xff0c;还能够模拟物理世界中的物体运动与交互&#xff0c;其核心在于其能够处理和生成具有复杂动态与空间关系…

C++的语法

可能需要用到存储各种数据类型&#xff08;比如字符型、宽字符型、整型、浮点型、双浮点型、布尔型等&#xff09; 下表显示了各种变量类型在内存中存储值时需要占用的内存&#xff0c;以及该类型的变量所能存储的最大值和最小值。 注意&#xff1a;不同系统会有所差异 #inc…

8.发布页面

发布页面 官网 https://vkuviewdoc.fsq.pub/components/form.html 复制官网中的内容 代码 write.vue <template><view class"u-wrap u-p-l-20 u-p-r-20"><u-form :model"addModel" ref"form1"><u-form-item label&quo…

出彩不停息!创维汽车SKYHOME又获国际大奖

祝贺&#xff01;创维汽车SKYHOME又获国际缪斯设计大奖&#xff01;进一步获得国际认可&#xff01; 卓越的意识、优秀的审美、无与伦比的专注&#xff0c;不仅是缪斯奖所看重的独特品质&#xff0c;也是SKYHOME设计团队在传递品牌故事中所秉持的优秀品格。作为缪斯奖青睐的设计…

干货分享 | 外贸搞钱必备!

一、谷歌搜索&#xff1a; 开发客户的神器&#xff0c;需结合搜索指令&#xff0c;常见的12大搜索指令如下&#xff1a; 1. 指令运用&#xff1a;email 说明&#xff1a;指定搜索内容&#xff0c;缩小范围&#xff0c;显示带有email的搜索结果 2.- 指令运用&#xff1a;-B2C 说…

【G. One-Dimensional Puzzle (组合数学+逆元)

解析&#xff1a; 本体是进行分类讨论这么才使全部的拼图用完&#xff0c;且可以合成多个种类。 列举其所有可以拼成的方法个数&#xff1a; 第一种&#xff1a;3 3 3 3 第二种&#xff1a; 4 4 4 4 第三种&#xff1a;1 2 1 2 第四种&#xff1a;1 3 3 2 第五种&…

新品上线 | NoV病毒抗原抗体系列,助力疫苗相关研究!

背景介绍 诺如病毒&#xff08;norovirus&#xff0c;NoV&#xff09;&#xff0c;又名诺瓦克病毒&#xff08;norwalk viruses&#xff0c;NV&#xff09;&#xff0c;是一种单链正义RNA病毒&#xff0c;引发急性胃肠炎&#xff08;AGE&#xff09;的主要原因之一。病症常伴随…

Java安全 反序列化(1) URLDNS链原理分析

Java安全 反序列化(1) URLDNS链原理分析 文章目录 Java安全 反序列化(1) URLDNS链原理分析前置知识应用分析payload1.新建HashMap类2.新建URL类3.获取URL 的 Class对象4.通过反射访问URL内部变量5.通过反射为URL中类赋值6.调用HashMap#put方法传入key和value7.再次通过反射为UR…

自己录的视频怎么配上字幕?推荐几种方法

自己录的视频怎么配上字幕&#xff1f;在数字化时代&#xff0c;视频已经成为人们获取信息、娱乐消遣的重要形式。而对于许多内容创作者来说&#xff0c;为自己的视频添加字幕不仅能提升观众的观看体验&#xff0c;还能增加视频的专业度和吸引力。那么&#xff0c;如何为自己的…

如何在Linux系统下创建共享文件?

首先共享文件一定不能在各用户的家目录下创建&#xff0c;因为各用户家目录通常都只对自己开放&#xff0c;其他用户没有权限进入目录&#xff0c;读写其中的文件。 因此&#xff0c;共享文件必须在非用户目录下创建&#xff0c;且需要将other的权限全部放开。例如&#xff0c;…

Springboot整合支付宝沙箱支付

2.配置说明 要记住这几个重要的配置 appId 这个是appIdprivateKey 商户私钥publicKey 支付宝公钥, 即对应APPID下的支付宝公钥notifyUrl 支付成功后异步回调地址(注意是必须是公网地址)returnUrl #支付后回调地址signType 签名类型 一般写 RSA2charset utf-8format json #网关…

PHP+MySQL开发组合:多端多商户DIY商城源码系统 带完整的搭建教程以及安装代码包

近年来&#xff0c;电商行业的迅猛发展&#xff0c;越来越多的商户开始寻求搭建自己的在线商城。然而&#xff0c;传统的商城系统往往功能单一&#xff0c;无法满足商户个性化、多样化的需求。同时&#xff0c;搭建一个功能完善的商城系统需要专业的技术团队和大量的时间成本&a…

数字人解决方案— SadTalker语音驱动图像生成视频原理与源码部署

简介 随着数字人物概念的兴起和生成技术的不断发展&#xff0c;将照片中的人物与音频输入进行同步变得越来越容易。然而&#xff0c;目前仍存在一些问题&#xff0c;比如头部运动不自然、面部表情扭曲以及图片和视频中人物面部的差异等。为了解决这些问题&#xff0c;来自西安…

华为OD七日集训第3期 - 按算法分类,由易到难,循序渐进,玩转OD

目录 一、适合人群二、本期训练时间三、如何参加四、七日集训第 3 期五、精心挑选21道高频100分经典题目&#xff0c;作为入门。第1天、逻辑分析第2天、字符串处理第3天、矩阵第4天、深度优先搜索dfs算法第5天、回溯法第6天、二分查找第7天、图、正则表达式 大家好&#xff0c;…

机器学习_正则化

文章目录 代价函数 如果我们有非常多的特征&#xff0c;我们通过学习得到的假设可能能够非常好地适应训练集&#xff08;代价函数可能几乎为 0&#xff09;&#xff0c;但是可能会不能推广到新的数据。 下图是一个回归问题的例子&#xff1a; 第一个模型是一个线性模型&#xf…

AGI之Humanoid Robot:Figure 01的介绍、核心技术、功能、应用案例之详细攻略

AGI之Humanoid Robot&#xff1a;Figure 01的介绍、核心技术、功能、应用案例之详细攻略 目录 Figure 01的介绍 1、特点 2、应用场景 Figure 01的主要功能 Figure 01的应用案例 1、传递苹果 2、搬箱子 3、拿盘子 Figure 01的介绍 2024年3月13日&#xff0c;Figure 01是…