Skip to content

第 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 二维数组的声明和初始化

二维数组就是"数组的数组"。声明方式:

cpp
int a[3][4];  // 3 行 4 列的二维数组(存整数)

这创建了一个 3 行 4 列的网格,共 3×4 = 12 个格子:

     列0  列1  列2  列3
行0: [ ]  [ ]  [ ]  [ ]
行1: [ ]  [ ]  [ ]  [ ]
行2: [ ]  [ ]  [ ]  [ ]

声明时可以直接初始化:

cpp
// 方式一:完整初始化
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)的元素。

cpp
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 列"。和电影院座位一样:先找排(行),再找座(列)。

常见踩坑——行和列搞反:

cpp
int a[3][4];           // 3 行 4 列
a[4][2] = 5;           // ❌ 以为 4 是"第 4 列"——错!4 越过了行范围!

正确理解a[行数][列数],行标范围 0~2,列标范围 0~3

18.3 双重循环遍历

遍历二维数组的标准写法——外层循环管行,内层循环管列

cpp
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

这是以后处理所有矩阵问题的"起手式"。先写双重循环的骨架,再往里面填具体操作。

常见踩坑——行列循环颠倒:

cpp
for (int j = 0; j < 4; j++) {     // ❌ 外层管列、内层管行
    for (int i = 0; i < 3; i++) {
        cout << a[i][j] << " ";   // 按列输出,访问顺序不连续,效率差
    }
}

正确:外层行 i,内层列 j——按内存布局顺序访问,效率最高。

18.4 矩阵基本操作

有了双重循环,矩阵操作就变成了"在循环里对 a[i][j] 做点什么"。

操作一:矩阵转置(行变列、列变行)

cpp
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(行号等于列号):

cpp
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 为边长):

cpp
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):

cpp
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 多维数组概念

二维数组再往上,就是三维、四维……理论上没有上限,但竞赛中三维偶尔用到,四维以上极少。

cpp
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 二维数组作函数参数

二维数组传给函数时,必须指定第二维(列数)的大小

cpp
// ✅ 正确:列数必须明确
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;
    }
}

常见踩坑——传参忘记列数:

cpp
void func(int a[][], int n, int m) {  // ❌ 编译错误!
    // 编译器不知道每行有多少列,无法计算 a[i][j] 的地址
}

正确:必须指定列数(用常量或全局常量):

cpp
void func(int a[][100], int n, int m) { ... }          // 列数写死
// 或用全局常量:
const int MAXN = 100;
void func(int a[][MAXN], int n, int m) { ... }

为什么必须指定列数?因为 a[i][j] 的地址计算需要知道每行有多少个元素。编译器要知道"跳过几列才能到下一行"。

💡 实用技巧:用全局常量统一管理行列:

cpp
const 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. 读代码写结果

cpp
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题。二维数组从遍历→坐标→方向数组→压缩编码逐步抽象。。★核心(课堂必做) ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0070https://hydro.ac/d/srqc/p/J0070三重循环、三维坐标
◆ 拓展J0081https://hydro.ac/d/srqc/p/J0081二维坐标、方向数组dx/dy
◆ 拓展J0082https://hydro.ac/d/srqc/p/J0082二维→一维编码、压缩
◆ 拓展J0083https://hydro.ac/d/srqc/p/J0083压缩解码、一维→二维还原
◆ 拓展luogu-P5729https://hydro.ac/p/luogu-P5729三维数组、坐标操作
◆ 拓展luogu-P5730https://hydro.ac/p/luogu-P5730二维数组、点阵显示
◆ 拓展J0079https://hydro.ac/d/srqc/p/J0079二维、蛇形填充、方向控制

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


配套练习

共7题。二维数组从遍历→坐标→方向数组→压缩编码逐步抽象。★核心(课堂必做) · ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0070https://hydro.ac/d/srqc/p/J0070三重循环、三维坐标
◆ 拓展J0081https://hydro.ac/d/srqc/p/J0081二维坐标、方向数组dx/dy
◆ 拓展J0082https://hydro.ac/d/srqc/p/J0082二维→一维编码、压缩
◆ 拓展J0083https://hydro.ac/d/srqc/p/J0083压缩解码、一维→二维还原
◆ 拓展luogu-P5729https://hydro.ac/p/luogu-P5729三维数组、坐标操作
◆ 拓展luogu-P5730https://hydro.ac/p/luogu-P5730二维数组、点阵显示
◆ 拓展J0079https://hydro.ac/d/srqc/p/J0079二维、蛇形填充、方向控制

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

自查清单

  • [ ] 我能声明和初始化二维数组
  • [ ] 我理解 a[i][j] 中 i 是行、j 是列(第一个是行,第二个是列)
  • [ ] 我会用双重循环遍历二维数组(外层行,内层列)
  • [ ] 我能写出矩阵转置、对角线提取的代码
  • [ ] 我知道二维数组传参必须指定列数
  • [ ] 我了解三维数组的概念
  • [ ] 我能在脑中把"带行列的数据"自然地映射到二维数组

🚀 下章预告

二维数组搞定了"数字表格"——成绩单、棋盘、矩阵,统统拿下。但等等,数字是存起来了,文字呢?你的名字可不是 int!"张三"、"Hello World"、"https://baidu.com"——这些全是字母、汉字、符号组成的"一串字符"。数组装数字,那文字怎么装?

下一章,C++ 的文字专家——string 正式登场!从此你的程序不光能算数,还能读写文字、拼接句子、处理用户输入的名字和地址。