C++算法之计数排序
一、算法描述
给定长度为n的序列,假设已知序列元素的范围都是[0..K]中的整数,并且K的范围比较小(例如10^6,开长度为10^6左右的int类型数组所占用的内存空间只有不到4M)。解决该问题的计数排序算法描述如下:
- 使用整数数组
cnt统计[1..K]范围内所有数字在序列中出现的个数。 - 使用变量
i枚举1到K,如果i出现了cnt[i]次,那么在答案序列的末尾添加cnt[i]个i。
下图是一个n=6, K=3的例子:

值得一提的是,如果元素的范围可以被很容易转换到[0..K],我们也可以使用计数排序。如果元素范围是[A..B],我们可以通过简单的平移关系将其对应到[0..B-A]上。或者所有数值均为绝对值不超过100的两位小数,那么我们可以通过将所有数字放大100倍将其转换为整数。
找出原序列中元素在答案中的位置
在有些场景中,比如我们根据(key, value)中的key关键字进行排序,如果只是使用上面的计数排序,我们无法将value放到相应的key在答案序列中的对应位置中。但是,如果我们可以将原序列和答案序列元素的位置对应求出来,那么这个问题就能得到解决。

试想,对于原序列中的数字x,它排序后的位置可能出现在哪里呢?
因为在排序后的序列中,假设x第一次出现的位置是i,最后一次出现的位置是j,那么i之前的元素一定比x小,j出现的位置之后的元素一定比x大。假设原序列中<x元素的个数是A,≤x的元素个数是B,那么x可能出现的位置一定是[(A+1)..B]!
sum数组的求法和意义
那么,我们怎样求出A和B呢?假设我们对cnt数组求前缀和,如下图所示,cnt数组元素求前缀和后为sum数组:

这里,我们指出sum数组的意义:对于一个序列中可能出现的值x,sum[x]的含义是“小于等于x的数字个数”,同时,也可以看作指向答案序列中最后一个x出现的位置的指针。
利用sum数组分配位置
所以对于值x,A即为sum[x - 1],B即为sum[x],x出现的排名为(sum[x - 1] + 1)..sum[x]],等价于[(sum[x] - cnt[x] + 1)..sum[x]]。我们将sum数组的位置标出来:

然后我们从后往前扫描每个元素,把它填到当前的sum对应值指向的格子中,并把sum向前移动。如下图:

有了原序列和答案序列的位置对应,我们也可以据此将对应元素放入答案数组中。所以该版本的计数排序算法描述如下:
- 统计原序列中每个值的出现次数,记为
cnt数组。 - 从小到大枚举值的范围,对cnt数组求前缀和,记为
sum数组。 - 从后往前枚举每个元素
a[i],分配其在答案中的位置idx[i]为当前的sum[a[i]],也就是将其放在所有值等于a[i]中的最后一个。并且将sum[a[i]]减少1,保证下次再遍历到同样的值时,它分配的位置正好在idx[i]前面一个。
二、代码实现
代码如下(示例):
#include <bits/stdc++.h>
#define N 1000005
#define K 1000001 // 假设非负整数最大元素范围为1000000
using namespace std;
int a[N], n, b[N];
int cnt[K];
int main() {
// 输入
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
++cnt[a[i]]; // 这里通过计数数组cnt来维护每一种值出现的次数
}
// 维护最终有序序列
for (int i = 0, j = 0; i < K; ++i) // 枚举每一种值i,指针j用来枚举填充答案数组中的位置
for (int k = 1; k <= cnt[i]; ++k) // 根据该值出现的次数
b[++j] = i; // 添加对应个数的i到答案序列
// 输出
for (int i = 1; i <= n; ++i)
cout << b[i] << ' ';
cout << endl;
return 0;
}
三、复杂度分析
空间复杂度
因为在上面的代码中一共开了3个数组,长度分别为O(N)(对于a和b)和O(K)(对于cnt)。整个空间复杂度为O(N + K)。
时间复杂度
容易发现,算法的输入输出部分所占时间复杂度为O(n)。
在“维护有序序列”的部分,我们首先考虑最外层循环,因为它遍历了所有[0..K]的数字,所以它的复杂度是O(K)。
其次,我们考虑内层循环的循环次数,其在外层循环为i时为cnt[i]。因为对于不同的输入,以及外层循环枚举到的不同的i,cnt[i]差别很大。但如果我们把所有i对应的内层循环次数相加,即可得到:

所以,整个算法的复杂度为O(n + K)。
我们提到过,有一条结论
- 所有基于比较的排序算法的时间复杂度都为
Ω(nlogn)。(Ω和O记号类似,但O表示的是“不会超过”,而Ω表示的是“不会少于”)。
我们看到当K = O(n)时,整个算法的时间复杂度为O(n)。之所以计数排序可以达到比O(nlogn)更好的时间复杂度,就是因为它并不是基于比较的排序。
对于基于原序列和答案序列位置对应设计的计数排序,经过分析可以发现其复杂度和第一种一样。大家可以自己尝试分析一下。
本文详细介绍了计数排序算法的原理,包括如何统计元素出现次数、构建前缀和数组以及如何根据前缀和分配元素位置。提供了一个C++实现示例,并分析了算法的时间和空间复杂度,指出在特定条件下,计数排序能达到线性时间复杂度,优于基于比较的排序算法。
373

被折叠的 条评论
为什么被折叠?



