1. 题目背景与需求解析
PAT(Programming Ability Test)乙级1038题是一道经典的算法练习题,主要考察对统计数据的处理能力。题目通常会给出N个学生的成绩,要求统计某个特定分数段的学生人数。这类问题在实际开发中非常常见,比如电商平台的销量统计、教育系统的成绩分析等场景。
从工程角度看,这道题的核心需求可以拆解为:
- 高效存储大量数值数据(学生成绩)
- 快速查询指定数值的出现次数
- 处理可能的大数据量情况(N可达10^6级别)
2. 算法选择与优化思路
2.1 暴力解法及其局限
最直观的做法是使用数组存储所有成绩,对于每个查询都遍历整个数组计数:
c复制int scores[1000000];
// 输入部分省略
for(int i=0; i<k; i++){
int query, count=0;
scanf("%d", &query);
for(int j=0; j<n; j++){
if(scores[j] == query) count++;
}
printf("%d", count);
}
这种解法的时间复杂度是O(k*n),当n和k都很大时(比如都是10^5),总操作次数会达到10^10量级,在OJ系统中必然超时。
2.2 哈希表法的优势
更优的方案是使用哈希表(或称为计数数组)预处理:
- 创建一个大小的101的数组count(成绩范围0-100)
- 输入时直接统计每个成绩出现的次数
- 查询时直接返回count[query]的值
c复制int count[101] = {0};
// 输入时统计
for(int i=0; i<n; i++){
int score;
scanf("%d", &score);
count[score]++;
}
// 查询时直接获取
for(int i=0; i<k; i++){
int query;
scanf("%d", &query);
printf("%d", count[query]);
}
这种方法将时间复杂度降为O(n+k),空间复杂度仅为O(1)(固定大小的数组),完美满足题目要求。
3. 完整实现与细节处理
3.1 C语言标准实现
c复制#include <stdio.h>
int main() {
int n, k;
scanf("%d", &n);
int count[101] = {0};
for(int i=0; i<n; i++){
int score;
scanf("%d", &score);
count[score]++;
}
scanf("%d", &k);
for(int i=0; i<k; i++){
int query;
scanf("%d", &query);
printf("%d", count[query]);
if(i != k-1) printf(" ");
}
return 0;
}
3.2 关键细节说明
- 数组初始化:
int count[101] = {0}确保所有元素初始化为0 - 输入处理:第一个循环读取n个成绩并统计
- 输出格式:注意最后一个数字后不能有空格,通过
if(i != k-1)判断 - 边界情况:题目保证查询的分数合法,否则需要添加范围检查
4. 性能分析与优化空间
4.1 时间复杂度对比
| 方法 | 预处理时间 | 单次查询时间 | 总时间复杂度 |
|---|---|---|---|
| 暴力法 | O(1) | O(n) | O(k*n) |
| 计数法 | O(n) | O(1) | O(n+k) |
当n=10^6,k=10^5时:
- 暴力法:约10^11次操作(不可接受)
- 计数法:约1.1×10^6次操作(毫秒级)
4.2 可能的优化方向
- IO优化:对于超大数据量,可以使用快速读写函数
c复制void fastRead(int *x){ char c = getchar(); *x = 0; for(; c>='0'&&c<='9'; c=getchar()) *x = (*x)<<3 + (*x)<<1 + c-'0'; } - 内存优化:成绩范围固定0-100,使用
unsigned char足够 - 并行处理:对于多核系统,可以分块统计(但PAT环境不需要)
5. 常见错误与调试技巧
5.1 典型错误案例
- 数组越界:
c复制int count[100] = {0}; // 应该是101,成绩可能为100 - 输出格式错误:
c复制printf("%d ", count[query]); // 最后会多一个空格 - 未初始化数组:
c复制int count[101]; // 未初始化可能包含随机值
5.2 调试建议
- 使用小数据测试边界情况:
- 输入n=1,k=1
- 成绩为0或100的极端值
- 添加中间输出调试:
c复制for(int i=0; i<=100; i++){ if(count[i]>0) printf("score %d: %d\n", i, count[i]); } - 使用OJ平台的错误提示:
- "答案错误":检查逻辑和输出格式
- "运行超时":优化算法复杂度
- "段错误":检查数组越界
6. 算法扩展与应用场景
6.1 变种题型
-
范围查询:查询分数在[a,b]区间的人数
- 解法:使用前缀和数组
c复制int prefix[101]; prefix[0] = count[0]; for(int i=1; i<=100; i++) prefix[i] = prefix[i-1] + count[i]; // 查询[a,b]:prefix[b]-prefix[a-1] -
动态更新:支持中途修改成绩
- 解法:使用树状数组或线段树
6.2 实际应用场景
- 电商系统:统计某价格区间的商品数量
- 日志分析:统计不同响应码的出现次数
- 游戏开发:玩家分数段位分布统计
7. 不同语言实现对比
7.1 C++版本
cpp复制#include <iostream>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, k, count[101] = {0};
cin >> n;
for(int i=0; i<n; i++){
int score;
cin >> score;
count[score]++;
}
cin >> k;
for(int i=0; i<k; i++){
int query;
cin >> query;
cout << count[query];
if(i != k-1) cout << " ";
}
return 0;
}
优势:
- 更简洁的输入输出
- 可以使用
unordered_map处理非连续分数(虽然本题不需要)
7.2 Python版本
python复制n = int(input())
count = [0] * 101
for score in map(int, input().split()):
count[score] += 1
queries = list(map(int, input().split()))
print(' '.join(str(count[q]) for q in queries[:-1]), end='')
print(count[queries[-1]] if queries else '')
注意事项:
- Python的列表切片处理更灵活
- 但大数据量下性能不如C/C++
- 注意处理空输入情况
8. 测试用例设计指南
8.1 标准测试用例
code复制输入:
10
60 75 90 55 75 99 32 75 90 90
5
75 90 100 0 60
输出:
3 3 0 0 1
8.2 边界测试用例
- 最小规模:
code复制
1 50 1 50 - 全相同成绩:
code复制100000 75 75 ... 75 1 75 - 极端值测试:
code复制5 0 100 0 100 50 3 0 100 50
9. 学习路径建议
-
基础巩固:
- 数组的基本操作
- 时间复杂度分析
- 输入输出处理
-
进阶学习:
- 哈希表的其他应用场景
- 前缀和与差分数组
- 树状数组与线段树
-
相关题目推荐:
- PAT乙级1047:编程团体赛(类似统计)
- LeetCode 347:前K个高频元素
- 洛谷P1177:【模板】快速排序(统计的逆运用)
10. 工程实践中的注意事项
-
内存管理:
- 全局数组自动初始化为0
- 局部数组需要手动初始化
- 动态分配时记得释放内存
-
代码风格:
- 合理使用空格和空行
- 变量命名要有意义(如count优于a)
- 添加必要注释
-
防御性编程:
c复制if(query >=0 && query <=100) // 虽然题目保证合法 printf("%d", count[query]); -
平台差异:
- Windows下栈空间约1MB,大数组应定义为全局变量
- 不同OJ的IO性能可能有差异
在实际开发中遇到类似需求时,建议先明确数据规模和查询特点。对于静态数据(如本题)计数法最优;动态数据可能需要更复杂的数据结构;分布式环境则要考虑分片统计。这个简单的算法题背后蕴含着重要的系统设计思想——空间换时间的权衡,这也是很多高性能系统的核心优化思路。