当您的排序任务涉及大量小整数—— 如分级、 年龄或绝对代码—— 经典的比较算法, 如QuickSort 或 合并Sort , 可能感觉像过度 kill。 这些算法在 O( n log n) 时间运行, 但如果可能的值范围有限, 您可以用 [ [FLT: 0]] 的线性 O(n + k) 时间排序 。 这种非比较排序算法可以计算出点数而不是比较元素, 提供一种既简单又快速的、 适合输入的稳定类型 。

如何计算排序工作

计算排序会利用从小范围中得出的输入值是整数的知识。它不进行对等比较,而是建立数值的频率直方图,然后利用直方图将每个元素置于正确的排序位置。

基本方针:直接重建

最简单的"计数排序"在两段路段中工作:

  1. 算法频率 –通过输入数组进行排列,并针对您看到的每个值增加一个计数器.
  2. 覆盖输入 – 穿过计数器阵列从最小到最大,并且每个值都以计数的倍数回写到输入阵列中.

生成一个排序的输出, 但不会[ [FLT: 0] [FLT: 1] 保存重复的相对顺序( 它不稳定 ) 。 当您在按键上排序时, 稳定很重要, 而同时保持记录的原始顺序与按键相同 。 接下来描述的稳定变体是实践中最常用的变体 。

稳定变量:累积数

为了稳定计算,我们增加第三个通行证:

  1. 数频率如前.
  2. 将频率数组转换成累积数组。在此步骤之后, 持有元素 {i
  3. 将输入数组反向排列( 从最后一个元素到第一个元素)。 对于每个元素, 使用其累积数来查找其在输出数组中的位置, 将其放置, 并减少数值 。

因为我们反向穿行,等元的相对顺序被保留下来。输出阵列与输入是分开的,因此这个版本使用O(n)输出的额外空间,而基本版本可以通过覆盖输入来排序位置.

C中执行计数排序#

以下为两个 C# 执行: 基本的就地版本( 对于不需要稳定性的情景) 和 使用辅助阵列的稳定版本 。 两者都需要提前知道最大值 。

基本( 非稳定) 计数排序

此变体直接排序输入数组, 但没有额外的输出缓冲器。 它具有内存效率, 但不稳定 。

public static void CountingSortBasic(int[] array, int maxValue)
{
 int[] counts = new int[maxValue + 1];

 // Count each element's frequency
 for (int i = 0; i < array.Length; i++)
 {
 counts[array[i]]++;
 }

 // Overwrite the original array in sorted order
 int index = 0;
 for (int value = 0; value <= maxValue; value++)
 {
 while (counts[value]-- > 0)
 {
 array[index++] = value;
 }
 }
}

稳定计数排序

稳定版本需要输出与输入大小相同的输出数组,它也使用累积计数来正确定位元素.

public static int[] CountingSortStable(int[] array, int maxValue)
{
 int[] counts = new int[maxValue + 1];
 int[] output = new int[array.Length];

 // Step 1: Count occurrences
 foreach (int num in array)
 {
 counts[num]++;
 }

 // Step 2: Transform counts to cumulative counts
 for (int i = 1; i <= maxValue; i++)
 {
 counts[i] += counts[i - 1];
 }

 // Step 3: Build the output array (iterate input in reverse for stability)
 for (int i = array.Length - 1; i >= 0; i--)
 {
 int value = array[i];
 output[counts[value] - 1] = value;
 counts[value]--;
 }

 return output;
}

在两个执行中,是阵列中出现的最大整数。如果真实的最大值未知,可以使用预览扫描(O(n))进行计算。稳定版本返回新排序的阵列,原数不变。

复杂性分析

n 成为元素的数量和k =最大 — min + 1(可能值的范围).

  • 时间: 计数排序运行于 O(n + k) 时间。计数阶段为O(n),累积前缀为O(k),重建为O(n)。当k是O(n)时,算法是线性。
  • 空间: 基本版本为计数阵列使用O(k)额外空间. 稳定版本使用O(n)+k),因为它也分配输出阵列。 当值值与项目数相比较大时,这使得计数排序不合适 。
  • 与其他类型比较: 比较的--类似QuickSort和合并Sort的类至少需要O(n log n)的比较。对于小 k(例如 k < 10,000和n & gt; 100 000) , 计数排序可以是数量级的更快的排序 。

变化和扩展

处理负整数

计算排序在本地与非负整数配合。 要处理负值, 将整个范围移到最小值变为零。 例如, 如果数字从 - 1 000 到 1000, 以 + 1 000 抵消每个元素。 那么计数数阵列有大小 [ [FLT: 5]] 。

public static int[] CountingSortWithNegative(int[] array)
{
 if (array.Length == 0) return array;

 int min = array.Min();
 int max = array.Max();
 int range = max - min + 1;

 int[] counts = new int[range];
 int[] output = new int[array.Length];

 foreach (int num in array)
 counts[num - min]++;

 for (int i = 1; i < range; i++)
 counts[i] += counts[i - 1];

 for (int i = array.Length - 1; i >= 0; i--)
 {
 int value = array[i];
 output[counts[value - min] - 1] = value;
 counts[value - min]--;
 }

 return output;
}

绘制非整数密钥

计算排序需要整数键。 如果您的数据包含字符( 字节) , 或者可以投到整数的点数, 您仍然可以应用它。 对于较大的对象, 您可以提取整数键并据此排序对象 。 这正是 Radix S排序经常使用计算排序作为内部子例程的方式 。

Radix 排序组合

Radix Sort 处理数字(或比特),当底数(例如 10 或 256)很小时,计算排序是每个通过时的自然选择。这样可以对任意整数进行线性排序,而不仅仅是小整数。

C# 中的实际考虑

内存脚印和大K

最大的陷阱是分配一个大于现有内存的数组。 例如, 排序1,000个元素, 范围为1,000,000 个废物空间。 总是要验证 [[FLT: 0] k [FLT: 1] 的量级不会大于[[FLT: 2] n —— 否则使用比较排序或混合处理方式。

并行性与 Span< T> 等

对于极其大的数组,您可以通过将输入分解为线程来平行计算相。每个线程将它的片段算入一个私有数组,然后将部分结果进行汇总。使用 和 来计算数组可以在范围小时减少堆积分配。

边缘案件

  • Empty 数组 – 立即返回.
  • 单元素[] –排序是微不足道的.
  • 所有相同的值 – 计数数阵列有一个非%零的条目;重建运行于O(n).
  • 范围较大但数据稀少 — — 计数排序因大多数计数条目为零而变得效率低下。 考虑基于hash的计数方法或Bucket排序。

业绩建议

当您知道输入整数会降入一个小范围(例如,等级0–100,年龄0–120,或错误代码0–255)时,使用“计算排序 ” 。 对于更大的范围,请考虑Radix Sort或一个可返回QuickSort的混合键,用于高距离分区。

何时使用计数排序( 和当不计时)

SituationRecommendation
Small integer range (k ~ n)Excellent choice – linear time, simple code.
Large integer range (k >> n)Avoid – memory waste and O(k) overhead.
Need stabilityUse the stable variant (cumulative counts).
Strings or objectsConsider Radix Sort or a comparison sort.
Extremely large datasetsCounting Sort can be parallelized; but watch memory.

基准和业绩

在n=1,000,000和k=1,000的典型基准中,计算排序在(使用内向计数法)所花费时间的20-30%左右完成。差距随着k的减少而扩大。下面是一个大致的比较(现代CPU的执行时间与.NET 8):

n = 1,000,000 | k = 1,000
Array.Sort (QuickSort variant) : 68 ms
CountingSortBasic : 12 ms
CountingSortStable : 18 ms

当范围扩大至10,000时,计数Sort仍然获胜,但边距缩小。对于 k = 100,000,内存的超高(QQ 400 KB for the counting range)开始伤害CPU缓存,性能可以降解.

结论

计算排序是一种欺骗性的简单算法,在数据符合其约束时提供线性性能。对于处理大量小整数的 C# 开发者来说, 这是一种有价值的工具, 可以大大缩短排序时间。 注意数据的范围: 如果数据数量小而且已知, 计算排序将比基于比较的“ ” 选项快得多。 对于更通用的“ ” 分类, 请使用“ 已建的” [ [FLT: 11]] , 但当数字直线和直线排列时, 总是可以随时在计算排序中下降 。

欲进一步阅读,请参看维基百科关于计数排序的文章, Array.Sort[上的微软文档,以及GeeksforGeeks的实用指南.