0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

深入浅谈计数排序

算法与数据结构 来源:袁厨的算法小屋 作者:厨子 2021-04-28 16:20 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

计数排序虽然不是面试常考题目,但是计数排序的求统计数组步骤和最后元素归位思想是我们刷题时经常用到的,例如原地置换,使用数组模拟 hashmap 等,所以还是很有必要看一下的。

今天我们就一起来看看线性排序里的计数排序到底是怎么回事吧。

我们将镜头切到袁记菜馆

因为今年袁记菜馆的效益不错,所以袁厨就想给员工发些小福利,让小二根据员工工龄进行排序,但是菜馆共有 100000 名员工,菜馆开业 10 年,员工工龄从 0 - 10 不等。

看来这真是一个艰巨的任务啊。

当然我们可以借助之前说过的 归并排序 和 快速排序 解决,但是我们有没有其他更好的方法呢?

了解排序算法的老哥可能已经猜到今天写什么啦。是滴,我们今天来写写用空间换时间的线性排序。

说之前我们先来回顾一下之前的排序算法,最好的时间复杂度为 O(nlogn) ,且都基于元素之间的比较来进行排序。

我们来说一下非基于元素比较的排序算法,且时间复杂度为 O(n),时间复杂度是线性的,所以我们称其为线性排序算法。

其优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k),此时的 k 则代表整数的范围。快于任何一种比较类排序算法,不过也是需要牺牲一些空间来换取时间。

下面我们先来看看什么是计数排序,这个计数的含义是什么?

我们假设某一分店共有 10 名员工,

工龄分别为 1,2,3,5,0,2,2,4,5,9

那么我们将其存在一个长度为 10 的数组里,,但是我们注意,我们数组此时存的并不是元素值,而是元素的个数。见下图

09f15c56-a7f9-11eb-9728-12bb97331649.png

注:此时我们这里统计次数的数组长度根据最大值来决定,上面的例子中最大值为 9 ,则长度为 9 + 1 = 10。暂且先这样理解,后面会对其优化 。

我们继续以上图的例子来说明,在该数组中,索引代表的为元素值(也就是上面例子中的工龄),数组的值代表的则是元素个数(也就是不同工龄出现的次数)。

即工龄为 0 的员工有 1 个, 工龄为 1 的员工有 1 个,工龄为 2 的员工有 3 个 。。。

然后我们根据出现次数将其依次取出看看是什么效果。

0,1,2,2,2,3,4,5,5,9

我们发现此时元素则变成了有序的,但是这并不是排序,只是简单的按照统计数组的下标,输出了元素值,并没有真正的给原始数组进行排序。

这样操作之后我们不知道工龄属于哪个员工。

见下图

0a30bfea-a7f9-11eb-9728-12bb97331649.png

举例

虽然喵哥和杰哥工龄相同,如果我们按照上面的操作输出之后,我们不能知道工龄为 4 的两个员工,哪个是喵哥哪个是杰哥。

所以我们需要借助其他方法来对元素进行排序。

大家还记不记得我们之前说过的前缀和,下面我们通过上面统计次数的数组求出其前缀和数组。

0a4cf3d6-a7f9-11eb-9728-12bb97331649.png

因为我们是通过统计次数的数组得到了前缀和数组,那么我们来分析一下 presum 数组里面值的含义。

例如我们的 presum[2] = 5 ,代表的则是原数组小于等于 2 的值共有 5 个。presum[4] = 7 代表小于等于 4 的元素共有 7 个。

是不是感觉计数排序的含义要慢慢显现出来啦。

其实到这里我们已经可以理解的差不多了,还差最后一步,

此时我们要从后往前遍历原始数组,然后将遍历到的元素放到临时数组的合适位置,并修改 presum 数组的值,遍历结束后则达到了排序的目的。

这时有人要问了,为什么我们要从后往前遍历呢?

这个问题的答案,我们等下说,继续往下看吧。

0a583e94-a7f9-11eb-9728-12bb97331649.png

计数排序

我们从后往前遍历,nums[9] = 9,则我们拿该值去 presum 数组中查找,发现 presum[nums[9]] = presum[9] = 10 ,

大家还记得我们 presum 数组里面每个值的含义吗,我们此时 presum[9] = 10,则代表在数组中,小于等于的数共有 10 个,则我们要将他排在临时数组的第 10 个位置,也就是 temp[9] = 9。

我们还需要干什么呢?我们想一下,我们已经把 9 放入到 temp 数组里了,已经对其排好序了,那么我们的 presum 数组则不应该再统计他了,则将相应的位置减 1 即可,也就是 presum[9] = 10 - 1 = 9;

0a79d392-a7f9-11eb-9728-12bb97331649.png

下面我们继续遍历 5 ,然后同样执行上诉步骤

0aa1f8f4-a7f9-11eb-9728-12bb97331649.png

我们继续查询 presum 数组,发现 presum[5] = 9,则说明小于等于 5 的数共有 9 个,我们将其放入到 temp 数组的第 9 个位置,也就是

temp[8] = 5。然后再将 presum[5] 减 1 。

