`
qindongliang1922
  • 浏览: 2147156 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
7265517b-f87e-3137-b62c-5c6e30e26109
证道Lucene4
浏览量:116320
097be4a0-491e-39c0-89ff-3456fadf8262
证道Hadoop
浏览量:124587
41c37529-f6d8-32e4-8563-3b42b2712a50
证道shell编程
浏览量:58453
43832365-bc15-3f5d-b3cd-c9161722a70c
ELK修真
浏览量:70348
社区版块
存档分类
最新评论

理解计数排序算法的原理和实现

    博客分类:
  • JAVA
阅读更多

计数排序(Counting sort)是一种稳定的线性时间排序算法,其平均时间复杂度和空间复杂度为O(n+k),其中n为数组元素的个数,k为待排序数组里面的最大值。同样具有线性时间排序的算法还有桶排序和基数排序,这一点不要搞混。


计数排序不是基于比较的排序,所以它的排序效率是线性的,在特定的场景下(已知数组的最大最小值,切数组元素整体量不是很大的情况下)排序效率极高,而基于比较排序的算法,其时间复杂度基本逃脱不了O(nlogn)的魔咒,当然能达到O(nlogn)的时间复杂度,已经是非常牛逼了,这里面典型的代表就是快速排序算法,因为没有其他条件限制,所以基本上是一种通用排序算法。


计数排序的算法的原理,其实是非常简单的,它不需要去跟其他元素比来比去,而是一开始就知道自己的位置,所以直接归位,在计数的该元素出现的词频数组里面,出现一次,就直接+1一次即可,如果没有出现改位置就是0,最后该位置的词频,就是代表其在原始数组里面出现的次数,由于词频数组的index是从0开始,所以最后直接遍历输出这个数组里面的每一个大于0的元素值即可。

我们先来看看简单版本的Java语言写的计数排序是如何实现的,假设有四个元素{2,1,0,1}。

```
    public static void  simpleCountSort(){
        int nums[]={2,1,0,1};

        int maxNum=2;

        int storeArray[]=new int[maxNum+1];

        //词频计数
        for(int i=0;i<nums.length;i++){
            storeArray[nums[i]]++;
        }

        System.out.println("==============排序后==============");

        int ndx=0;
        //遍历计数后的词频数组
        for (int i = 0; i <storeArray.length ; i++) {
            //对于每个index的值进行循环,输出,因为有可能重复
            while (storeArray[i]>0){
                nums[ndx]=i;//把词频数组的值,放回原数组里面,
                ndx++;//替换一个数,就索引自增
                storeArray[i]--;//词频减1,防止死循环
            }
        }


        System.out.println(Arrays.toString(nums));



    }
```

从上面可以看到,代码比较简单,但是并不是最优的,有三个缺点:

第一不支持负数排序,第二在特定情况下使用空间较多,比如90-100仅仅有10个元素,但是数组却需要声明空间为100,第三排序不具有稳定性,重复元素的相对位置可能会变。

经过优化后的计数排序算法,需要遍历一次得到元素的最小值和最大值,然后构造空间范围可以优化为,max-min+1,而不是前面简单的max,此外在实现的时候,对于原数组统计词频的时候,使用的每个元素减去min之后的值,这样能保证结果落在词频数组的范围之内,最后,为了保证排序算法的稳定性,我们需要对词频进行一次sum操作,从1开始,把每个位置的词频设置为当前的元素的词频+前一个元素的词频,这个值就代表了其在原数组里面应该出现的位置,接着我们倒序遍历原始数组,这样就能保证稳定性。 具体的算法过程,我推荐一个youtube上的一个视频,演示的最常清晰:

[url] https://m.youtube.com/watch?v=TTnvXY82dtM[/url]

优化后的代码如下:

```
  public static int[] countSort(int []a){
        //使用最大值和最小值的方式是一种优化的计数排序
        //可以兼容负数的情况,同时能减少存储的空间,比如最大数是100,但实际上只有90-100这10个数字
        //所以仅仅需要10个存储空间即可
        int max = a[0], min = a[0];
        for(int i : a){
            max=Math.max(max,i);
            min=Math.min(min,i);
        }
        System.out.println("max:"+max+"  min:"+min);
        int k = max - min + 1;
        System.out.println("count array len:"+k);

        int c[] = new int[k];
        //先是count计数词频
        for(int i = 0; i < a.length; ++i){
            c[a[i]-min] ++;//优化过的地方,减小了数组c的大小,同时a[i]-min能保证c数组的第一个元素一定有元素的
            //因为必定存在min-min=0
        }
        System.out.println("count: "+Arrays.toString(c));
        //然后为了保持排序稳定,我们需要做一次累加操作
        //这样做的目的,是为了标记出原始数组里面的该元素,前面有几个元素,这个值
        //实际上就是其在原生数组里面的位置,如果有重复的元素,则会先会
        //放置最右边的元素,这样就能保证,排序的稳定性
        for(int i = 1; i < c.length; ++i){
            c[i] = c[i] + c[i-1];
        }

        System.out.println("sumCount:"+Arrays.toString(c));

        //存储最终的排序结果
        int b[] = new int[a.length];
        //这里必须从后向前遍历,只有这样出现重复的元素,才会保持顺序的把最后面的重复元素,永远放在最右边。
        //从而保证排序的稳定性
        //如果从前向后排序,重复元素的顺序,刚好相反,所以就不是稳定的算法,但如果不关注稳定性,那么结果还是正确的
        for (int i = a.length-1; i >=0 ; i--) {
             //减去min是为了优化存储空间,这样得到新的转换值,
             int pos=a[i]-min;
             int sumCount=c[pos];

            System.out.println(a[i]+" 在原数组的排序后的位置是: "+(sumCount-1));

            //把最终生层的排序值,放在新的数组里面返回
            b[sumCount-1]=a[i];
            c[pos]--; //如果有重复元素,位置需要从右向左放置,所以需要把sumCount的值-1

        }


        return b;
    }
