15.百万考生成绩如何排序?计数排序给你好看

百万考生分数如何排序 - 计数排序

关注 「码哥字节」,这里有算法系列、大数据存储系列、Spring 系列、源码架构拆解系列、面试系列……敬请期待。设置星标不迷路

其实计数排序是桶排序的一种特殊情况。 桶排序的核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

「码哥字节」之前分享了百万订单如何根据金额排序,就是运用了桶排序。

计数排序的核心在于将输入的数据值转换成键保存在数组下标,所以作为一种线性时间复杂度的排序,输入的数据必须是有确定且范围不大的整数。比如当要排序的 n 个数据,所处的范围不大的时候,最大值是 m,我们就把数据化划分成 m 个桶。每个桶内的数据都是相同的大小,也就不需要桶内排序,这是与桶排序最大的区别。

场景重现

高考查分数系统,系统会展示我们的成绩以及所在省的排名。假如 H 省有 80 万考生,如何通过成绩快速排序得出排名呢?

再比如统计每个省人口的每个年龄人数并且从小到大排序,又如何实现呢?

考生的满分是 750 分,最小是 0 分,符合我们之前说的条件:数据范围小且是整数。我们可以划分为 751 个桶分别对应分数为 0 ~ 750 分数的考生。

接着开始遍历考生数据,每个考生按照分数则划分到对应数组下标,相同数组的下标则将该下标的数据值 + 1。其实就是每个数组下标位置对应的是数列数据出现的次数,最后直接遍历该数组,输出元素的下标就是对应的分数,下标对应的元素值是多少我们就输出几次。

桶内的数据都是分数相同的考生,所以并不需要再进行排序。我们只需要依次扫描每个桶,将桶内的考生依次输出到一个数组中,就实现了 80 万考生的排序。因为只涉及扫描遍历操作,所以时间复杂度是 O(n)。

计数排序的算法思想就是这么简单,跟桶排序非常类似,只是桶的大小粒度不一样。不过,为什么这个排序算法叫“计数”排序呢?“计数”的含义来自哪里呢?

刚刚所说的是朴素版的排序,只是简单的按照统计数组的下标输出元素值,并没有给原始数列进行排序。

在现实中,给学生排序遇到相同分数的就分不清谁是谁?比如并列 95 分的张无忌与周芷若,却不知道是张无忌哪个是周芷若。带着这些问题,请继续看下面的图解思路…...

图解思路

计数统计

为了方便理解,对数据进行简化,假设只有 8 个考生,分数在 0 ~ 5 之间,所以有 5 个桶对应考生分数,值代表每种分数的考生个数。考生的原始数据我们放在数组 SourceArray[8] = {2,5,3,0,2,3,0,3}。

考生的成绩从 0 到 5,使用 大小数组为 6 的 countArray[6] 表示桶,下标对应分数,值存储的是该分数的考生个数。我们只要遍历一遍原始数据就可以得到 countArray[6]。

分数与个数数组

可以知道,分数为 3 分的学生有 3 个, < 3 分的学生有 4 个,所以成绩 = 3 分的学生在排序后的有序数组 sortedArray[8] 中的下标会在 4, 5, 6 的位置,也就是排名是 5,6,7。如下图所示

image

我们如何计算出每个分数的考生在有序数组对应的存储位置呢?这个思路很巧妙,主要是对之前的 countArray[6] 做一下转换。

划重点了同学们:**我们对 countArray[6] 数组顺序求和,countArray[k] 里面存储的是 ≤ k 分数的考生个数 **。这样加的目的是什么?

其实是让统计数组存储的元素值,等于相应考试成绩数据的最终排序位置的序号。

image

现在我就要讲计数排序中最复杂、最难理解的一部分了,坚持啃下来。


  1. 从后往前遍历原始输入数组 SourceArray[8] = {2,5,3,0,2,3,0,3},扫描成绩为 3 的小强,我们就从 数组 countArray[6] 中取出下标 = 3 的值 = 7,也就意味着包括自己在内,分数 ≤ 3 的考生有 7 位,表示在 sortedArray 中排在第七位,当把小强成绩放到 sortedArray 之后 ≤ 3 的成绩就剩下 6 个了,所以 countArray[3] 要 - 1,变成 6.
  2. 遍历成绩表倒数第二个数据,成绩是 0,找到在 countArray[0] 的元素 = 2,表示排名第二,同时 countArray[0] 的元素值 -1。
  3. 以此类推,当扫描完整个原始数组之后, sortedArray 数据就是按照分数从小到大有序排列了。

代码实战

