Skip to content

第 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) 列。

cpp
#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;
}

常见踩坑——旋转方向搞反:

cpp
b[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) 和方向,碰到边界或已填的格子就转弯。

cpp
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 经典题③:字符串统计

题目:输入一段英文(可能包含空格和标点),统计其中有多少个字母、多少个数字、多少个单词。

cpp
#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。这样无论一个单词有多长,都只计一次。

常见踩坑——用空格数来统计单词:

cpp
int words = 1;
for (char ch : line) {
    if (ch == ' ') words++;    // ❌ "Hello,  world" 两个空格会多计
}                               //    开头有空格的段落也会错

正确——用 inWord 状态标记法,只在新单词的起始边界 +1。

22.5 经典题④:成绩排名

题目:输入 n 个学生的姓名和三科成绩,按总分降序排名。

关键技巧:排序时,交换总分的同时也要同步交换姓名和成绩——这叫做"捆绑交换"。

cpp
#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;
}

🎯 捆绑交换是综合题的灵魂——多个平行的数组描述同一组对象的多个属性时,对任何一个属性做排序,其他属性都必须跟着一起动。

经典踩坑——排序时忘了捆绑交换:

cpp
if (total[j] < total[j+1]) {
    swap(total[j], total[j+1]);   // ❌ 只交换了总分!
}
// 结果:总分排名正确,但姓名还留在原位置——张三的成绩挂在了李四的名字下!

正确——所有相关数组同步交换:

cpp
if (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 个整数(可能重复),输出去重后升序排列的结果。

cpp
#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. 读代码:以下程序的功能是?

cpp
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,检验数据容器选择的判断力。。★核心(课堂必做) ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0075https://hydro.ac/d/srqc/p/J0075数组、三重循环、集合判断
◆ 拓展J0100https://hydro.ac/d/srqc/p/J0100数组+函数、二维坐标
◆ 拓展J0103https://hydro.ac/d/srqc/p/J0103数组+函数、最值、平均数
◆ 拓展luogu-P5727https://hydro.ac/p/luogu-P5727数组、逆推、倒序输出
◆ 拓展luogu-P5732https://hydro.ac/p/luogu-P5732二维数组、递推、杨辉三角
◆ 拓展luogu-P1319https://hydro.ac/p/luogu-P1319压缩解码、数组填充
◆ 拓展luogu-P1205https://hydro.ac/p/luogu-P1205二维数组、变换、旋转
◆ 拓展luogu-P1152https://hydro.ac/p/luogu-P1152数组、相邻差、排序+遍历
◆ 拓展luogu-P1614https://hydro.ac/p/luogu-P1614数组、滑动窗口启蒙、连续和
◆ 拓展luogu-P1179https://hydro.ac/p/luogu-P1179NOIP2010、桶计数、逐位拆解
◆ 拓展luogu-P1876https://hydro.ac/p/luogu-P1876状态翻转、因子、完全平方数
◆ 拓展luogu-P2681https://hydro.ac/p/luogu-P2681桶计数、数据结构

💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 11 道◆拓展题,覆盖不同变式和细节。


配套练习

共7题。M3收官——同时用数组+string,检验数据容器选择的判断力。★核心(课堂必做) · ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0075https://hydro.ac/d/srqc/p/J0075数组、三重循环、集合判断
◆ 拓展J0100https://hydro.ac/d/srqc/p/J0100数组+函数、二维坐标
◆ 拓展J0103https://hydro.ac/d/srqc/p/J0103数组+函数、最值、平均数
◆ 拓展luogu-P5727https://hydro.ac/p/luogu-P5727数组、逆推、倒序输出
◆ 拓展luogu-P5732https://hydro.ac/p/luogu-P5732二维数组、递推、杨辉三角
◆ 拓展luogu-P1319https://hydro.ac/p/luogu-P1319压缩解码、数组填充
◆ 拓展luogu-P1205https://hydro.ac/p/luogu-P1205二维数组、变换、旋转
◆ 拓展luogu-P1152https://hydro.ac/p/luogu-P1152数组、相邻差、排序+遍历
◆ 拓展luogu-P1614https://hydro.ac/p/luogu-P1614数组、滑动窗口启蒙、连续和
◆ 拓展luogu-P1179https://hydro.ac/p/luogu-P1179NOIP2010、桶计数、逐位拆解
◆ 拓展luogu-P1876https://hydro.ac/p/luogu-P1876状态翻转、因子、完全平方数
◆ 拓展luogu-P2681https://hydro.ac/p/luogu-P2681桶计数、数据结构

练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 11 道◆拓展题,覆盖不同变式和细节。

自查清单

  • [ ] 我能根据数据形状快速选择正确的容器(数组 / 二维数组 / string / 平行数组)
  • [ ] 我会写矩阵旋转和转置的代码(知道顺时针和逆时针的公式区别)
  • [ ] 我能独立写出螺旋填数程序(方向数组 + 转向判断)
  • [ ] 我能用 inWord 标记法正确统计单词数(不靠数空格)
  • [ ] 排序时我会同步交换所有相关数组(捆绑交换——一个都不能少)
  • [ ] 我会用双指针在原数组上去重
  • [ ] 我能在同一程序里混合使用数组、二维数组和字符串
  • [ ] 我不再害怕"综合题"——它们只是知识点的串联

🚀 下章预告

M3 模块圆满收关!你已经能处理"一堆数据"了——数组、二维数组、字符串,三件武器在手,组合起来能解决大部分数据处理问题。

但你有没有发现一个问题?程序里重复的代码越来越多。成绩排名的交换逻辑写了三遍,矩阵旋转的坐标变换到处复制粘贴,一个求最大值的逻辑在三四道题里反复出现……有没有办法把一段代码"打包成一个小盒子",给它起个名字,需要的时候一行调用就行?

下一模块 M4——函数,编程世界里最重要、最优雅的"代码复用"工具正式登场。从今以后,你的代码将不再是长长的一串,而是一个个精巧的"功能积木",需要哪块搭哪块。告别复制粘贴,迎接模块化编程!