中 · 桶计数
计数排序
你用冒泡排序比较过每一对。这一道从头到尾不比任何两个数 —— 这不是取巧,而是一笔不同的交易:用内存和一个已知的值域,换速度。
一句话答案每个值都在 0 到 100 之间,所以用 101 格的数组统计出现次数,再按顺序把值打印回来 —— O(n + 101) 而不是 O(n log n)。
题目
输入: 第一行一个整数 n(1 ≤ n ≤ 1000)。第二行 n 个空格隔开的整数,每个都在 0 到 100 之间。
输出: 把同样的数按从小到大输出,单个空格隔开。
样例:
| 输入 | 输出 |
|---|---|
5 | 1 3 5 8 9 |
4 | 2 2 7 7 |
3 | 0 50 100 |
值域 0–100 不是细节,而是前提。计数排序能成立,唯一的原因是你负担得起「每个可能的值一个计数器」。如果值域到十亿,你就需要十亿个计数器。
你的解答
「运行我的输入」用的自定义输入
判题在浏览器本地运行。死循环会在 3 秒后被自动中断 —— 但还是确保每个循环都有出口。代码随打随存。
卡住了?提示
提示 1 — 你已经用过计数数组了
「数字出现次数」那道题做的就是这件事:让下标就是那个值,元素是它出现的次数。排序,只是把这个数组从左往右读一遍。
提示 2 — 两层循环,一层都不比较
外层 v 从 0 到 100。内层跑 cnt[v] 次,每次打印 v。某个值出现四次就打印四次;从没出现过,内层就是零次 —— 连 if 都不需要。
提示 3 — 那个在「互不相同」时看着对的 bug
「每个值只打印一次」(无视次数)在输入互不相同时答案完全正确 —— 所以它能过第一个样例,却在一出现重复时立刻挂掉。在相信自己的代码之前,先用 7 7 2 2 试一下。
题解(先自己试试!)
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int cnt[101] = {0};
for (int i = 0; i < n; i++) {
int x;
cin >> x;
cnt[x]++;
}
for (int v = 0; v <= 100; v++) {
for (int k = 0; k < cnt[v]; k++) cout << v << " ";
}
cout << endl;
return 0;
}
计数排序是 O(n + k),k 是值域 —— 比任何比较排序都快,而比较排序无法突破 O(n log n)。这不违反那个下界:下界只约束那些通过比较来了解数据的算法,而你是通过值本身了解的。真实系统里到处都是它:按年龄、分数、像素值、字节值排序。下一题是整套题里第一个真正的递归:汉诺塔。
深度解析
为什么这样做对
让下标就是值、元素是出现次数,排序就变成把这个数组从左往右读一遍。
代价
O(n + k),k 是值域。比任何比较排序都快 —— 因为 O(n log n) 那个下界只约束<em>通过比较</em>来了解数据的算法,而你是通过值本身了解的。
常见错法
- 每个值只打印一次、无视次数:输入互不相同时完全正确,一出现重复立刻挂掉。
- 忘记把计数数组清零(
int cnt[101] = {0};),就会在随机初值上继续累加。
下一步
整套题里第一个真正的递归:汉诺塔。
记住这两句
- 下标就是值,元素就是次数 —— 排序变成读数。
- 它快的前提是值域小。这是前提,不是细节。
🐞 这段是错的 —— 点开,亲手让它挂一次
每个值只打印一次 —— 只有在没有重复时才碰巧正确
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int cnt[101] = {0};
for (int i = 0; i < n; i++) {
int x;
cin >> x;
cnt[x]++;
}
for (int v = 0; v <= 100; v++) {
if (cnt[v] > 0) cout << v << " ";
}
cout << endl;
return 0;
}