【数据结构与算法】详解计数排序:小范围整数排序的最佳选择

news2024/9/23 7:31:19

            💓 博客主页:倔强的石头的CSDN主页 

           📝Gitee主页:倔强的石头的gitee主页

            ⏩ 文章专栏:《数据结构与算法》

                                  期待您的关注

 

1b7335aca73b41609b7f05d1d366f476.gif

 

目录

 一、引言

二、计数排序的基本原理

三、实现步骤

1. 确定数据范围

2. 初始化计数数组

3. 统计元素频率

4. 排序

5. 清理资源

四、C语言实现代码

🍃计数排序升序代码

🍃计数排序降序代码 

五、性能分析

🍃时间复杂度:O(n + k),近似于O(n)

🍃空间复杂度:O(k)

🍃稳定性:稳定

六、计数排序优缺点分析

优点

缺点

总结

 


 

 一、引言

传统的比较型排序算法(如快速排序、归并排序等)虽然应用广泛,但在面对特定类型的数据时,其性能往往受到一定限制。这时,非比较型排序算法,如计数排序,便展现出了其独特的优势。

 

本文旨在深入剖析计数排序的奥秘与魅力,从基本原理到实现步骤,从优缺点分析到实际应用场景,全面展现这一排序算法的独特之处。通过本文的阅读,读者将能够深刻理解计数排序的工作原理,掌握其实现方法,并学会在合适的场景下灵活运用这一算法,以提升数据处理的效率和质量。

 

二、计数排序的基本原理

计数排序(Counting Sort)是一种非比较型排序算法,它的基本原理是通过统计每个元素的出现次数,然后利用这些信息来重建排序后的数组。

 

核心思想在于:通过统计每个元素在数组中出现的次数,来确定该元素在排序后数组中的位置。这种方法在处理具有明显范围限制且分布相对均匀的整数数据时,尤为高效。

作为一种线性时间复杂度的排序,它要求输入的数据必须是有确定范围的整数。

 请看下图动图演示

027b5605bab4416094da68800bedf0d6.gif

 

三、实现步骤

1. 确定数据范围

首先,代码通过遍历待排序数组 a,找出其中的最大值 max 和最小值 min。这两个值用于确定计数数组 count 的大小,因为计数数组需要覆盖待排序数组中所有可能出现的值(在最小值和最大值之间)。

int min = INT_MAX;
int max = INT_MIN;
for (int i = 0; i < n; i++)//求得最大值和最小值
{
	if (a[i] > max)
		max = a[i];
	if (a[i] < min)
		min = a[i];
}

 

2. 初始化计数数组

根据最大值和最小值计算出的范围(max - min + 1),代码使用 calloc 分配了一个足够大的整数数组 count,并将所有元素初始化为 0。这个数组用于统计待排序数组中每个值出现的次数。使用 calloc 而不是 malloc 加初始化是为了确保所有元素都初始化为 0,因为计数排序需要这些初始值来正确统计。

int size = max - min + 1;
int* count = (int*)calloc(size, sizeof(int));//根据数据范围申请空间
if (count == NULL)
{
	perror("calloc fail\n");
	return;
}

 

3. 统计元素频率

接下来,代码再次遍历待排序数组 a,这次是为了统计每个元素出现的次数。对于数组 a 中的每个元素 a[i],代码通过 count[a[i] - min]++ 将 a[i] 的出现次数记录在 count 数组的相应位置上。这里减去 min 是为了将 a 中的值映射到 count 数组的有效索引范围内。

for (int i = 0; i < n; i++)//遍历原数组,将数据出现的次数写入count数组对应的位置
{
	count[a[i] - min]++;//核心代码1
}

 

4. 排序

代码遍历 count 数组,并根据每个值出现的次数,将对应的值依次放回原数组 a 中。这里使用了一个双层循环,外层循环遍历 count 数组的每个索引(即待排序数组中的每个可能值),内层循环(通过 while 循环实现)则根据 count[j] 的值(即该值出现的次数)将 j + min(即原始值)放回原数组 a 中。每次放回一个值后,count[j] 递减,直到该值的所有出现都被放回原数组。

