08-计数排序(Counting Sort)

计数排序(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))