303 lines
11 KiB
Markdown
303 lines
11 KiB
Markdown
# C++ 程序与算法:多重循环与循环嵌套
|
|
|
|
## 一、单层循环到循环嵌套
|
|
|
|
### 1. 从小任务开始
|
|
|
|
还记得怎样在同一行输出 4 个星号吗?用一个循环就可以:
|
|
|
|
```cpp
|
|
for (int j = 1; j <= 4; j++) {
|
|
cout << "*";
|
|
}
|
|
cout << '\n';
|
|
```
|
|
|
|
循环每执行一次,就输出一个星号。重复 4 次,屏幕上就有了:
|
|
|
|
```text
|
|
****
|
|
```
|
|
|
|
这里的换行为什么写在循环后面?
|
|
|
|
因为这一行的 4 个星号要先全部输出,再换行。如果把换行放进循环,每输出一个星号就换行,它们就会排成一列。
|
|
|
|
如果要输出 3 行,每行仍然是 4 个星号呢?
|
|
|
|
```text
|
|
****
|
|
****
|
|
****
|
|
```
|
|
|
|
把“输出一行并换行”的代码复制 3 遍,也能完成。不过,行数变成 100 时,复制起来就麻烦了;每行的星号数量要改,还得改很多处。
|
|
|
|
我们可以把“输出一行并换行”看成一件事,再用一个循环重复它。这样,循环里面就有了另一个循环,这种写法叫作**循环嵌套**。
|
|
|
|
### 2. 学习它有什么用?
|
|
|
|
许多题目都能像这样拆开:先重复处理一个对象,再重复处理它里面的内容。
|
|
|
|
| 场景 | 第一层任务 | 第二层任务 |
|
|
| --- | --- | --- |
|
|
| 输出矩形或三角形 | 逐行处理 | 输出当前行的内容 |
|
|
| 输出乘法表 | 确定当前行 | 计算这一行中的各个乘积 |
|
|
| 求多个阶乘的和 | 逐项处理阶乘 | 计算当前这一项的乘积 |
|
|
| 统计数字出现次数 | 枚举一个整数 | 检查这个整数的每一位 |
|
|
| 处理二维表格 | 枚举每一行 | 枚举该行中的每一列 |
|
|
|
|
算法竞赛中的枚举、模拟和表格处理,经常用到循环嵌套。以后学到数组、矩阵和图像处理时,我们还会遇到它。
|
|
|
|
## 二、核心思想:把完整的小任务放进循环体
|
|
|
|
### 1. 什么是循环嵌套?
|
|
|
|
在一个循环的循环体里,再放入另一个循环,就是循环嵌套。
|
|
|
|
```cpp
|
|
for (外层初始化; 外层条件; 外层更新) {
|
|
// 开始处理一个新的大任务
|
|
|
|
for (内层初始化; 内层条件; 内层更新) {
|
|
// 完成当前大任务中的一个小步骤
|
|
}
|
|
|
|
// 当前大任务的收尾工作
|
|
}
|
|
```
|
|
|
|
这段写法用来说明两层循环的结构;实际写程序时,需要把中文占位内容换成 C++ 语句。
|
|
|
|
外面的叫**外层循环**,里面的叫**内层循环**。两层循环也称为二重循环,题目需要时还可以嵌套三层或更多层。
|
|
|
|
判断是否嵌套,要看一个循环有没有放进另一个循环的循环体里。
|
|
|
|
```cpp
|
|
for (int i = 1; i <= 3; i++) {
|
|
cout << i;
|
|
}
|
|
for (int j = 1; j <= 4; j++) {
|
|
cout << j;
|
|
}
|
|
```
|
|
|
|
上面两个循环是先做完一个,再做另一个。它们没有互相包含,所以不属于循环嵌套。
|
|
|
|
### 2. 一次外层循环中,内层会执行多少次?
|
|
|
|
输出星号矩形时,两层循环是这样配合的:
|
|
|
|
```cpp
|
|
for (int i = 1; i <= 3; i++) {
|
|
for (int j = 1; j <= 4; j++) {
|
|
cout << "*";
|
|
}
|
|
cout << '\n';
|
|
}
|
|
```
|
|
|
|
外层每一轮都要完成一整行:
|
|
|
|
```text
|
|
外层第 1 轮:内层输出 4 个星号 → 换行
|
|
外层第 2 轮:内层输出 4 个星号 → 换行
|
|
外层第 3 轮:内层输出 4 个星号 → 换行
|
|
```
|
|
|
|
这里容易看错:外层进行一轮时,内层要从初始化开始,把当前行的 4 个星号全部输出。
|
|
|
|
输出这一行时,`i` 保持不变,内层执行 `j++` 只会改变行内位置。等这一行输出完并换行后,才轮到外层的 `i++`。
|
|
|
|
### 3. 为什么新的一行,`j` 又从 1 开始?
|
|
|
|
内层循环写的是:
|
|
|
|
```cpp
|
|
for (int j = 1; j <= 4; j++) {
|
|
cout << "*";
|
|
}
|
|
```
|
|
|
|
因为每次执行到这条 `for` 语句,程序都会先执行 `int j = 1`。所以,新的一行总是从第一个位置开始。
|
|
|
|
本例中的 `j` 只在内层 `for` 的范围内有效。内层结束后,这一轮的 `j` 就不能再使用了;下一次进入内层时,会重新创建并初始化 `j`。
|
|
|
|
### 4. 哪些语句属于外层?看大括号
|
|
|
|
下面的代码中,整个内层循环和后面的换行,都在外层的大括号里面:
|
|
|
|
```cpp
|
|
for (int i = 1; i <= 3; i++) {
|
|
// 任务 1:输出当前行的 4 个星号
|
|
for (int j = 1; j <= 4; j++) {
|
|
cout << "*";
|
|
}
|
|
|
|
// 任务 2:当前行结束,换行
|
|
cout << '\n';
|
|
}
|
|
```
|
|
|
|
C++ 根据大括号判断语句属于哪一层。初学时,建议大家把大括号写完整。即使循环体只有一条语句,也先保留它,以后添加代码时就不容易放错位置。
|
|
|
|
## 三、例题重点回顾
|
|
|
|
### 例一:输出 3 行 4 列的星号矩形
|
|
|
|
这道题要输出的图形是:
|
|
|
|
```text
|
|
****
|
|
****
|
|
****
|
|
```
|
|
|
|
一行的写法已经会了,把“输出 4 个星号,再换行”交给外层,重复 3 次就可以。
|
|
|
|
```cpp
|
|
for (int i = 1; i <= 3; i++) { // 当前是第 i 行。
|
|
for (int j = 1; j <= 4; j++) { // 当前行的第 j 个位置。
|
|
cout << '*';
|
|
}
|
|
cout << endl; // 一行全部输出后再换行。
|
|
}
|
|
```
|
|
|
|
回顾这段代码时,留意变量和换行的位置:
|
|
|
|
- `i` 控制行号;处理一行时,`i` 保持不变。
|
|
- `j` 控制行内位置;每进入新的一行,都重新从 1 开始。
|
|
- 换行放在内层结束后、外层之内,每行执行一次。
|
|
|
|
因此,星号输出 $$3\times4=12$$ 次,换行执行 3 次。
|
|
|
|
### 写循环嵌套的四个步骤
|
|
|
|
写类似的题目时,可以按这四步想:
|
|
|
|
| 步骤 | 要想清楚什么? | 星号矩形中的安排 |
|
|
| --- | --- | --- |
|
|
| ① 拆任务 | 外层和内层分别做什么? | 外层输出一整行,内层每次输出一个星号 |
|
|
| ② 判断循环范围 | 每层从哪里开始,到哪里结束? | 外层从 1 到 3,共 3 行;内层从 1 到 4,每行 4 个星号 |
|
|
| ③ 设置变量 | 变量表示什么,在哪里初始化? | `i` 表示行号;`j` 表示行内位置,每行重新从 1 开始 |
|
|
| ④ 写循环语句 | 每个动作放在哪一层才合适? | 内层输出星号,内层结束后、外层之内换行 |
|
|
|
|
遇到编号、总和这样的变量,再问自己一句:“下一轮还要接着用这个值吗?”要接着用,就在外层前初始化;每轮重新计算,就在外层内初始化。
|
|
|
|
### 例二:连续编号的数字直角三角形
|
|
|
|
输入整数 `n`,范围是 `1 ≤ n ≤ 13`,输出 `n` 行数字。第一行有 `n` 个数,以后每行少一个;编号从 1 开始,换行后接着往下数。每个数显示两位,不足两位补 0,数字之间不加空格。
|
|
|
|
`n=5` 时:
|
|
|
|
```text
|
|
0102030405
|
|
06070809
|
|
101112
|
|
1314
|
|
15
|
|
```
|
|
|
|
这道题要分清“这一行有几个数”和“当前输出哪个数”:
|
|
|
|
- 第 `i` 行比第一行少 $$i-1$$ 个数,所以第 `i` 行有 $$n-(i-1)$$ 个数,内层上界随行号变化。
|
|
- `i` 记录行号,`j` 记录行内位置,`cnt` 才是实际输出的编号。
|
|
- `cnt` 要在外层前设为 1,每输出一个数后加 1。换到下一行时,编号还要接着用,所以不能重置。
|
|
- 个位数前先输出字符 `'0'`,这一行的数字全部输出后再换行。
|
|
|
|
```cpp
|
|
int cnt = 1; // 保留编号
|
|
for (int i = 1; i <= n; i++) {
|
|
for (int j = 1; j <= n - i + 1; j++) {
|
|
if (cnt < 10) {
|
|
cout << '0';
|
|
}
|
|
cout << cnt;
|
|
cnt++;
|
|
}
|
|
cout << '\n'; // 一行结束后换行。
|
|
}
|
|
```
|
|
|
|
### 例三:计算阶乘之和
|
|
|
|
输入正整数 `n`,范围是 `1 ≤ n ≤ 20`,求 $$S=1!+2!+3!+\cdots+n!$$。
|
|
|
|
重点看乘积和 和 总和各自怎么保存:
|
|
|
|
- 外层依次处理每一项,内层从 1 乘到 `i`,算出当前的 `i!`。
|
|
- 总和 `ans` 从 0 开始,一直保留前面各项的结果;乘积 `factor` 从 1 开始,每算新的一项都要重新准备。
|
|
- 等内层乘完后,再执行 `ans += factor`。放进内层的话,计算过程中的乘积也会被加进去。
|
|
- `factor` 和 `ans` 都要用 `long long`,因为 `13!` 已经超出常见的 32 位 `int` 范围。
|
|
|
|
读入 `n` 后,关键循环如下:
|
|
|
|
```cpp
|
|
long long ans = 0; // 保留所有已完成项的总和。
|
|
for (int i = 1; i <= n; i++) {
|
|
long long factor = 1; // 每一项都从头计算。
|
|
for (int j = 1; j <= i; j++) {
|
|
factor *= j;
|
|
}
|
|
ans += factor; // 当前阶乘算完后再累加。
|
|
}
|
|
cout << ans << '\n';
|
|
```
|
|
|
|
本题 `n≤20`,用 `long long` 可以保存阶乘和总和。不过,这种类型也有上限,不能一直用它计算更大的阶乘。
|
|
|
|
#### 简单改进:利用已经算出的阶乘
|
|
|
|
上一项的阶乘已经算好了,下一项可以接着乘。利用 $$i!=(i-1)!\times i$$,就能省去内层的重复计算:
|
|
|
|
```cpp
|
|
long long ans = 0, factor = 1;
|
|
for (int i = 1; i <= n; i++) {
|
|
factor *= i;
|
|
ans += factor;
|
|
}
|
|
cout << ans << '\n';
|
|
```
|
|
|
|
这时要把 `factor` 放到循环外,因为下一轮还要接着用上一次的乘积。变量放在哪里,要看当前算法怎样使用它。
|
|
|
|
### 例四:统计数字出现的次数
|
|
|
|
输入 `n` 和 `x`,统计 1 到 `n` 的所有整数中,数字 `x` 一共出现了多少次。输入范围为 `1≤n≤1000000`、`0≤x≤9`。
|
|
|
|
这里要把“取出一个整数”和“检查它的每一位”分开:
|
|
|
|
- 外层 `for` 逐个取出从 `1~n` 的每一个整数,内层 `while` 检查这个整数的全部数位。
|
|
- 先用 `int tmp = i` 复制一份,再对副本拆位,因为外层变量 `i` 需要记住处理到哪个整数,所以不能把 `i` 给拆掉。
|
|
- 然后用 `tmp % 10` 取个位,检查完后用 `tmp /= 10` 去掉个位。等 `tmp` 变成 0,就检查完了。
|
|
- 每找到一个符合条件的数位,就计数一次。例如 `11` 里面有两个 1,要计两次。
|
|
|
|
读入 `n` 和 `x` 后,关键循环如下:
|
|
|
|
```cpp
|
|
int ans = 0;
|
|
for (int i = 1; i <= n; i++) {
|
|
int tmp = i; // 用副本去拆位,保留外层进度。
|
|
while (tmp != 0) {
|
|
int digit = tmp % 10;
|
|
if (digit == x) {
|
|
ans++;
|
|
}
|
|
tmp /= 10; // 无论是否匹配,都继续处理下一位。
|
|
}
|
|
}
|
|
cout << ans << '\n';
|
|
```
|
|
|
|
这里容易写错的是 `tmp /= 10` 的位置:无论当前数位是否匹配,都要继续处理下一位,所以它应放在 `if` 外面。注意不能直接对 `i` 拆位,外层的进度就会被改掉,反复回到 1。
|
|
|
|
## 四、要点总结
|
|
|
|
- 外层每轮完成一个完整任务,内层处理其中的重复动作。`for` 和 `while` 可以根据题目需要搭配使用。
|
|
- 写代码时,先拆任务,再判断循环范围、设置变量,最后安排循环里的语句。
|
|
- 变量要跨轮保留,就在外层前初始化;每轮要重新准备,就在外层内、内层前初始化。
|
|
- 每个元素都要做的操作放内层,一轮结束后的收尾放在内层后、外层内,最终结果等外层结束后再处理。
|
|
- 内层每轮次数相同时,可以用乘法算总次数;次数随外层变化时,就把每轮的次数加起来。
|
|
- 算过的结果有时还能接着用。像阶乘之和,就可以保留上一项,减少重复计算。
|