int i = 0;
for (int j = 0; j < size; j++)//遍历count数组,根据数据出现次数,将数据写入原数组对应的位置
{
	while (count[j]--)//每写入一次,次数--
	{
		a[i++] = j + min;//核心代码2
	}
}

 

5. 清理资源

最后,代码释放了 count 数组占用的内存,并将其指针设置为 NULL,以避免野指针问题。

free(count);
count = NULL;

 

四、C语言实现代码

🍃计数排序升序代码

void CountSort1(int* a, int n)//计数排序升序
{
	int min = INT_MAX;
	int max = INT_MIN;
	for (int i = 0; i < n; i++)//求得最大值和最小值
	{
		if (a[i] > max)
			max = a[i];
		if (a[i] < min)
			min = a[i];
	}
	int size = max - min + 1;
	int* count = (int*)calloc(size, sizeof(int));//根据数据范围申请空间
	if (count == NULL)
	{
		perror("calloc fail\n");
		return;
	}

	for (int i = 0; i < n; i++)//遍历原数组,将数据出现的次数写入count数组对应的位置
	{
		count[a[i] - min]++;//核心代码1
	}

	int i = 0;
	for (int j = 0; j < size; j++)//遍历count数组,根据数据出现次数,将数据写入原数组对应的位置
	{
		while (count[j]--)//每写入一次,次数--
		{
			a[i++] = j + min;//核心代码2
		}
	}

	free(count);
	count = NULL;
}

🍃计数排序降序代码 

void CountSort2(int* a, int n)//计数排序降序
{
	int min = INT_MAX;
	int max = INT_MIN;
	for (int i = 0; i < n; i++)//求得最大值和最小值
	{
		if (a[i] > max)
			max = a[i];
		if (a[i] < min)
			min = a[i];
	}
	int size = max - min + 1;
	int* count = (int*)calloc(size, sizeof(int));//根据数据范围申请空间
	if (count == NULL)
	{
		perror("calloc fail\n");
		return;
	}

	for (int i = 0; i < n; i++)//遍历原数组,将数据出现的次数写入count数组对应的位置
	{
		count[a[i] - min]++;//核心代码1
	}

	int i = 0;
	for (int j = size-1; j >=0; j--)//遍历count数组,根据数据出现次数,将数据写入原数组对应的位置
	{
		while (count[j]--)//每写入一次,次数--
		{
			a[i++] = j + min;//核心代码2
		}
	}

	free(count);
	count = NULL;
}

 

 

五、性能分析

🍃时间复杂度:O(n + k),近似于O(n)

计数排序的时间复杂度主要由以下几个部分组成:

  1. 确定数据范围:遍历一次待排序数组,找出最大值和最小值,这个过程的时间复杂度是O(n),其中n是数组的长度。

  2. 初始化计数数组:根据最大值和最小值确定计数数组的大小,并初始化所有元素为0。这一步的时间复杂度主要取决于计数数组的大小,但因为是常数时间操作(尽管这个“常数”可能很大,但它不随n的变化而变化),所以通常认为它是O(1)的,但更准确地说,它是O(k),其中k是数据范围的大小(即最大值和最小值之间的差值加1)。然而,在分析计数排序的时间复杂度时,我们更关注n,因此这一步通常被忽略或视为O(1)。

  3. 统计元素频率:再次遍历待排序数组,统计每个元素的出现次数,并将结果存储在计数数组中。这一步的时间复杂度是O(n)。

  4. 累加计数(隐含步骤):在计数排序的某些实现中,这一步是显式的,但在你的代码中,它是隐含的,因为你在填充原数组时直接使用了计数数组的信息。无论是否显式进行,这一步的时间复杂度都是O(k)。然而,由于我们更关注n,且k通常远小于n(对于实际应用中的许多情况),这一步的时间复杂度通常被忽略。

  5. 排序:遍历计数数组,根据元素的出现次数将元素放回原数组。这一步的时间复杂度是O(n + k),因为你需要遍历计数数组(O(k))并将元素放回原数组(O(n))。但是,由于k通常远小于n,且这一步是计数排序中唯一与n直接相关的步骤(除了确定数据范围),所以这一步的时间复杂度通常简化为O(n)。

