Table of Contents
当您的排序任务涉及大量小整数—— 如分级、 年龄或绝对代码—— 经典的比较算法, 如QuickSort 或 合并Sort , 可能感觉像过度 kill。 这些算法在 O( n log n) 时间运行, 但如果可能的值范围有限, 您可以用 [ [FLT: 0]] 的线性 O(n + k) 时间排序 。 这种非比较排序算法可以计算出点数而不是比较元素, 提供一种既简单又快速的、 适合输入的稳定类型 。
如何计算排序工作
计算排序会利用从小范围中得出的输入值是整数的知识。它不进行对等比较,而是建立数值的频率直方图,然后利用直方图将每个元素置于正确的排序位置。
基本方针:直接重建
最简单的"计数排序"在两段路段中工作:
- 算法频率 –通过输入数组进行排列,并针对您看到的每个值增加一个计数器.
- 覆盖输入 – 穿过计数器阵列从最小到最大,并且每个值都以计数的倍数回写到输入阵列中.
生成一个排序的输出, 但不会[ [FLT: 0] [FLT: 1] 保存重复的相对顺序( 它不稳定 ) 。 当您在按键上排序时, 稳定很重要, 而同时保持记录的原始顺序与按键相同 。 接下来描述的稳定变体是实践中最常用的变体 。
稳定变量:累积数
为了稳定计算,我们增加第三个通行证:
- 数频率如前.
- 将频率数组转换成累积数组。在此步骤之后, 持有元素 {i 。
- 将输入数组反向排列( 从最后一个元素到第一个元素)。 对于每个元素, 使用其累积数来查找其在输出数组中的位置, 将其放置, 并减少数值 。
因为我们反向穿行,等元的相对顺序被保留下来。输出阵列与输入是分开的,因此这个版本使用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的混合键,用于高距离分区。
何时使用计数排序( 和当不计时)
| Situation | Recommendation |
|---|---|
| Small integer range (k ~ n) | Excellent choice – linear time, simple code. |
| Large integer range (k >> n) | Avoid – memory waste and O(k) overhead. |
| Need stability | Use the stable variant (cumulative counts). |
| Strings or objects | Consider Radix Sort or a comparison sort. |
| Extremely large datasets | Counting 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的实用指南.