```

其中关键的地方有两个:

第一,在于理解计算max和min之后,需要使用原数组每一个元素减去min的转换值统计词频,特定情况下能节省存储空间,这样做的另一个好处是可以兼容负数的情况,因为每一个元素减去最小值之后,结果必定是大于等于0


第二,在于理解为什么采用词频求和的方式+倒序遍历原始数组的方式,能保证排序算法的稳定性


理解了上面的两点,再来看优化后的计数排序就非常简单了,如果想证明计数排序的稳定性,可以参考我的github上的例子。

https://github.com/qindongliang/Java-Note


总结:

经典的计数排序分四个阶段:

1,找出数组里面的最大值和最小值

2,求出每个元素出现的词频(count)

3,遍历词频数组求和

4,反向遍历原始数组,进行目标数组填充,填充后的数组再遍历就是有序的。



如果不考虑算法的稳定性和负数情况及特定情况的浪费空间,那么只需要前面的2步就可以了,如果想要保证稳定性,那么需要经过这4步计算。具体证明计数排序的稳定性的例子,可以参考我的github上例子:

https://github.com/qindongliang/Java-Note/blob/master/src/main/java/sort_algorithm/count_sort/ProveStableCountingSort.java


计数排序在特定的情况下,排序效率极高,但是如果排序的计数空间范围过大,而实际元素个数非常小的情况,效率就会非常差,比如,我只有3个元素,3,1,500000,这样的情况其实是不适合用计数排序的,这一点需要注意。










0
0
分享到:
评论

相关推荐

    用C实现了排序算法中的插入排序、选择排序、冒泡排序、希尔排序、归并排序、快速排序、桶排序、基数排序

    在这份文档中,我用C语言实现了排序算法的多种方法,包括插入排序、选择排序、...排序算法是计算机科学中非常重要的基础知识,深入研究和理解这些算法的原理和实现,将有助于我们更好地解决实际问题和提升编程能力。

    十大经典排序算法,附带动画演示效果,超硬核

    在学习过程中,学生将能够掌握各种排序算法的基本原理和实现方法,提高编程能力和算法分析能力。此外,本教程还将介绍各种实际应用场景,帮助学生将所学知识应用于解决实际问题。 通过本教程的学习,学生将能够深入...

    sorting-visualiser:使用Tkinter Python GUI可视化表示排序算法,例如冒泡排序,快速排序,合并排序,选择排序,插入排序,计数排序和基数排序

    排序可视化器 我们知道排序算法,例如冒泡排序,选择排序,插入... 这就是为什么我们进行此项目的目的是让每个人都了解这些算法的工作原理,并且通过该项目,您还将对此类排序算法有深入的了解。 Python GUI:Tkinter

    算法导论(part1)

    8.2 计数排序 8.3 基数排序 8.4 桶排序 第9章 中位数和顺序统计学 9.1 最小值和最大值 9.2 以期望线性时间做选择 9.3 最坏情况线性时间的选择 第三部分 数据结构 引言 第10章 基本数据结构 10.1 栈...

    算法导论(part2)

    8.2 计数排序 8.3 基数排序 8.4 桶排序 第9章 中位数和顺序统计学 9.1 最小值和最大值 9.2 以期望线性时间做选择 9.3 最坏情况线性时间的选择 第三部分 数据结构 引言 第10章 基本数据结构 10.1 栈...

    深入理解JVM内存结构及运行原理全套视频加资料.txt

    2019最新深入理解JVM内存结构及运行原理(JVM调优)高级核心课程视频教程下载。JVM是Java知识体系中的重要部分,对JVM底层的了解是每一位Java程序员深入Java技术领域的重要因素。本课程试图通过简单易懂的方式,系统...

    组合数学(第4版) 卢开澄 卢华明

    丰富的实例及理论和实际相结合是本书一大特点,有利于对问题的深入理解。. 本书是计算机系本科生和研究生的教学用书,也可作为数学专业师生的教学参考书。 目录回到顶部↑ 第1章 排列与组合. 1.1 加法法则与乘法...

    最新C++网络编程实践视频教程 陈硕

    实现前要考虑的问题.mkv26.procmon代码解析.mkv27.dummyload实现原理和代码解析.mkv28.procmon性能测试.mkv29.知识扩展和总结.mkv30.功能描述.mkv31.数据结构设计与分析.mkv32.数据结构代码解读.mkv33.网络IO模型与...

    java面试题,180多页,绝对良心制作,欢迎点评,涵盖各种知识点,排版优美,阅读舒心

    【JVM】JAVA编译原理和JVM原理 42 【JVM】Java内存模型 44 【JVM】jvm内存模型 45 主内存与工作内存 45 内存间交互操作 46 重排序 48 【JVM】内存泄漏 49 【JVM】java虚拟机的区域如何划分,每一个区的动能? 49 ...

    Hadoop实战(第2版)

    11.2.1 加载数据技术点67 加载Apache 日志文件11.2.2 过滤和投影技术点68 通过过滤和投影减少数据处理量11.2.3 分组和聚合UDF 技术点69 IP 地址的分组和计数 11.2.4 使用UDF 进行定位技术点70 使用...

    数据挖掘导论 中文完整版

    204 6.2.1 先验原理 205 6.2.2 Apriori算法的频繁项集产生 206 6.2.3 候选的产生与剪枝 208 6.2.4 支持度计数 210 6.2.5 计算复杂度 213 6.3 规则产生 215 6.3.1 基于置信度的剪枝 215 6.3.2 Apriori算法中规则的...

    网络互连_网桥.路由器.交换机和互连协议

    作者以专家的洞察力分析了网络的运作过程和工作机理,并深入到技术背后的概念和原理,帮助读者获得对可用的解决方案的更深理解。本书适用于作为大专院校计算机专业本科生网络课程的教材,也适用于从事网络研究的技术...

    java面试题

    请用java写二叉树算法,实现添加数据形成二叉树功能,并以先序的方式打印出来. 119 84.12. 请写一个java程序实现线程连接池功能? 122 84.13. 编一段代码,实现在控制台输入一组数字后,排序后在控制台输出; 122 ...

    汇编实验报告.doc

    书 一、 目的与要求 通过本门课程的学习与实践,加深对汇编语言程序设计课程的理解与掌握,提高 学生的汇编语言程序设计能力,同时可以加深对计算机工作原理的理解,有助于促进 后续课程的学习。 本课程要求学生针对...

    java常用工具类的使用

    而在Java类库中有一个Arrays类的sort方法已经实现各种数据类型的排序算法。程序员只需要调用该类的方法即可。 代码演示:Arrays实现排序 public static void main(String[] args) { int[] ages={23, 45,12,76,34,...

    TCP_IP详解卷1

    作者用Lawrence Berkeley实验室的tcpdump程序来捕获不同操作系统和TCP/IP实现之间传输的不同分组。对tcpdump输出的研究可以帮助理解不同协议如何工作。 本书适合作为计算机专业学生学习网络的教材和教师参考书。也...

    TCPIP详解卷[1].part04

    作者用Lawrence Berkeley实验室的tcpdump程序来捕获不同操作系统和TCP/IP实现之间传输的不同分组。对tcpdump输出的研究可以帮助理解不同协议如何工作。 本书适合作为计算机专业学生学习网络的教材和教师参考书。也...

    TCPIP详解卷[1].part09

    作者用Lawrence Berkeley实验室的tcpdump程序来捕获不同操作系统和TCP/IP实现之间传输的不同分组。对tcpdump输出的研究可以帮助理解不同协议如何工作。 本书适合作为计算机专业学生学习网络的教材和教师参考书。也...

    TCPIP详解卷[1].part03

    作者用Lawrence Berkeley实验室的tcpdump程序来捕获不同操作系统和TCP/IP实现之间传输的不同分组。对tcpdump输出的研究可以帮助理解不同协议如何工作。 本书适合作为计算机专业学生学习网络的教材和教师参考书。也...

    TCPIP详解卷[1].part05

    作者用Lawrence Berkeley实验室的tcpdump程序来捕获不同操作系统和TCP/IP实现之间传输的不同分组。对tcpdump输出的研究可以帮助理解不同协议如何工作。 本书适合作为计算机专业学生学习网络的教材和教师参考书。也...

Global site tag (gtag.js) - Google Analytics