综上所述,计数排序的总时间复杂度是O(n + k)。然而,在实际应用中,由于k通常远小于n(例如,当排序的是一定范围内的整数时),计数排序的性能可以近似地看作是线性的,即O(n)。

 

🍃空间复杂度:O(k)

计数排序的空间复杂度主要取决于计数数组的大小,即O(k),其中k是数据范围的大小。如果k与n相比很大,那么计数排序可能会消耗大量的内存。因此,计数排序只适用于数据范围不是很大的情况。

 

🍃稳定性:稳定

计数排序能够保持相等元素的相对顺序不变,即它是稳定的排序算法。

计数排序通过统计每个元素的出现次数,并根据这些统计信息来重建排序后的数组。在这个过程中,如果两个元素的值相等,它们会被放入计数数组的同一个位置(或者更准确地说,是相邻的位置,因为计数数组会记录每个值出现的次数),并且在重建排序后的数组时,这些相等的元素会按照它们在原数组中的顺序被依次放回。

六、计数排序优缺点分析

优点

  1. 稳定性:计数排序是稳定的排序算法。如果两个元素相等,它们在排序后的数组中的相对位置与排序前相同。

  2. 高效性:在数据范围不是很大的情况下,计数排序的时间复杂度可以认为是线性的,即O(n+k),其中n是数组的长度,k是数据范围的大小。这使得计数排序在处理具有明确范围且分布相对均匀的整数数据时非常高效。

  3. 易于实现:计数排序的实现相对简单直观,不需要复杂的比较和交换操作。

  4. 适用场景广泛:计数排序不仅适用于整数排序,还可以扩展到其他类型的数据排序,只要能够确定数据的范围并且数据分布相对均匀即可。

缺点

  1. 空间复杂度高:计数排序需要额外的空间来存储计数数组,这个数组的大小取决于数据的范围。如果数据的范围很大,那么计数数组将占用大量的内存空间,可能导致内存溢出。

  2. 数据范围限制:计数排序要求能够确定数据的范围,这限制了它的应用场景。如果数据的范围很大或者无法确定,那么计数排序可能不是一个好的选择。

  3. 对重复数据敏感:当数据中存在大量重复元素时,计数排序的效率会受到影响,因为计数数组中的某些位置会被多次更新。然而,这通常不会显著影响总体性能,因为排序过程仍然是线性的。

  4. 非原地排序:计数排序不是原地排序算法,因为它需要额外的空间来存储计数数组。这可能会在某些内存受限的环境下成为问题。

总结

计数排序是一种高效的排序算法,特别适用于一定范围内的整数排序。它的稳定性和高效性使得它在处理特定类型的数据时非常有用。然而,计数排序的空间复杂度较高,且对数据范围有一定的限制,这限制了它的应用范围。在选择排序算法时,需要根据具体的应用场景和数据特性来决定是否使用计数排序。如果数据范围明确且分布相对均匀,且内存空间足够,那么计数排序是一个很好的选择。

 

 

 

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

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

相关文章

JavaScript基础——JavaScript调用的三种方式

JavaScript简介 JavaScript的作用 JavaScript的使用方式 内嵌JS 引入外部js文件 编写函数 JavaScript简介 JavaScript&#xff08;简称“JS”&#xff09;是一种具有函数优先的轻量级&#xff0c;解释型或即时编译型的编程语言。它是Web开发中最常用的脚本语言之一&#x…

高清无水印,录屏软件对比盘点

