Table of Contents
计数排序介绍
计数排序是一种非比较性的排序算法,在排序整数超过一个小的已知范围时会非常出色。与比较的排序不同,例如依赖对偶元素比较的Quicksort或兼并类型,计数排序通过计算每个不同值的频率来决定排序顺序。这种方法在有利的条件下产生线性时间复杂性,使其成为许多对输入域有限的性能至关重要的应用程序的选用。
该算法最早由Harold H. Seward于1954年描述,仍然是计算机科学中的一种基础技术,它的简单高效使它对于诸如排序学生年龄,成绩,或任何分布适度的整数数据等任务来说是理想的. Counting Sort通过利用与值范围成比例的辅助存储,避免了O(n log n)的下限比较排序,在k是输入值范围的地方实现O(n + k)时间.
如何计算排序工作
Counting Sort的核心机制是直截了当的:它计算每个值在输入数组中出现多少次,然后使用该计数计算每个元素的最终位置。这一过程由三个不同的阶段组成:
- 算 创建大小 k(输入值范围) 的数组,初始化为零。通过输入数组进行斜体化,并递增每个值的数组。
- 计算前缀: 将计数数数组转换成前缀总和数组,其中索引一中的每个元素持有的元素累积数小于或等于一的数组,此步骤决定了排序输出中每个不同值的起始位置.
- 定位元素 : [ 将输入数组从右向左(为稳定性)拖动,使用计数数数组在输出数组中找到正确的索引,将元素放置在那里,并减少计数。最后输出是按输入排序的复制件。
算法返回一个新的排序数组, 原数组保持不变。 名为 [[ FLT: 0]] 的变体存在于位置计数数数 [[ [FLT: 1]] , 但很少使用, 因为它会损害稳定性或空间效率 。
步骤示例
考虑对数值在0至8之间的数组进行排序[4,2,8,3,3,1].
- 算法: 计数数数组大小9(0-8) → [0],[0],[1],[1],[1],[0],[0],[1],[1]] (Index 1出现一次,索引2两次,索引3次,索引4次,索引8次.
- 前缀总和: 转换为累积 → [0,1,3,5,6,6,6,6,6]。现在每个值都告诉我们排序输出中该数字的起始位置。
- 输出: 尾端的 Travers 原始阵列:第一元素读为1 → 位置= 计数 [1] - 1 = 0 → 输出 [0]= 1, 递减计数 [1] ; 下一个是 3 → 位置= 计数 [3] - 1 = 4 → 输出 [4] 3, 计数 [3]= 4. 继续,直到所有元素都到位。最后输出: [1,2,2,3,4,8] 。
这个例子说明计算排序如何完全避免比较,完全依靠算术操作.
计算复杂度
时间复杂度
- Best, Everyer, and Worst Case: O(n + k]],其中n是元素数,k是输入值范围。当k相对于n小时,算法以线性时间运行.
- 比较比较类型: 快速和合并组合有 O(n log n) 平均复杂度。对于 n = 106 和 k = 1000,计算排序(QQ 1,001,000 操作)比典型的 O(n log n) 排序快约13倍。
空间复杂度
- 初级: O(k) 表示计数数阵列,加上O(n)表示输出阵列。如果k是大,这种内存的超高可以令人望而却步(例如,在k=232处排序32位整数)。
- 稳定变体:需要大小为n的辅助输出阵列;在位变体会牺牲稳定性或使用复杂的指数操纵.
何时使用计数排序
计数排序在以下条件下最为有效:
- 输入由整数(或可映射到一个小整数范围的数据,如字符或离散类)组成.
- k 范围并不明显大于 n。 通常的拇指规则是 k \ O( n) 。
- 内存没有受到严格限制,因为计数阵列和输出缓冲需要额外的空间.
- 稳定性是必需的(例如,用多个键排序). 标准执行在元素从右到左放置时是稳定的.
极好的使用案例包括分级(0–100),年龄(0–120),产品类别(最多几百 SKU),或作为子例在 Radix Sort .
限制和考虑
尽管速度快, 计数排序有限制其应用的缺点:
- 整数只: 它不能直接排序浮点数或字符串,除非它们被转换成一个毗连的整数集.
- 长范围: 如果 k矮小于n——例如,对值在1至107之间的100个数字进行排序——计数数数组在只排序少数元素的同时消耗巨大的内存.
- 不适应: 计数排序总是需要扫描整个输入,构建计数数阵列,即使数据已经排序或接近排序.
- 负值: 标准计数排序假设非负整数。要处理负数,可以通过减去最小值(使最小值为0至最大值 — min)来转移数值。
这些限制意味着计数排序是一种专门的工具,而不是通用算法的通用替代.
与相关排序算法的比较
计算排序对 Radix 排序
Radix Sort通过排序数字从最小到最显著的方式扩展了这个想法,使用每个数字的稳定的排序(通常为计算排序). counting Sort在全程的k上一个传递上工作,而Radix Sort在较小的位数范围(例如基数256)上执行多个传递,减少大k的内存使用. 例如,用计算排序的32位整数需要232个条目的计数数阵列,而用8位数的Radix Sort则需要每通过256个条目,只有4个通过.
计算排序对 Bucket 排序
Bucket Sort将元素分配到多个桶中,并且将每个桶单独排序(通常带有插入排序). Counting Sort可以看作是Bucket Sort的特殊案例,每个桶对应一个单独的值. Bucket Sort在统一分布的浮点数据上效果良好,但 Counting Sort仅限于整数域.
执行稳定计数排序
稳定在按键排序的同时, 保留从另一个键中等元的相对顺序时很重要。 当输出放置循环从右到左通过输入时, 标准计数排序算法是固有的稳定。 以下是稳定变体的文字轮廓 :
- 计算描述的计数数阵列 。
- 转换为前缀总和(在排序输出中每个值的位置).
- 以反序排列输入数组。对于每个元素,将其放在其计数时显示的位置,然后切换该计数时的位置。
因为我们从尾端处理元素, 给定值的最后一次发生会进入尽可能高的索引, 维护相对顺序。 这个稳定的版本对于Radix Sort 在每个位数上正确运行至关重要 。
实用应用
- 教育分级系统: 排序在O(n)时间的上百个考试分数(范围0–100).
- 生物信息学:[] 字母大小小时,排序整数读数或DNA k ⁇ mer频率(A,C,G,T).
- 数据库索引维护: 将唯一整数标识符排序在范围小到足以适应内存.
- 图像处理:在构建查看表时排序直方图的插件或颜色强度(0–255).
- 由二级键吸吸:[]在Radix Sort内部使用,这是许多库和语言中高效排序的工作马(例如.NET运行时间使用包括小范围计数排序在内的算法的适应组合).
更多关于理论和变体,请参考权威参考文献,如 维基百科:计数排序和[ GeeksforGeeks:计数排序[]. 与其他算法的实际比较可以在Brilliant的计数排序文章中找到.
优化大范围的计数排序
当 k 虽大但 n 也大时, 纯数排序会变成内存密集。 存在一些优化 :
- 压缩的稀疏性:[ 当使用值范围大但不同值数量少时,使用散列图而不是毗连数组。这可以交易用于散列管理但减少内存消耗的恒定时索引。
- Hybrid 方法: 将计数排序与其他算法结合。例如,如果范围超过106,则使用Radix排序,其基数保持小数范围。
- 在位置变体:[一些优化将额外的空间缩小到O(k),没有输出阵列,但它们一般会牺牲稳定性或需要周期来定位位置.
结论
计算排序法在数值范围相对于元素数量而言很小时,是分解整数的非常有效的算法。它的O(n + k) 时间复杂度和线性性能使它在诸如等级排序、Radix排序子程序以及带定界整数键的应用程序中不可或缺。然而,算法对整数输入的依赖及其大范围的内存管理提醒我们,没有单一的分类对所有情况都是最理想的。通过理解计算排序法的优异之处—— 以及失败时—— 开发者可以建立更快、更可预测的系统。关于基于非比较法的分解法,请参看 TuriosPoint: 计数排序 Coursera: 计数排序讲座[。