0aba5ae8-a7f9-11eb-9728-12bb97331649.png

是不是到这里就理解了计数排序的大致思路啦。

那么我们为什么需要从后往前遍历呢?我们思考一下,如果我们从前往后遍历,相同元素的话,前面的元素则会先归位再减一,这样则会使计数排序变成不稳定的排序算法。

这个排序的过程像不像查字典呢?通过查询 presum 数组,得出自己应该排在临时数组的第几位。然后再修改下字典,直到遍历结束。

那么我们先来用动画模拟一下我们这个 bug 版的计数排序,加深理解。

注:我们得到 presum 数组的过程在动画中省略。直接模拟排序过程。

计数排序

但是到现在就完了吗?显然没有,我们思考下这个情况。

假如我们的数字为 90,93,94,91,92 如果我们根据上面方法设置 presum 数组的长度,那我们则需要设置数组长度为 95(因为最大值是94),这样显然是不合理的,会浪费掉很多空间。

还有就是当我们需要对负数进行排序时同样会出现问题,因为我们求次数的时候是根据 nums[index] 的值来填充 presum 数组的,所以当 nums[index] 为负数时,填充 presum 数组时则会报错。

此时通过最大值来定义数组长度也不合理。

所以我们需要采取别的方法来定义数组长度。

下面我们来说一下偏移量的概念。

例如 90,93,94,91,92,我们 可以通过 max ,min 的值来设置数组长度即 94 - 90 + 1 = 5 。偏移量则为 min 值,也就是 90。那么我们的 90 则对应索引 0 。

见下图。

0afe7e08-a7f9-11eb-9728-12bb97331649.png

这样我们填充 presum 数组时就不会出现浪费空间的情况了,负数?出现负数的情况当然也可以。继续看

例如:-1,-3,0,2,1

0b08c994-a7f9-11eb-9728-12bb97331649.png

一样可以,哦了,到这里我们就搞定了计数排序,下面我们来看一哈代码吧。

class Solution {

public int[] sortArray(int[] nums) {

int len = nums.length;

if (nums.length 《 1) {

return nums;

}

//求出最大最小值

int max = nums[0];

int min = nums[0];

for (int x : nums) {

if (max 《 x) max = x;

if (min 》 x) min = x;

}

//设置 presum 数组长度,然后求出我们的前缀和数组,

//这里我们可以把求次数数组和前缀和数组用一个数组处理

int[] presum = new int[max-min+1];

for (int x : nums) {

presum[x-min]++;

}

for (int i = 1; i 《 presum.length; ++i) {

presum[i] = presum[i-1]+presum[i];

}

//临时数组

int[] temp = new int[len];

//遍历数组,开始排序,注意偏移量

for (int i = len-1; i 》= 0; --i) {

//查找 presum 字典,然后将其放到临时数组,注意偏移度

int index = presum[nums[i]-min]-1;

temp[index] = nums[i];

//相应位置减一

presum[nums[i]-min]--;

}

//copy回原数组

System.arraycopy(temp,0,nums,0,len);

return nums;

}

}

好啦,这个排序算法我们已经搞定了,下面我们来扒一扒它。

计数排序时间复杂度分析

我们的总体运算量为 n+n+k+n ,总体运算是 3n + k 所以时间复杂度为 O(N+K);

计数排序空间复杂度分析

我们用到了辅助数组,空间复杂度为 O(n)

计数排序稳定性分析

稳定性在我们最后存入临时数组时有体现,我们当时让其放入临时数组的合适位置,并减一,所以某元素前面的相同元素,在临时数组,仍然在其前面。所以计数排序是稳定的排序算法。

虽然计数排序效率不错但是用到的并不多。

这是因为其当数组元素的范围太大时,并不适合计数排序,不仅浪费时间,效率还会大大降低。

当待排序的元素非整数时,也不适用,大家思考一下这是为什么呢?好啦,今天的文章就到这啦,我们下期再见,拜了个拜

巨人的肩膀

算法导论

极客时间数据结构与算法之美
编辑:lyn

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 计数
    +关注

    关注

    1

    文章

    58

    浏览量

    20560
  • 排序
    +关注

    关注

    0

    文章

    32

    浏览量

    9946
  • 排序算法
    +关注

    关注

    0

    文章

    53

    浏览量

    10372

