美文网首页
08-计数排序(Counting Sort)

08-计数排序(Counting Sort)

作者: ducktobey | 来源:发表于2019-12-04 23:07 被阅读0次

    计数排序(Counting Sort)

    本节内容,继续介绍排序算法,在本节内容之前,介绍过7种排序算法,那计数排序算法,对比前面的几种排序算法, 有没有不一样呢?请继续往下看。

    • 前面介绍的冒泡,选择,插入,归并,快速,希尔,堆排序,都是基于比较的排序,这些基于比较的排序,有以下几个特点
      • 平均时间复杂度最低的是O(nlogn)
    • 而本节内容介绍的计数排序,不是基于比较的排序。其中不基于比较的排序还有桶排序,基数排序等
      • 它们是典型的用空间换时间,在某些时候,平均时间复杂度可以比O(nlogn)更低,也就是说,在某些时候,这种利用空间换时间的排序算法,性能比前面基于比较的排序算法更快

    计数排序是在1954年由Harold H.Seward提出,适合对一定范围内的整数进行排序。后面会讲为什么要这样规定

    计数排序核心思想

    统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引

    计数排序简单实现

    例如将下图的这一堆整数,进行计数排序

    然后利用一个数组,对应元素作为数组的索引,然后来记录每一个元素出现的次数,所以上图中的序列,对应到数组中出现的次数,可以用下图来进行表示

    3出现1次,4出现1次,5出现2次,6出现1次,7出现2次,8出现1次。通过这样统计,每一个整数出现的次数都记录下来了。并且利用这种方式来存储每个元素出现的次数,则数组的大小取决于序列中最大的元素。这种设计虽然有很大的缺陷,但是却可以完成排序,并且在后面会对这种方式进行优化。

    那看到这里,你可能会想,知道这些元素出现的次数,有什么用呢,就能完成排序了吗?是的,知道元素出现的次数后,就可以推断出元素在有序序列中对应的位置。接下来就是对索引数组进行扫描,从0位置开始扫描,依次将出现次数不为0的索引,按照出现次数进行依次排序,最终就可以将序列排好序。所以统计完成后的序列为

    接下来,可以利用这种思路,尝试将算法进行实现。

    protected void sort() {
        //找出最大值
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) {
                max = array[i];
            }
        }
        //开辟内存空间,存储每个整数出现的次数
        int[] counts = new int[max + 1];
        //统计每个整数出现的次数
        for (int i = 0; i < array.length; i++) {
            counts[array[i]]++;
        }
        //根据整数出现次数,对整数进行排序
        int index = 0;
        for (int i = 0; i < counts.length; i++) {
            if (counts[i] > 0) {
                while (counts[i]-- > 0) {
                    array[index++] = i;
                }
            }
        }
    }
    

    写出了这个简单计数排序后,来观察以下现在这个算法存在哪些问题

    1. 无法对负整数进行排序
    2. 极其浪费内存空间
    3. 是一个不稳定排序
    4. ...

    接下来,就可以对上面的算法进行改进了。那应该如何改进呢?首先,我觉得应该从内存空间开始

    由于用来计数的数组,是根据序列中的最大值来申请的,所以当如果像上面例子中,最小值都比较大时,数组前面就会空出很大的空间,造成空间的浪费,所以可以利用最大值 - 最小值 + 1的值来创建用来计数的数组。

    改进

    下图表示要进行排序的序列

    根据上面的内存优化思路,则可以通过下图的这种方式来存储数据

    由于现在优化后,不是使用索引直接与元素值进行对应,所以巧妙的将不能对负整数进行排序的问题也解决了,因为现在是将索引统一减去了最小值的大小,所以,也可以存储负整数了。

    那如何才能办到,变为一个稳定的排序呢?那可以进行另外一个优化,以前,在数组中存储的是每一个整数出现的次数,现在将存储的值改为每次出现的次数累加加上前面的所有次数,这样得到的就是元素在有序序列中的位置信息[下图]

    次数解释:

    1. 例如现在的元素8,次数是8,这个值是累加前面的所有次数 + 自己出现的次数,即1 + 1 + 2 +1 + 2 +1 = 7 + 1 = 8,所以在8前面有7个元素,并且8出现了1次,索引为7
    2. 例如7现在的次数是7,这个字是通过前面的次数1 + 1 + 2 + 1 + 2 = 5 + 2 = 7,所以在7前面有5个元素,7出现了2次,索引为5/6

    所以上面每个元素的索引是

    元素 3 4 5 6 7 8
    索引次数 1 2 4 5 7 8
    出现次数 1 1 2 1 2 1
    真正索引 1 - 1 = 0 2 - 1 = 1 4 - 2 = 2/3 5 - 1 = 4 7 - 2 = 5/6 8 - 1 = 7

    所以,根据最终的索引排序后的结果为

    根据上面的的优化,可以得出下面的结论

    • 假设数组中的最小值是min
      • 数组中的元素k对应在counts数组中的索引为k - min
      • 数组中的元素k在有序序列中的索引
        • counts[k - min] - p(p表示倒数第几个k)

    有这个结论后,可以根据结论计算

    • 元素8在有序序列中的索引,counts[8 - 3] - 1 结果为7
    • 第一个元素7在有序序列中的索引为:counts[7 - 3] - 1,结果为6
    • 第二个元素7在有序序列中的索引为:counts[7 - 3] - 2,结果为5

    然后,再对原来的数组进行遍历,并且创建一个容量相等了新数组,根据原来数组中的元素,直接去counts数组中去获取真正的索引。这样就能直接得到新数组对应的位置,并且可以保证是稳定排序算法。

    根据上面的分析,最终得到的代码如下

    protected void sort() {
            // 找出最值
            int max = array[0];
            int min = array[0];
            for (int i = 1; i < array.length; i++) {
                if (array[i] > max) {
                    max = array[i];
                }
                if (array[i] < min) {
                    min = array[i];
                }
            }
            
            // 开辟内存空间,存储次数
            int[] counts = new int[max - min + 1];
            // 统计每个整数出现的次数
            for (int i = 0; i < array.length; i++) {
                counts[array[i] - min]++;
            }
            // 累加次数
            for (int i = 1; i < counts.length; i++) {
                counts[i] += counts[i - 1];
            }
            
            // 从后往前遍历元素,将它放到有序数组中的合适位置
            int[] newArray = new int[array.length];
            for (int i = array.length - 1; i >= 0; i--) {
                newArray[--counts[array[i] - min]] = array[i];
            }
            
            // 将有序数组赋值到array
            for (int i = 0; i < newArray.length; i++) {
                array[i] = newArray[i];
            }
        }
    

    这样就将整个排序算法,进行了实现。现在与前面的几种算法进行比较,得到的结果如下

    从结果来看,可以看到,计数排序的耗时表现非常优秀

    复杂度分析

    空间复杂度:O(n + k)

    最好,最坏,平均时间复杂度:O(n + k)

    k是整数的取值范围

    属于稳定的排序算法

    对自定义对象进行排序

    如果自定义对象可以提供用以排序的整数类型,依然可以使用技术排序

    demo下载地址

    完!

    相关文章

      网友评论

          本文标题:08-计数排序(Counting Sort)

          本文链接:https://www.haomeiwen.com/subject/fhergctx.html