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下载地址

完!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容