原文标题:计数排序真的不重要?

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    深入解析与使用感受:Isograph、Medini与REANA可靠性分析软件对比

    上海磐时PANSHI“磐时,做汽车企业的安全智库”深入解析与使用感受:Isograph、Medini与REANA可靠性分析软件对比汽车行业的复杂性和对安全性的高要求,使得传统的分析工具往往需要耗费
    的头像 发表于 09-05 16:20 10次阅读
    <b class='flag-5'>深入</b>解析与使用感受:Isograph、Medini与REANA可靠性分析软件对比

    国际首创新突破!中国团队以存算一体排序架构攻克智能硬件加速难题

    2025 年 6 月 25 日,北京大学团队在智能计算硬件方面取得领先突破,国际上首次实现了基于存算一体技术的高效排序硬件架构 (A fast and reconfigurable
    的头像 发表于 07-02 16:50 545次阅读
    国际首创新突破!中国团队以存算一体<b class='flag-5'>排序</b>架构攻克智能硬件加速难题

    雷电(雷击)计数器的原理、作用及行业应用解决方案

    安全事故。雷电(雷击)计数器作为防雷系统中的重要组成部分,扮演着“记录雷击事件、辅助安全运维”的关键角色,已经成为智能防雷系统中不可或缺的一环。 二、雷电(雷击)计数器的原理 1. 工作原理 雷电计数器主要依靠电磁
    的头像 发表于 06-12 15:14 773次阅读
    雷电(雷击)<b class='flag-5'>计数</b>器的原理、作用及行业应用解决方案

    九进制计数电路仿真设计

    九进制计数电路仿真
    发表于 06-09 14:48 0次下载

    低成本电源排序器解决方案

    绝大多数负载点DC-DC转换器可以将上一个转换器的电源就绪输出连接至下一个转换器的使能输入,实现上电排序。这种方法只适合比较简单的设计,不能满足多数现代微处理器和DSP的要求一这类器件要求断电顺序必须与上电顺序相反。许多厂商针对这类应用推出了可编程排序IC,但器件价格较为
    的头像 发表于 05-21 09:55 963次阅读
    低成本电源<b class='flag-5'>排序</b>器解决方案

    STM32U083进行外部计数只能计数8000多一点,为什么?

    背景: 使用LPTIME1进行计数。 LPTIMER配置如下: 时钟配置如下: 使用过LSE;PCLK配置,计数还是一样的 实际计数只能计数到8000多;使用信号发生器产生9KHz
    发表于 03-12 06:21

    浅谈直流有刷电机驱动及调速技术

    ,图1 为 H 桥电机驱动 电路示意图 : 图1 H桥电机驱动电路示意图 点击下方附件查看全文*附件:20250307_浅谈直流有刷电机驱动及调速技术.docx
    发表于 03-07 15:24

    如何在SonarWiz中导入和处理磁强计数

    本指南将向您介绍如何在 SonarWiz 中导入和处理磁强计数据。 我们概述的程序将减少数据中的昼夜变化和航向变化,消除层回偏移,并生成总场图和分析场图。 下一步是在数据中标记磁异常,并将异常与声纳
    的头像 发表于 02-17 17:29 811次阅读
    如何在SonarWiz中导入和处理磁强<b class='flag-5'>计数</b>据

    【RA-Eco-RA2L1-48PIN-V1.0开发板试用】——PWM

    定时器,共有6个通道(GPT16,编号4-9) 3、支持三种计数模式(上计数模式、下计数模式、上下计数模式) 4、每个通道都可以独立选择时钟源 5、每个通道都有两个输入\\\\输出引脚
    发表于 01-26 15:40

    攻克反光难题,整箱矿泉水高速精准计数

    在工业生产中,一次性进行大量计数的应用场景非常广泛。比如制药生产的药粒计数、食品加工、电子制造行业微小电子计数。这些场景不仅要求计数工具具备高精度和高速度,还要求能够应对各种复杂环境和
    的头像 发表于 01-14 07:34 677次阅读
    攻克反光难题,整箱矿泉水高速精准<b class='flag-5'>计数</b>

    UC-018:时间间隔计数器的用途

    电子发烧友网站提供《UC-018:时间间隔计数器的用途.pdf》资料免费下载
    发表于 01-13 17:02 0次下载
    UC-018:时间间隔<b class='flag-5'>计数</b>器的用途

    详解Linux sort命令之掌握排序技巧与实用案例

    排序,与-g区别为不转为浮点数 -g –general-number-sort 按通用数值排序,支持科学计数法 -f –ignore-case 忽略大小写,默认大小写字母不同 -k –key=POS1
    的头像 发表于 01-09 10:10 1581次阅读

    TimSort:一个在标准函数库中广泛使用的排序算法

    在计算机科学的领域,排序算法是每位学生必学的基础,而排序的需求是每位程序员在编程过程中都会遇到的。 在你轻松调用 .sort() 方法对数据进行排序时,是否曾好奇过,这个简单的方法背后使用的是哪种
    的头像 发表于 01-03 11:42 954次阅读

    霍尔开关的应用有哪些?矽睿半导体霍尔开关在产品计数的智能应用

    霍尔开关在产品计数中的智能应用主要体现在提高计数精度和效率方面。霍尔开关是一种感应元件,可以感知磁场的变化,并将其转化为电信号进行处理。这种特性使其在产品计数系统中具有广泛的应用。 霍尔开关
    的头像 发表于 12-20 16:48 792次阅读

    智能雷击计数器的综合行业解决方案

    智能雷击计数器 是一种用于记录雷击事件发生次数的高科技装置,广泛应用于防雷系统中。与传统的机械式雷击计数器相比,智能雷击计数器不仅能够精确记录雷击次数,还能对雷电参数进行分析,并通过智能通信模块实现
    的头像 发表于 12-20 10:50 861次阅读
    智能雷击<b class='flag-5'>计数</b>器的综合行业解决方案