第 18 章 表格世界——二维数组
🏗️ 前情回顾:上一章你掌握了"一排柜子"的玩法——一维数组能存一列成绩,你学会了遍历、统计、插入、删除、反转、去重。但真实世界不只有一列数据。一张成绩单:横向是语文、数学、英语三科,纵向是 40 个学生——这是有行有列的"表格",一维数组(一条线)装不下。
💡 本章要解决的问题:现实数据有行有列——成绩单每行一人、每列一科,怎么用一个数据结构同时管好"行"和"列"? 答案是把数组"叠"起来——一维数组的每个元素再变成一个数组。这就是二维数组,编程世界的 Excel 表格。
🎯 本章目标
学完这一章,你能:
- 声明和初始化二维数组
- 用行列概念理解
a[i][j]的含义 - 用双重循环遍历二维数组的每个元素
- 实现矩阵基本操作:转置、对角线提取、边界遍历
- 理解多维数组的概念(三维及以上)
- 正确地将二维数组作为函数参数传递
📖 故事引入
小杰在帮体育老师整理校运会的成绩。跳远比赛有 8 个选手,每人跳 3 次。成绩表长这样:
第1次 第2次 第3次
选手1: 4.5 4.8 4.3
选手2: 5.1 5.0 5.3
选手3: 3.9 4.2 4.1
...
选手8: 5.5 5.2 5.6如果用一维数组,他需要 8×3=24 个变量,或者声明三个分开的数组(first[8]、second[8]、third[8])。但"第 4 个选手的第 2 跳"这种表达,用分离的数组很别扭。
小杰想要一种"像 Excel 表格"的数据结构——有行、有列,用 成绩[选手编号][第几跳] 就能直接访问。
C++ 的二维数组,就是这样一个"编程世界的 Excel 表格"。
🧱 知识讲解
18.1 二维数组的声明和初始化
二维数组就是"数组的数组"。声明方式:
int a[3][4]; // 3 行 4 列的二维数组(存整数)这创建了一个 3 行 4 列的网格,共 3×4 = 12 个格子:
列0 列1 列2 列3
行0: [ ] [ ] [ ] [ ]
行1: [ ] [ ] [ ] [ ]
行2: [ ] [ ] [ ] [ ]声明时可以直接初始化:
// 方式一:完整初始化
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 方式二:自动补 0
int b[3][4] = {
{1, 2},
{3, 4}
}; // 其余位置自动填 0
// 方式三:全部初始化为 0
int c[3][4] = {0}; // 所有 12 个元素都是 0💡
int c[3][4] = {0};是把整个数组清零的最快方式。
18.2 行和列:a[i][j] 的含义
a[i][j] 中,第一个下标是行号,第二个下标是列号。
a[0][0] a[0][1] a[0][2] a[0][3] ← 第 0 行
a[1][0] a[1][1] a[1][2] a[1][3] ← 第 1 行
a[2][0] a[2][1] a[2][2] a[2][3] ← 第 2 行
↑ ↑ ↑ ↑
第0列 第1列 第2列 第3列和普通数组一样,行和列都从 0 开始编号。a[2][3] 是第 3 行(下标 2)、第 4 列(下标 3)的元素。
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
cout << a[0][1] << endl; // 输出 2(第0行第1列)
cout << a[2][3] << endl; // 输出 12(第2行第3列)记忆技巧:
a[i][j]——"先找第 i 行,再找该行的第 j 列"。和电影院座位一样:先找排(行),再找座(列)。
❌ 常见踩坑——行和列搞反:
cppint a[3][4]; // 3 行 4 列 a[4][2] = 5; // ❌ 以为 4 是"第 4 列"——错!4 越过了行范围!✅ 正确理解:
a[行数][列数],行标范围0~2,列标范围0~3。
18.3 双重循环遍历
遍历二维数组的标准写法——外层循环管行,内层循环管列:
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
for (int i = 0; i < 3; i++) { // i 是行号
for (int j = 0; j < 4; j++) { // j 是列号
cout << a[i][j] << " ";
}
cout << endl; // 每行结束换行
}输出:
1 2 3 4
5 6 7 8
9 10 11 12这是以后处理所有矩阵问题的"起手式"。先写双重循环的骨架,再往里面填具体操作。
❌ 常见踩坑——行列循环颠倒:
cppfor (int j = 0; j < 4; j++) { // ❌ 外层管列、内层管行 for (int i = 0; i < 3; i++) { cout << a[i][j] << " "; // 按列输出,访问顺序不连续,效率差 } }✅ 正确:外层行
i,内层列j——按内存布局顺序访问,效率最高。
18.4 矩阵基本操作
有了双重循环,矩阵操作就变成了"在循环里对 a[i][j] 做点什么"。
操作一:矩阵转置(行变列、列变行)
int a[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
int b[4][3]; // 转置后变成 4 行 3 列
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
b[j][i] = a[i][j]; // 关键:行列互换
}
}
// 现在 b 是 a 的转置矩阵操作二:对角线元素
主对角线的特点是 i == j(行号等于列号):
int a[3][3] = { {1,2,3}, {4,5,6}, {7,8,9} };
cout << "主对角线:";
for (int i = 0; i < 3; i++) {
cout << a[i][i] << " "; // a[0][0], a[1][1], a[2][2] → 1 5 9
}副对角线的特点是 i + j == n - 1(n 为边长):
cout << "副对角线:";
for (int i = 0; i < 3; i++) {
cout << a[i][2 - i] << " "; // a[0][2], a[1][1], a[2][0] → 3 5 7
}操作三:边界元素
四个边界分别是:第一行(i == 0)、最后一行(i == n-1)、第一列(j == 0)、最后一列(j == m-1):
int sum = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (i == 0 || i == n-1 || j == 0 || j == m-1) {
sum += a[i][j]; // 只加边界元素
}
}
}18.5 多维数组概念
二维数组再往上,就是三维、四维……理论上没有上限,但竞赛中三维偶尔用到,四维以上极少。
int cube[3][4][5]; // 三维数组:3 层 × 4 行 × 5 列
cube[0][1][2] = 42; // 第 0 层、第 1 行、第 2 列
// 三维遍历:三层循环
for (int k = 0; k < 3; k++) {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 5; j++) {
cout << cube[k][i][j] << " ";
}
}
}三维数组可以想象成"一摞表格"——每一层是一张二维表。但现阶段重点掌握二维即可,三维概念知道就行。
18.6 二维数组作函数参数
二维数组传给函数时,必须指定第二维(列数)的大小:
// ✅ 正确:列数必须明确
void printMatrix(int a[][4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
cout << a[i][j] << " ";
}
cout << endl;
}
}❌ 常见踩坑——传参忘记列数:
cppvoid func(int a[][], int n, int m) { // ❌ 编译错误! // 编译器不知道每行有多少列,无法计算 a[i][j] 的地址 }✅ 正确:必须指定列数(用常量或全局常量):
cppvoid func(int a[][100], int n, int m) { ... } // 列数写死 // 或用全局常量: const int MAXN = 100; void func(int a[][MAXN], int n, int m) { ... }
为什么必须指定列数?因为 a[i][j] 的地址计算需要知道每行有多少个元素。编译器要知道"跳过几列才能到下一行"。
💡 实用技巧:用全局常量统一管理行列:
cppconst int MAXN = 100; int a[MAXN][MAXN]; void process(int a[][MAXN], int n, int m) { ... }
✋ 动手试试
试试 1:创建一个 5×5 的二维数组,全部初始化为 0,然后把主对角线上的元素设为 1。用双重循环输出,看看你打印出了一个什么图案。
试试 2:手动创建一个 3×3 的"幻方候选"数组,分别计算每行之和、每列之和、两条对角线之和。验证所有和是否相等。
试试 3:创建一个 4×5 的数组,用随机想法填上数字。写代码找出最大值,并输出它所在的行号和列号。用一个小变量记住"到目前为止看到的最大值"。
试试 4:写一个 transpose 函数,接收一个 N×N 的方阵,在原地完成转置(不能创建新数组)。提示:只需要遍历上三角(j > i 的元素),交换 a[i][j] 和 a[j][i]。
🦶 你踩过这些坑吗?
- [ ] 坑 1:以为
a[3][4]是 4 行 3 列——第一个数字永远是行,第二个是列 - [ ] 坑 2:传参写成
void f(int a[][], int n, int m)——忘了指定列数,编译不通过 - [ ] 坑 3:遍历时外层管列、内层管行——虽然语法没错,但访问顺序不连续,效率低
- [ ] 坑 4:访问
a[3][0]但只声明了 3 行——行下标最大是 2,越界了 - [ ] 坑 5:转置时新矩阵的行列数没反——转置后
b的行数 = 原矩阵列数,b的列数 = 原矩阵行数
📝 练习
基础题
1. 填空题
(1)int a[5][6]; 总共能存 ____ 个元素。
(2)a[2][3] 中,2 是 ____ 号,3 是 ____ 号。
(3)二维数组传给函数时,必须指定 ____ 的大小。
(4)主对角线上元素满足 i ____ j(填 ==、!=、> 或 <)。
2. 读代码写结果
int a[3][3] = { {1,2,3}, {4,5,6}, {7,8,9} };
int sum = 0;
for (int i = 0; i < 3; i++) {
sum += a[i][2 - i];
}
cout << sum << endl;输出是什么?
3. 选择题
(1)a[3][5] 这个数组有几行几列? A. 3 行 5 列 B. 5 行 3 列 C. 3 行 3 列 D. 15 行 1 列
(2)要访问二维数组第 2 行第 4 列的元素,下标写法是? A. a[2][4] B. a[4][2] C. a[1][3] D. a[3][1]
提高题
4. 编程题 — 矩阵加法
输入两个 N×M 的矩阵 A 和 B,计算它们的和矩阵 C(C[i][j] = A[i][j] + B[i][j]),输出 C。
5. 编程题 — 杨辉三角
用二维数组输出杨辉三角的前 10 行。规则:
- 每行第一个和最后一个元素是 1
- 其他元素 = 上一行左上方 + 上一行正上方:
a[i][j] = a[i-1][j-1] + a[i-1][j]
输出格式:数字之间用空格分隔,每行换行。
6. 编程题 — 行和与列和
输入一个 N×M 的矩阵,分别计算每行的和与每列的和,输出"行和数组"和"列和数组"。例如输入:
3 4
1 2 3 4
5 6 7 8
9 10 11 12输出:行和 10 26 42,列和 15 18 21 24。
挑战题
7. 编程题 — 螺旋矩阵
输入一个奇数 N(如 5),在 N×N 的二维数组中按顺时针螺旋填入 1 到 N²。例如 N=3 时:
1 2 3
8 9 4
7 6 5(提示:用四个边界变量 top、bottom、left、right,一圈一圈地填。这是经典的"螺旋填数"问题。)
8. 编程题 — 马鞍点
在一个矩阵中,如果某个元素是它所在行的最大值,同时又是它所在列的最小值,这个元素叫"马鞍点"。输入一个 N×M 矩阵,找出所有马鞍点并输出它们的位置和值。如果不存在马鞍点,输出"没有马鞍点"。
🧠 本章小结
二维数组:a[行数][列数]
访问:a[i][j] → 第 i 行第 j 列
遍历:双重循环(外层行,内层列)
矩阵操作:
转置:b[j][i] = a[i][j]
主对角线:a[i][i]
副对角线:a[i][n-1-i]
边界:i==0 || i==n-1 || j==0 || j==m-1
函数传参:必须写列数 → void f(int a[][列数], int 行数)
三维数组:a[层][行][列](了解即可)📝 配套练习
共7题。二维数组从遍历→坐标→方向数组→压缩编码逐步抽象。。★核心(课堂必做) ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | J0070 | https://hydro.ac/d/srqc/p/J0070 | 三重循环、三维坐标 |
| ◆ 拓展 | J0081 | https://hydro.ac/d/srqc/p/J0081 | 二维坐标、方向数组dx/dy |
| ◆ 拓展 | J0082 | https://hydro.ac/d/srqc/p/J0082 | 二维→一维编码、压缩 |
| ◆ 拓展 | J0083 | https://hydro.ac/d/srqc/p/J0083 | 压缩解码、一维→二维还原 |
| ◆ 拓展 | luogu-P5729 | https://hydro.ac/p/luogu-P5729 | 三维数组、坐标操作 |
| ◆ 拓展 | luogu-P5730 | https://hydro.ac/p/luogu-P5730 | 二维数组、点阵显示 |
| ◆ 拓展 | J0079 | https://hydro.ac/d/srqc/p/J0079 | 二维、蛇形填充、方向控制 |
💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 6 道◆拓展题,覆盖不同变式和细节。
配套练习
共7题。二维数组从遍历→坐标→方向数组→压缩编码逐步抽象。★核心(课堂必做) · ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | J0070 | https://hydro.ac/d/srqc/p/J0070 | 三重循环、三维坐标 |
| ◆ 拓展 | J0081 | https://hydro.ac/d/srqc/p/J0081 | 二维坐标、方向数组dx/dy |
| ◆ 拓展 | J0082 | https://hydro.ac/d/srqc/p/J0082 | 二维→一维编码、压缩 |
| ◆ 拓展 | J0083 | https://hydro.ac/d/srqc/p/J0083 | 压缩解码、一维→二维还原 |
| ◆ 拓展 | luogu-P5729 | https://hydro.ac/p/luogu-P5729 | 三维数组、坐标操作 |
| ◆ 拓展 | luogu-P5730 | https://hydro.ac/p/luogu-P5730 | 二维数组、点阵显示 |
| ◆ 拓展 | J0079 | https://hydro.ac/d/srqc/p/J0079 | 二维、蛇形填充、方向控制 |
练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 6 道◆拓展题,覆盖不同变式和细节。
自查清单:
- [ ] 我能声明和初始化二维数组
- [ ] 我理解
a[i][j]中 i 是行、j 是列(第一个是行,第二个是列) - [ ] 我会用双重循环遍历二维数组(外层行,内层列)
- [ ] 我能写出矩阵转置、对角线提取的代码
- [ ] 我知道二维数组传参必须指定列数
- [ ] 我了解三维数组的概念
- [ ] 我能在脑中把"带行列的数据"自然地映射到二维数组
🚀 下章预告
二维数组搞定了"数字表格"——成绩单、棋盘、矩阵,统统拿下。但等等,数字是存起来了,文字呢?你的名字可不是 int!"张三"、"Hello World"、"https://baidu.com"——这些全是字母、汉字、符号组成的"一串字符"。数组装数字,那文字怎么装?
下一章,C++ 的文字专家——string 正式登场!从此你的程序不光能算数,还能读写文字、拼接句子、处理用户输入的名字和地址。