现在生活中不论是想要记录赛事精彩瞬间、制作教学视频&#xff0c;都可以用录屏大师这样的录屏软件来实现。今天我就介绍几款备受好评的录屏工具。 1.福昕录屏大师 链接直达&#xff1a;https://www.foxitsoftware.cn/REC/ 这个软件就是一个专业的录屏工具。它可以控制屏幕…

UltraEdit v27文本代码程序编辑器免费版下载安装教程(亲测可用)

前言 UltraEdit 是一套功能强大的文本编辑器&#xff0c;可以编辑文本、十六进制、ASCII 码&#xff0c;完全可以取代记事本&#xff08;如果电脑配置足够强大&#xff09;&#xff0c;内建英文单字检查、C 及 VB 指令突显&#xff0c;可同时编辑多个文件&#xff0c;而且即使…

OrangePi AI Pro 固件升级 —— 让主频从 1.0 GHz 到 1.6 GHz 的巨大升级

前言 OrangePi AI Pro 最近发布了Ascend310B-firmware 固件包&#xff0c;据说升级之后可以将 CPU 主频从 1.0 GHz 提升至 1.6 GHz&#xff0c;据群主大大说&#xff0c;算力也从原本的 8T 提升到了 12T&#xff0c;这波开发板的成长让我非常的 Amazing 啊&#xff01;下面就来…

【学习日记】U-Boot 环境变量与 U-Boot 命令概述

本文记录了在学习 i.MX6ULL 的 U-Boot 代码时关于环境变量和 U-Boot 命令的一些关键点。 1 环境变量的定义 在 U-Boot 中&#xff0c;使用 #define 和反斜杠 \ 来定义多行字符串。 反斜杠 \&#xff1a; 反斜杠用于指示宏定义在下一行继续。这意味着所有的行将被视为一个连续的…

mqtt协议详解(0)初步认识mqtt

文章目录 1. 介绍2. 主要特性3. 架构1. 介绍 MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议)是一种构建在TCP/IP协议之上的轻量级、基于发布-订阅模式的消息传输协议,适用于资源受限的设备和低带宽、高延迟或不稳定的网络环境,例如IOT。 MQTT 协议于 1…

JVM:栈上的数据存储

文章目录 一、Java虚拟机中的基本数据类型 一、Java虚拟机中的基本数据类型 在Java中有8大基本数据类型&#xff1a; 这里的内存占用&#xff0c;指的是堆上或者数组中内存分配的空间大小&#xff0c;栈上的实现更加复杂。 Java中的8大数据类型在虚拟机中的实现&#xff1a;…

【error】ModuleNotFoundError: No module named ‘mmcv.cnn.weight_init‘

from&#xff1a; oduleNotFoundError: No module named ‘mmcv.cnn.weight_init‘_modulenotfounderror: no module named mmcv.cnn-CSDN博客https://blog.csdn.net/qq_36679208/article/details/107815137?spm1001.2101.3001.6650.1&utm_mediumdistribute.pc_relevant.…

java基础 之 集合与栈的使用(二)

文章目录 List 和 Set的比较Set接口对于set的无序该怎么理解&#xff1f;&#xff08;一&#xff09;实现类&#xff1a;HashSet&#xff08;二&#xff09;实现类&#xff1a;LinkedHashSet【代码部分】HashSet 和 LinkedHashSet浅谈HashSet 和 LinkedHashSet的打印结果 &…

MYSQL-初级-事务篇

目录 概述为什么有事务&#xff1f; 事务操作事务的四大特性&#xff08;AICD&#xff09;原子性&#xff08;Atomicity&#xff09;一致性&#xff08;Consistency&#xff09;隔离性&#xff08;Isolation&#xff09;持久性&#xff08;Durability&#xff09; 并发事务问题…

为虚幻引擎C++项目设置VS开发环境

