计数排序
最后更新
计数排序是一种非比较排序,适用于取值范围已知且有限的整数。它统计每个值出现了多少次,再根据这些计数把每个值直接写到排序后的位置,完全不需要比较。点击上方的“播放”,观察数值如何被计数,然后按顺序放回去。
计数排序的运行时间是 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] | 现在每个位置存的是 <= 该下标的值有多少个,由此得到最终位置。 |
放置 4 | output = [_, _, _, _, 4] | 从右向左读取:count[4] = 5,所以 4 放到下标 4;计数减到 4。 |
放置 2 | output = [_, _, 2, _, 4] | count[2] = 3,所以 2 放到下标 2;计数减到 2。 |
放置 1 | output = [_, 1, 2, _, 4] | count[1] = 2,所以 1 放到下标 1;计数减到 1。 |
放置 4 | output = [_, 1, 2, 4, 4] | count[4] = 4,所以这个 4 放到下标 3;计数减到 3。 |
放置 1 | output = [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))Counting Sort 的 JavaScript 代码
JavaScript
1function countingSort(arr) {2 // Count occurrences of each value, then rebuild in order3 const max = Math.max(...arr);4 const count = new Array(max + 1).fill(0);5 for (const x of arr) count[x]++;6 const out = [];7 count.forEach((c, value) => {8 for (let k = 0; k < c; k++) out.push(value);9 });10 return out;11}12
13const data = [4, 2, 9, 2, 7, 4, 1, 4];14console.log("Before:", data);15console.log("Sorted:", countingSort(data));Counting Sort 的 Java 代码
Java
1import java.util.Arrays;2
3public class Main {4 static int[] countingSort(int[] arr) {5 int max = 0;6 for (int v : arr) max = Math.max(max, v);7 int[] count = new int[max + 1];8 for (int v : arr) count[v]++;9 // Prefix sums turn counts into final positions10 for (int i = 1; i <= max; i++) count[i] += count[i - 1];11 int[] out = new int[arr.length];12 // Walk backwards so equal values keep their order (stable)13 for (int i = arr.length - 1; i >= 0; i--) {14 out[--count[arr[i]]] = arr[i];15 }16 return out;17 }18
19 public static void main(String[] args) {20 int[] arr = {4, 2, 2, 8, 3, 3, 1};21 System.out.println("Before: " + Arrays.toString(arr));22 int[] sorted = countingSort(arr);23 System.out.println("After: " + Arrays.toString(sorted));24 }25}Counting Sort 的 C++ 代码
C++
1#include <algorithm>2#include <iostream>3#include <vector>4
5void printVec(const std::vector<int>& a) {6 for (int x : a) std::cout << x << " ";7 std::cout << "\n";8}9
10void countingSort(std::vector<int>& a) {11 if (a.empty()) return;12 int maxVal = *std::max_element(a.begin(), a.end());13 // count[v] = how many times v appears14 std::vector<int> count(maxVal + 1, 0);15 for (int x : a) ++count[x];16 // Rebuild the array from the counts17 size_t idx = 0;18 for (int v = 0; v <= maxVal; ++v) {19 while (count[v]-- > 0) a[idx++] = v;20 }21}22
23int main() {24 std::vector<int> data = {4, 2, 2, 8, 3, 3, 1, 7};25 std::cout << "Before: ";26 printVec(data);27 countingSort(data);28 std::cout << "After: ";29 printVec(data);30 return 0;31}Counting Sort 的 C 代码
C
1#include <stdio.h>2#include <stdlib.h>3
4void printArr(const int a[], int n) {5 for (int i = 0; i < n; i++) printf("%d ", a[i]);6 printf("\n");7}8
9void countingSort(int a[], int n) {10 int maxVal = a[0];11 for (int i = 1; i < n; i++) {12 if (a[i] > maxVal) maxVal = a[i];13 }14 // count[v] = how many times v appears15 int* count = calloc(maxVal + 1, sizeof(int));16 for (int i = 0; i < n; i++) count[a[i]]++;17 // Rebuild the array from the counts18 int idx = 0;19 for (int v = 0; v <= maxVal; v++) {20 while (count[v]-- > 0) a[idx++] = v;21 }22 free(count);23}24
25int main(void) {26 int data[] = {4, 2, 2, 8, 3, 3, 1, 7};27 int n = sizeof(data) / sizeof(data[0]);28 printf("Before: ");29 printArr(data, n);30 countingSort(data, n);31 printf("After: ");32 printArr(data, n);33 return 0;34}计数排序常见问题
计数排序的时间复杂度是多少?
计数排序是
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,遇到负数输入就会崩溃。