数据结构排序系列指南:从O(n²)到O(n),计数排序如何实现线性时间复杂度



🔥@晨非辰Tong: 个人主页
👀专栏:《C语言》《数据结构与算法入门指南》
💪学习阶段:C语言、数据结构与算法初学者
⏳"人理解迭代,神理解递归。"


文章目录


引言

计数排序是排序算法中的"异类"------它不通过比较元素大小,而是通过统计计数来实现排序。这种基于"鸽巢原理"的独特思路,让它在特定场景下能达到惊人的线性时间复杂度O(n+range)

本文将从基础原理出发 ,通过C语言实战代码,详细讲解计数排序的"统计-映射-回收"核心步骤,帮助您彻底掌握这种高效排序算法的实现技巧和应用场景。


一、 计数排序的理念精髓:什么是"以数为据"?

1.1 计数排序的"异类"的体现

计数排序是一种非比较排序算法。顾名思义,它的核心思想不是通过比较元素的大小来确定顺序,而是通过计数 的方式来实现排序。

计数排序别名又叫"鸽巢原理",其本质体现是:如果鸽子的数量比鸽巢多,那么至少有一个鸽巢里面不止一个鸽子 。那么就引出排序原理:将数组本身作为数组的索引,通过统计每个整数出现的次数,来直接确定每个整数在排序后的数组中的正确位置。

特性维度 比较排序 非比较排序
核心原理 通过比较元素之间的相对大小来确定元素位置 利用算术运算、映射等特定数据本身的性质来确定位置
决策基础 元素间的序关系 元素本身的实际值
代表性算法 快速排序、归并排序、堆排序 计数排序
使用场景 通用场景,数据范围大或类型复杂 数据有明显特征,整数、范围小

二、计数排序的底层逻辑:"统计与映射"取代比较?

在这里,你将会看到的是最全面、最系统地对计数排序的算法思路的解释(附图解)。

操作步骤:

  1. 统计数组中相同元素出现的次数;
  2. 根据统计结果将序列回收到原序列中;

2.1 算法逻辑:简单起步,理解计数统计

为了能够将计数排序操作步骤先理解清楚,先从一个较为简单的数组arr[6,1,2,4,9,4,2,4,1,4],演示从统计相同数据重复出现的次数到回归原数组全过程。

图示如下:

过程剖析(大致思路):

  • 先进行一次循环遍历原数组的每个元素,同时将每个元素出现的次数进行记录------>这就是所谓的"统计 "操作
  • 下一步就是将记录到的每个元素出现的次数存放在新创建的数组count中(当然要根据数据与count 数组下标进行匹配后再进行存放,存放结果如上图)。
c 复制代码
int arr[n] = {//.....//};
int count[a] = {//.....//};
//遍历统计
for(int i = 0; i < n; i++)
{
	//将原数组元素作为下标,这就实现了数据与下标匹配
	count[arr[i]]++;
}
  • 整个过程的最后,再次进行循环遍历,但这次遍历的对象是 count 数组。由于未涉及到的数据在count 数组中体现为0,等遍历到 !0 时就将当前位置的下标数字(原数组数据)放入原数组(注意,此操作也是循环内)。
c 复制代码
//......
int index = 0;//定义下标变量

for(int i = 0; i < a; i++)
{
	while(count[i]--)
	{
		//这里的i就是count数组的下标-->数据
		arr[index++] = i;				
	}
}

关键点解析(精准定位):

  1. count数组的大小是如何确定的?

第一种 :在上面的思路中:是数组arr的数据对应着数组count 的下标存放出现次数。那么很容易想到------>原数组元素max+1(数组下标从0开始,max 对应最大下标,空间大小就是max+1)来申请count数组的大小。

C语言代码实战:

--大家自行将代码分文件(其中打印函数代表不在演示)。

c 复制代码
//非比较排序------计数排序
void CountSort(int* arr, int n)
{
	//找最大值
	int max = arr[0];
	for (int i = 0; i < n; i++)
	{
		if (arr[i] > max)
		{
			max = arr[i];
		}
	}

	//确定count数组的空间大小
	int range = max + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		perror("malloc fail");
		exit(1);
	}

	//因为malloc,先对数组初始化
	memset(count, 0, sizeof(int) * (range));

	//进行"统计"
	for (int i = 0; i < n; i++)
	{
		count[arr[i]]++;//确定大小的第一种情况
	}

	//"回收"
	int index = 0;
	for (int i = 0; i < range; i++)
	{
		//当count数组元素!0在进入
		while (count[i]--)
		{
			//数据匹配下标,将下标存放
			arr[index++] = i;
		}
	}
}

test01()
{
	int arr[] = {6, 1, 2, 4, 9, 4, 2, 4, 1, 4};
	int n = sizeof(arr) / sizeof(arr[0]);
	printf("排序之前:");
	PrintArr(arr, n);
	//计数排序
	QuickSort(arr, 0, n - 1);
	CountSort(arr, n);
	printf("排序之后:");
	PrintArr(arr, n);
}

2.2 算法逻辑:进阶核心,掌握映射定位

关键点解析(精准定位):

  1. count数组的大小是如何确定的?

第二种 :当使用范围跨度较大原数组时,第一种申请数组count的空间大小,会造成空间的浪费。

根据图示,原数组为{100,101,109,105,101,105},这样申请的数组从下标0开始到下标109结束,前面的 0~99 的空间全部浪费,这样就不太好。

如果只考虑为100~109申请对应数量的空间------>max-min +1,那么存放数据就用到了"映射"。这里先给出"映射"公式:data-min;使用原因:前面大小范围确定了,就要对应下标放入统计的次数。使用"映射",就将计数数组的元素压缩到仅与数据的范围(max - min + 1) 的索引有关。


C语言代码实战(修改版)

c 复制代码
//2.
void CountSort(int* arr, int n)
{
	//找最大值、最小值
	int max = arr[0], min = arr[0];
	for (int i = 0; i < n; i++)
	{
		if (arr[i] < min)
		{
			min = arr[i];
		}
		if (arr[i] > max)
		{
			max = arr[i];
		}
	}

	//确定count数组的空间大小
	int range = max -min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		perror("malloc fail");
		exit(1);
	}

	//因为malloc,先对数组初始化
	memset(count, 0, sizeof(int) * (range));

	//进行"统计"、映射
	for (int i = 0; i < n; i++)
	{
		count[arr[i] - min]++;//确定大小的第二种情况
	}

	int index = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			//逆映射
			arr[index++] = i + min;
		}
	}
}

