【算法图解】计数排序:不比较也能排序

计数排序用“统计次数”代替比较,能在特定条件下做到 O(n+k) 的线性时间。本文讲清楚适用前提、稳定实现、空间代价与工程注意点。

14 分钟阅读
小明

【算法图解】计数排序:不比较也能排序

你学过的冒泡/快排/归并都属于“比较排序”:

  • 通过比较两个元素大小来决定顺序

比较排序有一个理论下界:

  • 最好也得 $\Omega(n\log n)$(比较模型下)

计数排序很特别:它不走比较模型。

它的思路是:

既然值域有限,我就把每个值出现了多少次数出来。

这样就能在条件满足时做到线性级别的时间。


1. 计数排序的适用条件

计数排序不是万能。

它适合:

  • 数据是整数
  • 值域范围不大(例如 0~100000)
  • 或者可以接受开一个大小为 $k$ 的计数数组

其中:

  • $n$ 是元素数量
  • $k$ 是值域大小

复杂度通常是:

  • 时间:$O(n + k)$
  • 空间:$O(k)$

如果 $k \gg n$,计数排序就不划算。


2. 最简单版本:只排序不求稳定

function countingSort(arr, maxValue) {
  const count = new Array(maxValue + 1).fill(0)
  for (const x of arr) count[x]++

  const result = []
  for (let v = 0; v <= maxValue; v++) {
    for (let i = 0; i < count[v]; i++) result.push(v)
  }

  return result
}

这个版本能排序,但不保证稳定性(如果元素带“附加信息”,相等值的相对顺序无法保证)。


3. 稳定计数排序:前缀和 + 逆序回填

稳定计数排序的经典写法是:

  1. 统计 count
  2. 做前缀和,让 count[v] 表示“<= v 的元素数量”
  3. 从右到左扫描原数组,把元素放到正确位置
function countingSortStable(arr, maxValue) {
  const count = new Array(maxValue + 1).fill(0)
  for (const x of arr) count[x]++

  for (let v = 1; v <= maxValue; v++) {
    count[v] += count[v - 1]
  }

  const out = new Array(arr.length)
  for (let i = arr.length - 1; i >= 0; i--) {
    const x = arr[i]
    count[x]--
    out[count[x]] = x
  }

  return out
}

为什么要“逆序回填”?

  • 保证相同值时,右边的先占后面的坑,左边的会落在更前面,从而保持相对顺序。

4. 工程注意点

  • 需要知道 maxValue(或先扫描一遍找最大值)
  • 数据可能有负数:需要偏移(offset)
  • 值域过大要谨慎:空间会炸

5. 面试回答模板

  • 计数排序适用于整数且值域不大的情况。
  • 通过统计出现次数(以及前缀和定位位置)在 $O(n+k)$ 时间内排序。
  • 空间是 $O(k)$,当 $k$ 太大时不划算。

总结

计数排序的核心是:

  • 用空间换时间
  • 在值域可控时跳出比较排序下界