Menu
CoddyTech

计数排序

最后更新

计数排序是一种非比较排序,适用于取值范围已知且有限的整数。它统计每个值出现了多少次,再根据这些计数把每个值直接写到排序后的位置,完全不需要比较。点击上方的“播放”,观察数值如何被计数,然后按顺序放回去。

计数排序的运行时间是 O(n + k),其中 k 是输入值的范围。当 k 不比 n 大太多时,它非常快,可以胜过 O(n log n) 的比较排序;但如果取值范围很大,O(k) 的计数数组就会让它变得不切实际。

时间和空间复杂度

情况复杂度说明
时间O(n + k)n 个元素,k = 取值范围
空间O(n + k)计数数组加输出数组
稳定性稳定借助前缀和从右向左放置时稳定
是否比较?否靠计数排序,而不是比较
最适合整数范围较小k 接近 n

分步说明

步骤发生了什么
1找出最大值,以确定计数数组的大小。
2统计每个值出现的次数。
3(可选)把计数转换为前缀和,以保证稳定性。
4按每个值出现的次数,把它写入输出数组。
5输出数组现在已完全有序。

示例演算

对 [1, 4, 1, 2, 4] 排序(取值范围为 0 到 4,所以计数数组有 5 个位置):

轮次状态操作
扫描输入count = [0, 2, 1, 0, 2]统计出现次数:1 出现两次,2 出现一次,4 出现两次。
前缀和count = [0, 2, 3, 3, 5]现在每个位置存的是 <= 该下标的值有多少个,由此得到最终位置。
放置 4output = [_, _, _, _, 4]从右向左读取:count[4] = 5,所以 4 放到下标 4;计数减到 4。
放置 2output = [_, _, 2, _, 4]count[2] = 3,所以 2 放到下标 2;计数减到 2。
放置 1output = [_, 1, 2, _, 4]count[1] = 2,所以 1 放到下标 1;计数减到 1。
放置 4output = [_, 1, 2, 4, 4]count[4] = 4,所以这个 4 放到下标 3;计数减到 3。
放置 1output = [1, 1, 2, 4, 4]count[1] = 1,所以这个 1 放到下标 0。数组已有序。

什么时候使用计数排序

适合使用的情况不适合使用的情况
要排序的是取值范围小且已知的整数(或可映射为整数的键)。取值范围 k 远大于元素个数 n。
需要线性的 O(n + k) 时间,并且承担得起额外的数组。内存紧张,无论 n 多大,计数数组都要占用 O(k)。
需要一个稳定排序作为子过程(例如在基数排序中)。键是浮点数、字符串或无法映射为整数的任意对象。
最大值有上限,并且可以预先低成本地算出来。取值范围未知或没有上限,无法确定计数数组的大小。

Counting Sort 代码

简洁、可运行的 Counting Sort 实现,提供 Python, JavaScript, Java, C++, C 版本。选择一种语言,复制代码,或在 Coddy 在线编辑器中直接打开。

Counting Sort 的 Python 代码

Python
1def counting_sort(a):2    # Works for non-negative integers with a small max value3    counts = [0] * (max(a) + 1)4    for value in a:5        counts[value] += 16    # Prefix sums turn counts into final positions7    for i in range(1, len(counts)):8        counts[i] += counts[i - 1]9    out = [0] * len(a)10    for value in reversed(a):  # reversed keeps equal values stable11        counts[value] -= 112        out[counts[value]] = value13    return out14
15
16nums = [4, 2, 2, 8, 3, 3, 1]17print("Before:", nums)18print("After: ", counting_sort(nums))
在 Python 在线编辑器中运行这段代码

计数排序常见问题

计数排序的时间复杂度是多少?
计数排序是 O(n + k),其中 n 是元素个数,k 是可能取值的范围。当 k = O(n) 时,这就是线性时间。它占用 O(n + k) 的额外空间。
计数排序是稳定的吗?

可以是。稳定版本先对计数求前缀和,再从右向左放置元素,这样能保持相等键的相对顺序。这里演示的简单“按值重写”版本也能正确排序,但主要用于普通整数。

什么时候该用计数排序?
当要排序的是取值范围小且已知的整数(或可映射为整数的键)时使用。如果取值范围 k 远大于元素个数,计数数组会浪费内存,这时比较排序更合适。
计数排序和基数排序有什么区别?
计数排序一次按整个值排序,需要一个和取值范围一样大的计数数组。基数排序逐位排序,通常对每一位调用一次稳定的计数排序,这样每轮的取值范围都很小(例如十进制数字只有 10 个)。对于单次计数排序处理不了的大取值范围,基数排序也能应付。
为什么计数排序不总是比快速排序快?
计数排序是 O(n + k),所以只有当取值范围 k 和 n 相当时它才占优。如果 k 非常大,比如对范围在 0 到 1,000,000,000 之间的 100 个值排序,O(k) 的计数数组就会占据主导并浪费内存,而快速排序这类 O(n log n) 的比较排序依然又快又省空间。
计数排序能处理负数吗?
能,加一个小偏移量即可。不要直接用值作为计数数组的下标,而是用 value - min,让最小值映射到下标 0。计数数组的大小就变成 max - min + 1。忘记这个偏移量是一个常见的 bug,遇到负数输入就会崩溃。
Coddy 编程语言插图

在 Coddy 掌握算法

开始