为虚幻引擎C项目设置VS开发环境 虚幻引擎&#xff08;简称UE&#xff09; 能与 Visual Studio&#xff08;简称VS&#xff09; 完美结合&#xff0c;使你能够快速、简单地改写项目代码&#xff0c;并能即刻查看编译结果。设置Visual Studio以使用虚幻引擎能提高开发者对虚幻引…

rust 桌面 sip 软电话(基于tauri 、pjsip库)

本文尝试下rust 的tauri 桌面运用 原因在于体积小 1、pjsip 提供了rust 接口官方的 rust demo 没编译出来 在git找了个sip-phone-rs-master https://github.com/Charles-Schleich/sip-phone-rs 可以自己编译下pjsip lib库替换该项目的lib 2、创建一个tauri demo 引用 [depe…

操作系统(4)——文件系统

目录 小程一言文件系统管理基础概念&功能基本概念文件的结构和属性文件的操作文件的安全性和权限控制文件系统的实现和分配方式 问题&解答1、文件系统在操作系统中起到什么作用&#xff1f;2、文件的逻辑结构和物理结构有何区别&#xff1f;3、如何理解文件权限控制在操…

时间序列异常值检验替换——基于Hampel滤波器

Hampel滤波器作为一种强大的时间序列异常值处理工具&#xff0c;在数据清洗和分析中发挥着重要作用。本文将深入研究Hampel滤波器的原理和数学推导&#xff0c;并通过实际代码演示其在异常值处理中的应用 1. Hampel滤波器简介 1.1 什么是Hampel滤波器&#xff1f; Hampel滤波…

【达梦数据库】通过线程pid定位会话SQL

【达梦数据库】通过线程pid定位会话SQL 1、查找数据库进程 ps -ef|grep dmserver2、通过进程pid去找对应的线程 top -H -p $pid -------------------- top命令经常用来监控linux的系统状况&#xff0c;是常用的性能分析工具&#xff0c;能够实时显示系统中各个进程的资源占用…

Signac包-1.Analyzing PBMC scATAC-seq

–https://stuartlab.org/signac/articles/pbmc_vignette 好的&#xff0c;开始学习scATAC-seq的数据是怎么玩的了&#xff0c;先跑完Signac的教程&#xff0c;边跑边思考怎么跟自己的课题相结合。 留意更多内容&#xff0c;欢迎关注微信公众号&#xff1a;组学之心 数据和R…

Qt安卓开发的一些概念

目录 1、Android 版本和 API 的对应关系&#xff1f; 2、ABI是什么 2.1、x86_64 2.2、x86 2.3、arm64-v8a 2.4、armeabi-v7a 3、不同架构的特点 3.1、32位 ARM 架构 (ARMv7) 3.2、64位 ARM 架构 (ARMv8-A) 3.3、32位 Intel 架构 (x86) 3.4、64位 Intel 架构 (x86-64…

vue+element-ui的列表查询条件/筛选条件太多以下拉选择方式动态添加条件(支持全选、反选、清空)

1、此功能已集成到TQueryCondition组件中 2、最终效果 3、具体源码(新增moreChoose.vue) <template><el-popoverpopper-class"t_query_condition_more":bind"popoverAttrsBind"ref"popover"v-if"allcheckList.length>0"…

本地VSCode连接远程linux环境服务器的docker

目录 1、安装远程SSH 2、连接远程主机 3、远程中安装docker 4、查看容器 &#xff08;1&#xff09;直接查看容器和镜像 &#xff08;2&#xff09;使用命令查看 最近在新服务器中执行程序&#xff0c;要用到远程的docker。但是命令行环境下查看代码非常不方便&#xff0…

upload-labs 1-19关 攻略 附带项目下载地址 小白也能看会

本文章提供的工具、教程、学习路线等均为原创或互联网收集&#xff0c;旨在提高网络安全技术水平为目的&#xff0c;只做技术研究&#xff0c;谨遵守国家相关法律法规&#xff0c;请勿用于违法用途&#xff0c;如有侵权请联系小编处理。 环境准备&#xff1a; 1.靶场搭建 下…