计数排序的最终版------>"映射",非常完美。

三、计数排序的性能优势:"非比较"如何带来线性时间?

计数排序的特性:

  • 计数排序在数据范围集中时,效率很高,但是适用范围以及场景有限;
  • 时间复杂度:O(n+range) 空间复杂度:O(range);
  • 稳定性:稳定。

3.1 算法核心:时间O(n+k) 的深度解读

我们以第二版实现的代码为例,剖析计数排序的时间复杂度。

代码中第一个for循环,循环次数取决于原数组元素个数,时间复杂度:O(N)

代码中第二个for循环,循环次数又是取决于原数组大小,并且两个循环为并列关系,所以前两个循环的时间复杂度:O(N)

这次的循环就比较特殊,因为循环次数取决于range = max- min + 1,由输入数据的值域范围决定,并且这个范围可以独立于数据量 n 发生巨大变化,必须将它作为一个独立的变量来考虑。时间复杂度:O(N + range)

  1. 数据max,min差距较小。场景一:对 1,000,000 个人的年龄进行排序
  • n (数据量) = 1,000,000;
  • min = 0, max = 150 ;
  • range (范围) = 150 - 0 + 1 = 151;
  • 时间复杂度 O(n + range ) = O(1,000,000 + 151) ≈ O(n) ;
  • 结论:非常高效,因为 range 很小且固定。
  1. 数据max,min差距较大。场景二:对 10 个随机整数排序,数值在 0 到 1,000,000,000 之间
  • n (数据量) = 10;
  • min = 0, max = 1,000,000,000 (假设);
  • k (范围) = 1,000,000,000 - 0 + 1 = 1,000,000,001;
  • 时间复杂度 O(n + k) = O(10 + 1,000,000,001) ≈ O(k) ;
  • 结论:极其低效,虽然只有10个数据,但 range 巨大无比

核心洞察

计数排序的效率不取决于你有多少数据,而取决于你的数据"分散"在多大的范围内


系列博客 :
归并排序全透视:从算法原理全分析到源码实战应用

排序详解:从快速排序分区逻辑,到携手冒泡排序的算法效率深度评测

从"最小值筛选"到代码落地,解锁选择排序的核心思想!

总结

html 复制代码
🍓 我是晨非辰Tong!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:

计数排序是一种高效的非比较型排序算法,通过统计每个元素的出现频率来确定其在输出数组中的位置。算法步骤包括:统计频率、计算位置、放置元素。时间复杂度为O(n+range)线性时间。适用于小范围整数排序但需要提前知道数据范围,大范围数据可能消耗较多内存。

随着计数排序的学习完成,我们的"数据结构主线系统学习"也告一段落。这段系统学习让我们从基础的线性结构(数组、链表)到复杂的树形结构(二叉树、堆),逐步构建了完整的数据结构知识体系。重要的是,我们培养了算法思维,理解时空复杂度的权衡,掌握从理论到实践的完整实现流程。

相关推荐
2301_812914871 小时前
简单神经网络
人工智能·深度学习·神经网络
RoboWizard2 小时前
高性能电脑热战寒冬 11月DIY配置推荐
linux·运维·服务器·电脑·金士顿
q***04052 小时前
Nginx 缓存清理
运维·nginx·缓存
koo3642 小时前
pytorch环境配置
人工智能·pytorch·python
残影飞雪2 小时前
Jetson版本下Pytorch和torchvision
c++
松涛和鸣3 小时前
14、C 语言进阶:函数指针、typedef、二级指针、const 指针
c语言·开发语言·算法·排序算法·学习方法
星期天25 小时前
3.0 C语⾔内存函数:memcpy memmove memset memcmp 数据在内存中的存储:整数在内存中的存储 ⼤⼩端字节序和字节序判断
c语言·数据结构·进阶·内存函数·数据内存存储
模型启动机6 小时前
黄仁勋GTC开场:「AI-XR Scientist」来了!
人工智能·ai·大模型
小小测试开发6 小时前
JMeter JSR223预处理程序全攻略:用Groovy解锁复杂场景自动化
运维·jmeter·自动化