跳转至

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)

执行顺序:

  1. 外层 i = 1;
  2. 内层 j 从 1 执行到 3;
  3. 内层结束,换行;
  4. 外层变成 i = 2;
  5. 内层的 j 重新从 1 开始;
  6. 重复上述过程。

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. 图形题四步法

拿到图形题,不要直接敲代码,先做四件事:

  1. 确定图形有几行;
  2. 观察第 i 行要输出什么;
  3. 找出空格、星号或数字的数量规律;
  4. 内层循环输出当前行,当前行结束后换行。

可以记成:

外层控制行,内层控制这一行;一行结束后再换行。

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. 图形打印检查表

  1. 外层循环是否控制了行数?
  2. 内层循环是否只负责当前行?
  3. 空格和星号的数量公式是否正确?
  4. 换行是否在内层循环外?
  5. 是否多输出了题目不需要的空格?

八、坐标特征法:把屏幕看成方格

按行拆分法适合规律明显的图形。遇到“某些位置输出星号,其他位置输出空格”的图形,可以让 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. 图形题自查顺序

  1. 行数对不对?
  2. 第 i 行要输出几个空格?
  3. 第 i 行要输出几个星号?
  4. 内层循环是否只处理当前行?
  5. 换行是否在内层循环外?
  6. 坐标条件是否准确覆盖了所有边和线?
  7. 是否输出了题目不需要的末尾空格?