第 22 章 三合一——数据容器综合实战
🏗️ 前情回顾:M3 模块,你一路打怪升级——Ch16 一维数组让你把 50 个成绩装进一排柜子;Ch17 统计与变换让你能算平均、找最值、桶计数、插入删除;Ch18 二维数组让你像操作 Excel 一样处理行×列的表格;Ch19~20 string 让你读写文字、查找替换、编辑文档;Ch21 ASCII 揭开了字符底层是数字的秘密。但每一件武器都是单独练的。
💡 本章要解决的问题:所有容器分开了——数组、二维数组、字符串——怎么在一个程序里组合使用? 真实问题从来不会贴标签说"请用一维数组解我"。成绩排名既需要姓名(string)又需要分数(数组);螺旋填数需要二维数组 + 方向判断;文本统计需要 string + 字符判断……这一章,我们把三件武器装进同一个"战术背包",打一场联合作战。
🎯 本章目标
学完这一章,你能:
- 面对一道数据处理题,快速选择合适的数据容器
- 用二维数组完成矩阵旋转、螺旋填数等经典操作
- 用一维数组 + 字符串完成文本统计和单词计数
- 独立写出成绩排名、数据去重与排序程序
- 在同一个程序里混合使用数组和字符串
- 建立起"数据形状决定容器选择"的直觉
📖 故事引入
小林参加编程月考,最后一道大题是综合题:
"输入 n 个学生的姓名和语数英三科成绩,输出按总分从高到低的排名表。如果总分相同,按语文成绩降序;如果语文也相同,按姓名字典序升序。"
小林想:姓名用 string,成绩用数组,排名用排序……可这些知识点分属不同章节,怎么揉在一起?
他先不写代码,在纸上画了一张表:
姓名 语文 数学 英语 总分
张三 85 90 88 263
李四 92 85 90 267
王五 85 90 88 263 ← 和张三同分!他意识到这道题需要:
string name[100]— 存姓名int score[100][3]— 存三科成绩(二维数组)int total[100]— 存总分(一维数组)- 排序时需要"捆绑交换"——交换总分的同时,也要交换姓名和各科成绩
"原来所谓的综合题,就是把学会的每个小工具,按逻辑串起来。"小林想通了这一点,30 分钟就写出了满分代码。
综合题不难——难的是你有没有勇气把不同章节的知识揉在一起。本章的目标,就是帮你跨过这道坎。
🧱 知识讲解
22.1 容器选择的直觉
拿到一道数据处理题,先问自己一个问题:"数据长什么样?"
| 数据形状 | 容器 | 典型例子 |
|---|---|---|
| 一列数字 | int a[N] | 成绩单、温度记录 |
| 一行文字 | string s | 姓名、文章、密码 |
| 多行同列 | string names[N] | 全班姓名、单词列表 |
| 行×列的表格 | int a[N][M] | 成绩表、棋盘、图像 |
| 表格 + 标签 | 多个数组配合 | 姓名 + 各科成绩 |
🎯 核心直觉:数据的"形状"决定了容器的"形状"。如果你发现自己在声明 5 个分散的变量来描述同一个东西,说明你需要一个数组。如果你发现数组不够用还要再加一个平行的数组,说明你可能需要二维数组或结构体。
22.2 经典题①:矩阵旋转
题目:给定一个 n×n 的矩阵,将其顺时针旋转 90 度。
例子:
输入: 输出:
1 2 3 7 4 1
4 5 6 8 5 2
7 8 9 9 6 3思路:观察规律——旋转后,原来的第 i 行第 j 列,变成了新矩阵的第 j 行第 (n-1-i) 列。
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int a[100][100], b[100][100];
// 读入原矩阵
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
cin >> a[i][j];
// 旋转:a[i][j] → b[j][n-1-i]
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
b[j][n - 1 - i] = a[i][j];
// 输出新矩阵
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++)
cout << b[i][j] << " ";
cout << endl;
}
return 0;
}❌ 常见踩坑——旋转方向搞反:
cppb[i][j] = a[j][n-1-i]; // ❌ 这是逆时针旋转!✅ 顺时针正确公式:
b[j][n-1-i] = a[i][j]——原矩阵的 (i,j) 到新矩阵的 (j, n-1-i)。🧠 验证方法:拿一个 2×2 的小矩阵在纸上画一遍,确认公式正确再写代码。
💡 旋转 180° 就是转两次 90°,旋转 270° 就是转三次。掌握一个方向的变换规律就行!
22.3 经典题②:螺旋填数
题目:给定 n,按顺时针螺旋填入 1 到 n²。
例子(n=4):
1 2 3 4
12 13 14 5
11 16 15 6
10 9 8 7思路:模拟"走迷宫"——维护当前坐标 (x, y) 和方向,碰到边界或已填的格子就转弯。
int n;
cin >> n;
int a[20][20] = {0};
int dx[] = {0, 1, 0, -1}; // 右、下、左、上(x变化)
int dy[] = {1, 0, -1, 0}; // 右、下、左、上(y变化)
int dir = 0; // 当前方向:0=右 1=下 2=左 3=上
int x = 0, y = 0; // 当前位置
for (int k = 1; k <= n * n; k++) {
a[x][y] = k;
int nx = x + dx[dir];
int ny = y + dy[dir];
// 如果下一步出界或已经填过,就转弯
if (nx < 0 || nx >= n || ny < 0 || ny >= n || a[nx][ny] != 0) {
dir = (dir + 1) % 4; // 顺时针转 90°
nx = x + dx[dir];
ny = y + dy[dir];
}
x = nx;
y = ny;
}
// 输出矩阵
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++)
printf("%3d", a[i][j]); // 占 3 格对齐
cout << endl;
}🧩 螺旋填数是"模拟 + 数组"的经典结合。方向数组
dx[]和dy[]是一种高级技巧——用循环代替四个方向的if-else,代码更简洁优雅。
22.4 经典题③:字符串统计
题目:输入一段英文(可能包含空格和标点),统计其中有多少个字母、多少个数字、多少个单词。
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main() {
string line;
getline(cin, line); // 读入整行(含空格)
int letters = 0, digits = 0, words = 0;
bool inWord = false; // 标记:当前是否在单词内部
for (char ch : line) {
if (isalpha(ch)) letters++;
if (isdigit(ch)) digits++;
if (isalpha(ch) && !inWord) { // 进入一个新单词
words++;
inWord = true;
}
if (!isalpha(ch) && !isdigit(ch)) { // 遇到分隔符,离开单词
inWord = false;
}
}
cout << "字母数:" << letters << endl;
cout << "数字数:" << digits << endl;
cout << "单词数:" << words << endl;
return 0;
}💡 用
bool inWord标记"是否在单词内"是统计单词数的经典技巧——只在从"不在单词内"切换到"在单词内"的瞬间 +1。这样无论一个单词有多长,都只计一次。
❌ 常见踩坑——用空格数来统计单词:
cppint words = 1; for (char ch : line) { if (ch == ' ') words++; // ❌ "Hello, world" 两个空格会多计 } // 开头有空格的段落也会错✅ 正确——用 inWord 状态标记法,只在新单词的起始边界 +1。
22.5 经典题④:成绩排名
题目:输入 n 个学生的姓名和三科成绩,按总分降序排名。
关键技巧:排序时,交换总分的同时也要同步交换姓名和成绩——这叫做"捆绑交换"。
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
string name[100];
int score[100][3]; // 语文、数学、英语
int total[100];
// 读入并计算总分
for (int i = 0; i < n; i++) {
cin >> name[i];
total[i] = 0;
for (int j = 0; j < 3; j++) {
cin >> score[i][j];
total[i] += score[i][j];
}
}
// 冒泡排序(按总分降序)
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
if (total[j] < total[j + 1]) {
// 捆绑交换——三步同步
swap(total[j], total[j + 1]);
swap(name[j], name[j + 1]);
for (int k = 0; k < 3; k++)
swap(score[j][k], score[j + 1][k]);
}
}
}
// 输出排名
cout << "排名\t姓名\t语文\t数学\t英语\t总分" << endl;
for (int i = 0; i < n; i++) {
cout << i + 1 << "\t" << name[i];
for (int j = 0; j < 3; j++)
cout << "\t" << score[i][j];
cout << "\t" << total[i] << endl;
}
return 0;
}🎯 捆绑交换是综合题的灵魂——多个平行的数组描述同一组对象的多个属性时,对任何一个属性做排序,其他属性都必须跟着一起动。
❌ 经典踩坑——排序时忘了捆绑交换:
cppif (total[j] < total[j+1]) { swap(total[j], total[j+1]); // ❌ 只交换了总分! } // 结果:总分排名正确,但姓名还留在原位置——张三的成绩挂在了李四的名字下!✅ 正确——所有相关数组同步交换:
cppif (total[j] < total[j+1]) { swap(total[j], total[j+1]); // ✅ 总分 swap(name[j], name[j+1]); // ✅ 姓名 swap(score[j][0], score[j+1][0]); // ✅ 各科成绩都要换 swap(score[j][1], score[j+1][1]); swap(score[j][2], score[j+1][2]); }🧠 记忆:一条绳上的蚂蚱——动一个,全都要跟着动。
22.6 经典题⑤:数据去重与排序
题目:输入 n 个整数(可能重复),输出去重后升序排列的结果。
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int n, a[1000];
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
// 先排序
sort(a, a + n);
// 去重覆盖(双指针法)
int pos = 0; // pos 指向"不重复区间的最后一个位置"
for (int i = 1; i < n; i++) {
if (a[i] != a[pos]) {
pos++;
a[pos] = a[i]; // 把新元素覆盖到去重区
}
}
// 输出去重后的结果(pos+1 个元素)
for (int i = 0; i <= pos; i++)
cout << a[i] << " ";
cout << endl;
return 0;
}🧩 双指针去重比"读到新数组"更省空间。这种"原地修改"的思路(不新开数组,在原数组上操作)是竞赛里的常用优化。
✋ 动手试试
试试 1:把螺旋填数的 n 改成 5,观察输出。再试试逆时针螺旋填数(只需改方向数组和 dir 的初始值和转弯方向)。
试试 2:字符串统计程序现在只能统计一段英文。试试改成能统计整篇文章(多行输入,以空行结束)的单词总数。
试试 3:成绩排名程序里,如果总分相同要按语文成绩降序,怎么改?在交换的条件里加 || (total[j] == total[j+1] && score[j][0] < score[j+1][0])。试试看。
🦶 你踩过这些坑吗?
- [ ] 坑 1:排序时只交换了总分,忘了交换姓名和成绩——张冠李戴,数据全乱了
- [ ] 坑 2:螺旋填数中坐标更新顺序错——先填数再检查边界,最后一个数可能填在界外
- [ ] 坑 3:单词统计用空格计数——多个空格、"Hello, world!" 这种场景会统计错误
- [ ] 坑 4:矩阵旋转坐标公式写反——顺时针和逆时针的公式不一样,写反了方向就错了
- [ ] 坑 5:去重时直接在原数组上一边遍历一边删除——下标全乱,比不删还乱
📝 练习
基础题
1. 填空题:矩阵顺时针旋转 90 度后,原矩阵的 a[i][j] 会去新矩阵的 (____, ____) 位置。
2. 选择题:以下哪种操作不能用 string 直接完成? A. 查找子串 B. 删除一段字符 C. 按拼音排序 D. 截取一段子串
3. 读代码:以下程序的功能是?
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (i == j || i + j == n - 1) cout << "*";
else cout << " ";
}
cout << endl;
}4. 简答题:成绩排名系统中,为什么交换总分时也要同步交换姓名和各科成绩?如果只交换总分会发生什么?
提高题
5. 编程题 — 矩阵转置
输入一个 n×m 的矩阵,输出它的转置(行变列、列变行)。注意:转置后变成 m×n。
6. 编程题 — 单词频率统计
输入一段英文,统计每个单词出现的频率(忽略大小写)。输出出现次数最多的单词和它的次数。如果并列最多,输出字典序最小的那个。
提示:用一个 string words[1000] 数组存不重复的单词,用 int cnt[1000] 存对应的次数。注意去重时的大小写统一(用 tolower)。
7. 编程题 — 蛇形填数
类似螺旋填数,但改成蛇形(第一行从左到右,第二行从右到左,第三行从左到右……)。如 n=4:
1 2 3 4
8 7 6 5
9 10 11 12
16 15 14 13挑战题
8. 编程题 — 俄罗斯方块消行
在一个 10×20 的棋盘上模拟俄罗斯方块"消行"。输入当前棋盘(0 空格,1 有方块),扫描每一行,如果某一行全是 1,则消除该行,上面的所有行整体下移。输出消除后的棋盘。
9. 编程题 — 简易通讯录
实现一个通讯录管理程序,支持以下操作:
add 姓名 电话:添加联系人find 姓名:查询电话delete 姓名:删除联系人list:列出所有联系人(按姓名升序)
数据用 string name[100] 和 string phone[100] 两个平行数组存储。操作循环进行,直到输入 quit 退出。
🧠 本章小结
M3 综合实战 —— 数据容器联合作战:
【容器选择】
一列数 → int a[N]
一串文字 → string s
多行文字 → string arr[N]
表格(行×列) → int a[N][M]
对象的多个属性 → 平行数组(需捆绑交换!)
【经典综合题】
矩阵旋转 → 坐标变换公式 b[j][n-1-i] = a[i][j]
螺旋填数 → 方向数组 + 边界检测 + 转弯
字符串统计 → 状态标记 inWord 法
成绩排名 → 计算总分 + 排序 + 捆绑交换
数据去重 → 先排序 + 双指针原地覆盖
【关键技巧】
捆绑交换 | 方向数组 | 状态标记 | 双指针去重📝 配套练习
共7题。M3收官——同时用数组+string,检验数据容器选择的判断力。。★核心(课堂必做) ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | J0075 | https://hydro.ac/d/srqc/p/J0075 | 数组、三重循环、集合判断 |
| ◆ 拓展 | J0100 | https://hydro.ac/d/srqc/p/J0100 | 数组+函数、二维坐标 |
| ◆ 拓展 | J0103 | https://hydro.ac/d/srqc/p/J0103 | 数组+函数、最值、平均数 |
| ◆ 拓展 | luogu-P5727 | https://hydro.ac/p/luogu-P5727 | 数组、逆推、倒序输出 |
| ◆ 拓展 | luogu-P5732 | https://hydro.ac/p/luogu-P5732 | 二维数组、递推、杨辉三角 |
| ◆ 拓展 | luogu-P1319 | https://hydro.ac/p/luogu-P1319 | 压缩解码、数组填充 |
| ◆ 拓展 | luogu-P1205 | https://hydro.ac/p/luogu-P1205 | 二维数组、变换、旋转 |
| ◆ 拓展 | luogu-P1152 | https://hydro.ac/p/luogu-P1152 | 数组、相邻差、排序+遍历 |
| ◆ 拓展 | luogu-P1614 | https://hydro.ac/p/luogu-P1614 | 数组、滑动窗口启蒙、连续和 |
| ◆ 拓展 | luogu-P1179 | https://hydro.ac/p/luogu-P1179 | NOIP2010、桶计数、逐位拆解 |
| ◆ 拓展 | luogu-P1876 | https://hydro.ac/p/luogu-P1876 | 状态翻转、因子、完全平方数 |
| ◆ 拓展 | luogu-P2681 | https://hydro.ac/p/luogu-P2681 | 桶计数、数据结构 |
💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 11 道◆拓展题,覆盖不同变式和细节。
配套练习
共7题。M3收官——同时用数组+string,检验数据容器选择的判断力。★核心(课堂必做) · ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | J0075 | https://hydro.ac/d/srqc/p/J0075 | 数组、三重循环、集合判断 |
| ◆ 拓展 | J0100 | https://hydro.ac/d/srqc/p/J0100 | 数组+函数、二维坐标 |
| ◆ 拓展 | J0103 | https://hydro.ac/d/srqc/p/J0103 | 数组+函数、最值、平均数 |
| ◆ 拓展 | luogu-P5727 | https://hydro.ac/p/luogu-P5727 | 数组、逆推、倒序输出 |
| ◆ 拓展 | luogu-P5732 | https://hydro.ac/p/luogu-P5732 | 二维数组、递推、杨辉三角 |
| ◆ 拓展 | luogu-P1319 | https://hydro.ac/p/luogu-P1319 | 压缩解码、数组填充 |
| ◆ 拓展 | luogu-P1205 | https://hydro.ac/p/luogu-P1205 | 二维数组、变换、旋转 |
| ◆ 拓展 | luogu-P1152 | https://hydro.ac/p/luogu-P1152 | 数组、相邻差、排序+遍历 |
| ◆ 拓展 | luogu-P1614 | https://hydro.ac/p/luogu-P1614 | 数组、滑动窗口启蒙、连续和 |
| ◆ 拓展 | luogu-P1179 | https://hydro.ac/p/luogu-P1179 | NOIP2010、桶计数、逐位拆解 |
| ◆ 拓展 | luogu-P1876 | https://hydro.ac/p/luogu-P1876 | 状态翻转、因子、完全平方数 |
| ◆ 拓展 | luogu-P2681 | https://hydro.ac/p/luogu-P2681 | 桶计数、数据结构 |
练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 11 道◆拓展题,覆盖不同变式和细节。
自查清单:
- [ ] 我能根据数据形状快速选择正确的容器(数组 / 二维数组 / string / 平行数组)
- [ ] 我会写矩阵旋转和转置的代码(知道顺时针和逆时针的公式区别)
- [ ] 我能独立写出螺旋填数程序(方向数组 + 转向判断)
- [ ] 我能用
inWord标记法正确统计单词数(不靠数空格) - [ ] 排序时我会同步交换所有相关数组(捆绑交换——一个都不能少)
- [ ] 我会用双指针在原数组上去重
- [ ] 我能在同一程序里混合使用数组、二维数组和字符串
- [ ] 我不再害怕"综合题"——它们只是知识点的串联
🚀 下章预告
M3 模块圆满收关!你已经能处理"一堆数据"了——数组、二维数组、字符串,三件武器在手,组合起来能解决大部分数据处理问题。
但你有没有发现一个问题?程序里重复的代码越来越多。成绩排名的交换逻辑写了三遍,矩阵旋转的坐标变换到处复制粘贴,一个求最大值的逻辑在三四道题里反复出现……有没有办法把一段代码"打包成一个小盒子",给它起个名字,需要的时候一行调用就行?
下一模块 M4——函数,编程世界里最重要、最优雅的"代码复用"工具正式登场。从今以后,你的代码将不再是长长的一串,而是一个个精巧的"功能积木",需要哪块搭哪块。告别复制粘贴,迎接模块化编程!