C1-10 循环嵌套
单层循环处理一条“线”,循环嵌套可以处理一个“面”:一行一行打印图形,一格一格检查方格。本课先用矩形绘制理解循环嵌套,再继续学习各种字符图形的绘制方法。
学习目标:理解“外层循环每执行一次,内层循环完整执行一遍”;掌握双重
for循环的基本语法;能够用按行拆分法绘制矩形、直角三角形、金字塔和翻转金字塔;能够用坐标特征法绘制单点、行、列、对角线、“日”字、X和菱形;养成先观察规律、再写循环的习惯。
一、引入:为什么要把循环套起来
1. 单层循环只能处理一个方向
输出一行 5 个星号:
for (int i = 1; i <= 5; i++) {
cout << '*';
}
cout << '\n';
输出:
*****
如果要输出 3 行、每行 5 个星号,复制三次当然可以:
cout << "*****\n";
cout << "*****\n";
cout << "*****\n";
但如果行数和列数改变,就要重新修改很多代码。更好的办法是:
- 外层循环负责第几行;
- 内层循环负责这一行的第几个位置。
2. 什么是循环嵌套
一个循环的循环体中又包含另一个完整的循环,就叫循环嵌套。
for (初始化1; 条件1; 更新1) { // 外层循环
for (初始化2; 条件2; 更新2) { // 内层循环
// 重复执行的语句
}
}
核心规律:
外层循环每执行一次,内层循环就从头到尾完整执行一遍。
例如:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 4; j++) {
cout << '*';
}
cout << '\n';
}
- 外层执行 3 次,表示 3 行;
- 每行内层执行 4 次,表示 4 列;
- 输出语句一共执行 \(3 \times 4 = 12\) 次。
二、双重 for 循环的基本语法
1. 基本格式
for (初始化; 条件; 更新) { // 外层
for (初始化; 条件; 更新) { // 内层
// 内层循环体
}
// 当前行处理结束后要做的事
}
常用变量名:
| 循环层次 | 常用变量 | 常见含义 |
|---|---|---|
| 外层 | i |
行号、大轮次 |
| 内层 | j |
列号、小轮次 |
| 第三层 | k |
更细的一层 |
初学阶段建议外层用 i、内层用 j,不要让内外层使用同一个变量名。
2. 执行顺序
for (int i = 1; i <= 2; i++) {
for (int j = 1; j <= 3; j++) {
cout << '(' << i << ',' << j << ") ";
}
cout << '\n';
}
输出:
(1,1) (1,2) (1,3)
(2,1) (2,2) (2,3)
执行顺序:
- 外层
i = 1; - 内层
j从 1 执行到 3; - 内层结束,换行;
- 外层变成
i = 2; - 内层的
j重新从 1 开始; - 重复上述过程。
3. 换行必须放对位置
正确写法:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 4; j++) {
cout << '*';
}
cout << '\n'; // 内层循环结束后,每行换一次
}
错误写法:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 4; j++) {
cout << '*' << '\n'; // 每个星号都换行
}
}
输出会变成一列星号。图形题中要牢记:
内层负责一行的内容,内层结束后再换行。
4. for 和 while 也可以嵌套
本课主要使用 for 套 for。不同类型的循环也可以嵌套,例如:
int i = 1;
while (i <= 3) {
for (int j = 1; j <= 4; j++) {
cout << '*';
}
cout << '\n';
i++;
}
while 嵌套时,要特别检查初始化和更新是否完整。
三、示例:绘制矩形
矩形绘制是理解“外层管行、内层管列”的好例子。我们输入行数 n 和列数 m,使用 * 输出一个 n 行 m 列的矩形。
1. 先明确图形的行和列
例如,输入 3 5,目标图形是:
*****
*****
*****
观察这个图形:
- 一共有 3 行,所以外层循环执行 3 次;
- 每一行有 5 个
*,所以内层循环执行 5 次; - 每完成一行,就输出一个换行符。
2. 使用双重循环绘制矩形
#include <iostream>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) { // 外层:控制行
for (int j = 1; j <= m; j++) { // 内层:控制列
cout << '*';
}
cout << '\n'; // 当前行结束后换行
}
return 0;
}
3. 代码分析
- 外层循环变量
i表示当前正在绘制第几行; - 内层循环变量
j表示当前行中的第几列; - 外层每执行一次,内层就完整执行一次;
cout << '*'一共执行n * m次;- 换行语句必须放在内层循环外,否则每个星号都会单独换行。
矩形示例已经包含了双重循环绘制字符图形的基本结构:
外层循环:一行一行地处理
内层循环:一列一列地输出
行结束后:统一换行
后面的直角三角形、金字塔、翻转金字塔以及坐标特征图形,都是在这个结构上改变“每一行输出什么”的规律。
四、图形打印总方法:按行拆分法
1. 图形题四步法
拿到图形题,不要直接敲代码,先做四件事:
- 确定图形有几行;
- 观察第
i行要输出什么; - 找出空格、星号或数字的数量规律;
- 内层循环输出当前行,当前行结束后换行。
可以记成:
外层控制行,内层控制这一行;一行结束后再换行。
2. 先画表格找规律
例如左下三角形:
*
**
***
****
行号 i |
星号数 |
|---|---|
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
所以第 i 行输出 i 个星号,内层循环上界就是 i。
五、矩形
1. 题目
输入正整数 n 和 m,输出 n 行 m 列的星号矩形。
输入:
3 5
输出:
*****
*****
*****
2. 代码
#include <iostream>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) { // n 行
for (int j = 1; j <= m; j++) { // 每行 m 列
cout << '*';
}
cout << '\n';
}
return 0;
}
矩形的规律:
| 项目 | 规律 |
|---|---|
| 行数 | n |
| 每行字符数 | m |
| 外层循环 | i = 1 ~ n |
| 内层循环 | j = 1 ~ m |
如果题目要求每个星号后有空格,可以写成:
cout << "* ";
是否保留末尾空格,要以题目要求为准。
六、直角三角形
1. 左下直角三角形
目标图形:
*
**
***
****
*****
第 i 行输出 i 个星号:
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
cout << '*';
}
cout << '\n';
}
2. 右下直角三角形
目标图形:
*
**
***
****
*****
第 i 行:
- 空格数为
n - i; - 星号数为
i。
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n - i; j++) {
cout << ' ';
}
for (int j = 1; j <= i; j++) {
cout << '*';
}
cout << '\n';
}
3. 左上直角三角形
目标图形:
*****
****
***
**
*
第 i 行输出 n - i + 1 个星号:
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n - i + 1; j++) {
cout << '*';
}
cout << '\n';
}
4. 右上直角三角形
目标图形:
*****
****
***
**
*
第 i 行:
- 空格数为
i - 1; - 星号数为
n - i + 1。
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i - 1; j++) {
cout << ' ';
}
for (int j = 1; j <= n - i + 1; j++) {
cout << '*';
}
cout << '\n';
}
5. 四种直角三角形对照
| 图形 | 前导空格数 | 星号数 |
|---|---|---|
| 左下 | 0 | i |
| 右下 | n-i |
i |
| 左上 | 0 | n-i+1 |
| 右上 | i-1 |
n-i+1 |
不要死记代码。先写出“第 i 行空格几个、星号几个”,再写循环。
七、金字塔与翻转金字塔
1. 金字塔
目标图形:
*
***
*****
*******
*********
第 i 行:
- 空格数为
n - i; - 星号数为
2 * i - 1。
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n - i; j++) {
cout << ' ';
}
for (int j = 1; j <= 2 * i - 1; j++) {
cout << '*';
}
cout << '\n';
}
2. 翻转金字塔
目标图形:
*********
*******
*****
***
*
第 i 行:
- 空格数为
i - 1; - 星号数为
2 * (n - i) + 1。
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i - 1; j++) {
cout << ' ';
}
for (int j = 1; j <= 2 * (n - i) + 1; j++) {
cout << '*';
}
cout << '\n';
}
3. 图形打印检查表
- 外层循环是否控制了行数?
- 内层循环是否只负责当前行?
- 空格和星号的数量公式是否正确?
- 换行是否在内层循环外?
- 是否多输出了题目不需要的空格?
八、坐标特征法:把屏幕看成方格
按行拆分法适合规律明显的图形。遇到“某些位置输出星号,其他位置输出空格”的图形,可以让 i 表示行号、j 表示列号,逐格判断。
1. 基本格式
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (满足条件) {
cout << '*';
} else {
cout << ' ';
}
}
cout << '\n';
}
可以把每个格子都问一遍:
第
i行、第j列,满足图形条件吗?
满足就输出 *,否则输出空格。
2. 单点:i == row && j == col
如果只画一个点,点所在的行是 row,列是 col,必须同时满足:
i == row && j == col
int n, row, col;
cin >> n >> row >> col;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == row && j == col) {
cout << '*';
} else {
cout << ' ';
}
}
cout << '\n';
}
&& 表示“并且”:行、列都正确,才是这个点。
3. 第 row 行:i == row
if (i == row) {
cout << '*';
} else {
cout << ' ';
}
这一行中的每一列都要画,所以不需要判断 j。
4. 第 col 列:j == col
if (j == col) {
cout << '*';
} else {
cout << ' ';
}
5. 例题:画“日”字
输入一个奇数 n,在 n × n 方格中画“日”字。要画:
- 第一行;
- 中间一行;
- 最后一行;
- 第一列;
- 最后一列。
中间行是 (n + 1) / 2,条件为:
i == 1 || i == (n + 1) / 2 || i == n || j == 1 || j == n
完整代码:
int n;
cin >> n; // n 为奇数
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == 1 || i == (n + 1) / 2 || i == n ||
j == 1 || j == n) {
cout << '*';
} else {
cout << ' ';
}
}
cout << '\n';
}
输入 5:
*****
* *
*****
* *
*****
6. 主对角线:i == j
主对角线从左上角到右下角,行号和列号相等:
if (i == j) {
cout << '*';
} else {
cout << ' ';
}
7. 副对角线:i + j == n + 1
副对角线从右上角到左下角。对于 n × n 方格:
if (i + j == n + 1) {
cout << '*';
} else {
cout << ' ';
}
8. 例题:画 X
一个位置在主对角线或副对角线上,就输出 *:
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == j || i + j == n + 1) {
cout << '*';
} else {
cout << ' ';
}
}
cout << '\n';
}
输入 7:
* *
* *
* *
*
* *
* *
* *
9. 主对角线的平行线:i - j == 定值
主对角线本身可以写成:
i - j == 0
把差值换成其他固定值,就得到它的平行线:
if (i - j == d) {
cout << '*';
} else {
cout << ' ';
}
例如:
d == 0:主对角线;d == 1:主对角线下方一条平行线;d == -1:主对角线上方一条平行线。
10. 副对角线的平行线:i + j == 定值
副对角线本身是:
i + j == n + 1
把右边换成其他固定值,就得到副对角线的平行线:
if (i + j == s) {
cout << '*';
} else {
cout << ' ';
}
11. 例题:画菱形的四条边
设 n 为奇数,方格的行号、列号都从 1 开始。菱形中心就是方格中心,中心坐标为:
c = (n + 1) / 2
中心 = (c, c)
例如 n = 7 时,中心是 (4,4)。菱形的四个顶点分别是:
上顶点: (1, c)
左顶点: (c, 1)
下顶点: (n, c)
右顶点: (c, n)
先不要急着写代码。先判断每条边的方向:
- 左上边、右下边与副对角线平行,所以要观察
i + j是否为定值; - 右上边、左下边与主对角线平行,所以要观察
i - j是否为定值。
然后分别取每条边的两个端点,用端点坐标计算出这个定值。
可以先用下面的方向表确定应该观察哪一种坐标特征:
| 菱形边 | 与哪条对角线平行 | 应观察的量 |
|---|---|---|
| 左上边 | 副对角线 | i + j |
| 右上边 | 主对角线 | i - j |
| 左下边 | 主对角线 | i - j |
| 右下边 | 副对角线 | i + j |
也就是说:斜率方向相同的边,使用同一种“定值”判断;先分清方向,再计算定值。
(1)左上边:副对角线的平行线
左上边连接上顶点 (1,c) 和左顶点 (c,1):
上顶点:1 + c
左顶点:c + 1
两者的坐标和相等,所以左上边满足:
i + j == 1 + c
这是一条副对角线的平行线。
(2)右上边:主对角线的平行线
右上边连接上顶点 (1,c) 和右顶点 (c,n)。观察坐标差:
上顶点:1 - c
右顶点:c - n
因为 c = (n+1)/2,所以:
1 - c = c - n = -(n - 1) / 2
两端点的坐标差相等,所以右上边满足:
i - j == 1 - c
这是一条主对角线的平行线。
(3)左下边:主对角线的平行线
左下边连接左顶点 (c,1) 和下顶点 (n,c)。观察坐标差:
左顶点:c - 1
下顶点:n - c
因为 c = (n+1)/2,所以:
c - 1 = n - c = (n - 1) / 2
因此左下边满足:
i - j == c - 1
这也是一条主对角线的平行线。
(4)右下边:副对角线的平行线
右下边连接下顶点 (n,c) 和右顶点 (c,n):
下顶点:n + c
右顶点:c + n
两端点的坐标和相等,所以右下边满足:
i + j == n + c
这是一条副对角线的平行线。
(5)把定值统一写出来
令:
int c = (n + 1) / 2; // 中心坐标
int topLeftSum = 1 + c; // 左上边:i+j 的定值
int topRightDiff = 1 - c; // 右上边:i-j 的定值
int bottomLeftDiff = c - 1;// 左下边:i-j 的定值
int bottomRightSum = n + c;// 右下边:i+j 的定值
再代入 c = (n+1)/2,也可以得到:
左上边:i + j == (n + 3) / 2
右上边:i - j == -(n - 1) / 2
左下边:i - j == (n - 1) / 2
右下边:i + j == (3n + 1) / 2
这里的定值不是凭空记忆出来的,而是由边上两个端点计算得到的:
| 菱形边 | 取哪一种特征 | 两个端点的计算 | 定值 |
|---|---|---|---|
| 左上边 | 坐标和 | 1+c 与 c+1 |
1+c |
| 右上边 | 坐标差 | 1-c 与 c-n |
1-c |
| 左下边 | 坐标差 | c-1 与 n-c |
c-1 |
| 右下边 | 坐标和 | n+c 与 c+n |
n+c |
完整代码:
int n;
cin >> n; // n 为奇数
int c = (n + 1) / 2;
int topLeftSum = 1 + c;
int topRightDiff = 1 - c;
int bottomLeftDiff = c - 1;
int bottomRightSum = n + c;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i + j == topLeftSum ||
i - j == topRightDiff ||
i - j == bottomLeftDiff ||
i + j == bottomRightSum) {
cout << '*';
} else {
cout << ' ';
}
}
cout << '\n';
}
输入 7:
*
* *
* *
* *
* *
* *
*
写坐标图形时,可以先在方格中标出边界,再把每条边翻译成“坐标差相等”或“坐标和相等”。
12. 坐标特征速查表
| 图形位置 | 条件 |
|---|---|
单点 (row,col) |
i == row && j == col |
第 row 行 |
i == row |
第 col 列 |
j == col |
| 主对角线 | i == j |
| 副对角线 | i + j == n + 1 |
| 主对角线平行线 | i - j == d |
| 副对角线平行线 | i + j == s |
| “日”字 | `i == 1 |
X |
`i == j |
记忆方法:单点看“行和列同时相等”;主对角线看“差”;副对角线看“和”。
九、常见错误与注意事项
1. 编译类错误
| 错误 | 常见现象 | 改正 |
|---|---|---|
| 内外层循环变量未定义 | 'i' was not declared |
先定义 i、j |
for 的分号漏写 |
for 结构无法识别 |
写成 for (初始化; 条件; 更新) |
| 括号或花括号不配对 | expected ')'、expected '}' |
逐层检查 {}、() |
| 字符和字符串引号混用 | 编译错误或输出异常 | 一个字符用 ' ',字符串用 " " |
2. 图形结果错误
| 错误 | 后果 | 改正 |
|---|---|---|
| 外层和内层循环次数写反 | 行列颠倒、图形方向错误 | 外层控制行,内层控制列 |
| 换行写在内层循环里 | 每个字符单独占一行 | 内层结束后再换行 |
| 忘记换行 | 所有字符挤在一行 | 补上 cout << '\n' |
| 空格数量公式写错 | 图形不居中 | 先列出每行空格数 |
| 星号数量公式写错 | 三角形或金字塔变形 | 先列出每行星号数 |
| 坐标条件少写或写反 | 点、线、图形缺少部分 | 先说清楚“哪些点要画” |
| 忘记输出空格 | 坐标图形无法保持形状 | else 中输出 ' ' |
3. 图形题自查顺序
- 行数对不对?
- 第
i行要输出几个空格? - 第
i行要输出几个星号? - 内层循环是否只处理当前行?
- 换行是否在内层循环外?
- 坐标条件是否准确覆盖了所有边和线?
- 是否输出了题目不需要的末尾空格?