整个步骤:

  1. 查找数列最大值。
  2. 根据数列最大值确定 countArray 统计数组长度。
  3. 遍历原始数据填充统计数组,统计对应元素的个数。
  4. 统计数组做变形,后面的元素等于前面元素之和。
  5. 倒序遍历原始数组,从统计数组中找到元素的正确排位,输出到结果数组中。

源码详见 GitHub: https://github.com/UniqueDong/algorithms

package com.zero.algorithms.linear.sort;

/**
 * 公众号:码哥字节
 * 计数排序
 */
public class CountingSort {
    public int[] sort(int[] sourceArray) {
        if (sourceArray == null || sourceArray.length <= 1) {
            return new int[0];
        }
        // 1.查找数列最大值
        int max = sourceArray[0];
        for (int value : sourceArray) {
            max = Math.max(max, value);
        }
        // 2.根据数据最大值确定统计数组长度
        int[] countArray = new int[max + 1];
        // 3. 遍历原始数组映射到统计数组中,统计元素的个数
        for (int value : sourceArray) {
            countArray[value]++;
        }
        // 4.统计数组变形,后面的元素等于前面元素之和。目的是定位在结果数组中的排位
        for (int i = 1; i <= max; i++) {
            countArray[i] += countArray[i - 1];
        }
        // 5.倒序遍历原始数组,从统计数组查找对应的正确位置,输出到结果表
        int[] sortedArray = new int[sourceArray.length];
        for (int i = sourceArray.length - 1; i >= 0; i--) {
            int value = sourceArray[i];
            // 分数在 countArray 中的排名, - 1 则是结果数组的下标
            int index = countArray[value] - 1;
            sortedArray[index] = value;
            countArray[value]--;
        }
        return sortedArray;
    }
}

复杂度分析

第 1、3、5 步都涉及遍历原始数组,时间复杂度都是 O(n),第 4 步统计数组变形,时间复杂度是 O(m),所以总体的时间复杂度是 O(3n +m),去掉系数 O(n) 时间复杂度。

空间复杂度,结果数组 O(n)。

优化思路

前面的代码,第一步我们查找最大值,假如原始数据是 {99,98,92,80,88,87,82,88,99,97,92},最大值是 99,最小值是 80,如果直接创建 100 长度的数组,那么 从 0 到 79 的空间全都浪费了。

要怎么解决呢?

跟着 「码哥字节」来优化,很简单,我们不再使用 max + 1 作为统计数组的长度,而是 max - min + 1 作为统计数组的长度即可。

代码如下:

package com.zero.algorithms.linear.sort;

/**
* 公众号:码哥字节
* 计数排序
*/
public class CountingSort {
   public int[] sort(int[] sourceArray) {
       if (sourceArray == null || sourceArray.length <= 1) {
           return new int[0];
       }
       // 1.查找数列最大值,最小值
       int max = sourceArray[0];
       int min = sourceArray[0];
       for (int value : sourceArray) {
           max = Math.max(max, value);
           min = Math.min(min, value);
       }
       int d = max - min;
       // 2.根据数据最大值确定统计数组长度
       int[] countArray = new int[d + 1];
       // 3. 遍历原始数组映射到统计数组中,统计元素的个数
       for (int value : sourceArray) {
           countArray[value - min]++;
       }
       // 4.统计数组变形,后面的元素等于前面元素之和。目的是定位在结果数组中的排位
       for (int i = 1; i < countArray.length; i++) {
           countArray[i] += countArray[i - 1];
       }
       // 5.倒序遍历原始数组,从统计数组查找对应的正确位置,输出到结果表
       int[] sortedArray = new int[sourceArray.length];
       for (int i = sourceArray.length - 1; i >= 0; i--) {
           int value = sourceArray[i];
           // 分数在 countArray 中的排名, - 1 则是结果数组的下标
           int index = countArray[value - min] - 1;
           sortedArray[index] = value;
           countArray[value - min]--;
       }
       return sortedArray;
   }
}

总结一下

计数排序适用于在数据范围不大的场景中,并且只能给非负整数排序,对于其他类型的数据,要排序的话要在不改变相对大小的情况下,转成非负整数。

比如数据范围 [-1000, 1000] ,就对每个数据 +1000,转换成非负整数。

计数排序这么强大,但是局限性主要有如下两点:

  1. 当数列的最大与最小值差距过大,不适合使用计数排序。

比如 20 个随机整数,范围在 0 - 1 亿之间,这时候使用计数排序需要创建长度为 1 亿的数组,严重浪费空间。

  1. 数列元素不是整数,不适合使用

参考资料

极客时间 《数据结构与算法之美》

漫画算法-小灰的算法之旅

关注 「码哥字节」后台回复加群,进入专属技术。

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

推荐阅读更多精彩内容