Initial commit: C++ courseware

This commit is contained in:
xiaolinquan committed 2026-11-05 22:51:02 +08:00
commit 4a4ee7c287
1789 files changed
+373489

No files matched your search

@@ -0,0 +1,195 @@
## C++ `for` 循环(一) 讲义
### **1. 引言**
#### **问题引入**
我们来看一个非常经典的题目:**在屏幕上打印从 1 到 10 的所有整数**。
用我们学过的 `while` 循环,可以这样实现:
```cpp
int main() {
int i = 1; // 1. 循环变量初始化(起点)
while (i <= 10) { // 2. 循环条件判断(终点)
cout << i << endl;
i++; // 3. 循环变量更新(推进)
}
return 0;
}
```
这段代码完全正确,但仔细观察一下:为了完成这个循环,我们定义了三个关键部分:**初始化 `i`、判断 `i`、更新 `i`**。但这三个兄弟却被分散在了代码的三个不同位置。如果循环体很长,更新语句 `i++` 可能会离 `while` 很远,一不小心就容易忘记写,从而导致无限循环!
**思考一下**:有没有一种方法,能把这三个控制循环的核心要素“捆绑”在一起,让我们的意图一目了然呢?
#### **价值阐述**
C++ 的设计者们也注意到了这个问题。对于这种**“次数确定”**或**“范围已知”**的循环(比如循环10次,从1循环到100等),他们专门设计了一种语法结构,将循环的“三要素”集中管理。这就是 `for` 循环。
---
### **2. 核心思想**
#### **思路铺垫**
`while` 循环的逻辑是分散的,而 `for` 循环的核心思想就是**“集中管理”**。它让我们把所有控制循环进程的操作都放在一起。
#### **`for` 循环的语法**
让我们来看看 `for` 循环的语法长什么样(非常重要!):
```cpp
for (① 循环变量初始化; ② 循环条件判断; ③ 循环变量更新) {
// 循环体...
}
```
它由三个部分组成,用**分号 `;`** 隔开:
1. **`① 循环变量初始化`**
* **作用**:设置循环的**起点**。比如 `int i = 1;`。
* **执行时机**:**只在**整个 `for` 循环开始时执行**唯一一次**。
2. **`② 循环条件判断`**
* **作用**:决定循环**是否继续**。这是一个布尔表达式,比如 `i <= 10;`。
* **执行时机**:**每一次**循环体执行**之前**,都会进行判断。如果为 `true`,就执行循环体;如果为 `false`,循环立即结束。
3. **`③ 循环变量更新`**
* **作用**:让循环**向前推进**,确保循环最终能够结束。比如 `i++`。
* **执行时机**:**每一次**循环体执行**之后**,都会执行这个更新操作。
#### **步骤拆解:`for` 循环是如何运行的?**
我们可以把 `for` 循环想象成在操场上跑步:
`for (int i= 1; i<= 5; i++)`
1. **【准备开跑】 (执行 ① 初始化)**
* `int i= 1;` (我准备从第1圈开始跑)
* 这个动作只在最开始做一次。
2. **【检查】 (执行 ② 条件判断)**
* `i<= 5?` (我跑完的圈数是否小于等于5圈?)
* **当前是第1圈**:`1 <= 5` -> `true`。好的,可以跑!
3. **【执行任务】 (执行循环体)**
* `{ ... }` (跑一圈!)
4. **【更新状态】 (执行 ③ 更新)**
* `i++` (圈数加一,我现在跑完1圈,准备跑第2圈了)
* `i` 的值现在变成了 `2`。
5. **【回到第2步,开始新一轮】**
* **跑前检查**:`i<= 5?` (当前是第2圈) -> `2 <= 5` -> `true` -> 继续跑!
* **执行任务**:跑一圈!
* **更新状态**:`i++` -> `i` 变成 `3`。
* ...这个过程不断重复...
6. **【循环终点】**
* ...当 `i` 变成 `5` 时,检查 `5 <= 5` 依然为 `true`,跑完第5圈。
* **更新状态**:`i++` -> `i` 变成 `6`。
* **跑前检查**:`i<= 5?` (当前是第6圈) -> `6 <= 5` -> `false`。
* **条件不成立,循环立即结束!** 程序跳出 `for` 循环,执行后面的代码。
**执行流程**:**`① -> (② -> 循环体 -> ③) -> (② -> 循环体 -> ③) -> ... -> ② (条件为false) -> 结束`**
---
### **3. 实例**
#### **案例:打印1到5**
用刚才学到的知识,写出通过 `for` 循环打印输出1到5的代码。
```cpp
for (int i = 1; i <= 5; i++) {
cout << i << endl;
}
```
#### **分步演示**
| 轮次 | 检查 `i <= 5`? | `i` 的值 | 循环体操作 (输出) | 更新 `i++` 后, `i` 变为 |
| :-------- | :--------------- | :--------------- | :---------------- | :---------------------- |
| **开始** | - | `1` (来自初始化) | - | - |
| **第1轮** | `1 <= 5` (True) | 1 | 输出 `1` | `2` |
| **第2轮** | `2 <= 5` (True) | 2 | 输出 `2` | `3` |
| **第3轮** | `3 <= 5` (True) | 3 | 输出 `3` | `4` |
| **第4轮** | `4 <= 5` (True) | 4 | 输出 `4` | `5` |
| **第5轮** | `5 <= 5` (True) | 5 | 输出 `5` | `6` |
| **第6轮** | `6 <= 5` (False) | 6 | **不执行** | **循环结束** |
---
### **4. 代码实现**
现在,我们来解决书本中的 `Apples Plus` 例题。
**问题简述**:输入一个数 L,程序需要重复L次,打印 "Today, I ate x apple(s).",其中x从1变到L。当x是1时,单词是`apple`,否则是`apples`。
#### **分析问题**
1. **重复性**:要打印L句话,这是典型的重复任务 -> **用循环**。
2. **规律性**:句子里的数字 `x` 从1递增到L -> 这和`for`循环的计数器`i` 的变化一模一样!-> **确定使用`for`循环**,循环范围是 `1` 到 `L`。
3. **特殊性**:第1句话和后面的话格式略有不同 (`apple` 和 `apples`) -> 在循环内部需要根据当前是第几次循环(也就是`i`的值)来做一个**判断** -> **用 `if` 语句**。
#### **完整代码**
```cpp
int main() {
int L;
cin >> L;
// 搭建一个从 1 循环到 L 的 for 循环框架
for (int i = 1; i <= L; i++) {
// 在循环的每一次,变量 i 的值都代表当前是第几个苹果
// 先打印每一句都相同的部分
cout << "Today, I ate " << i << " apple";
// 根据循环变量 i 的值,判断是否需要加 's'
if (i > 1) {
cout << "s";
}
// 打印句末的句号和换行符
cout << "." << endl;
}
return 0;
}
```
#### **代码解读**
* `for (int i = 1; i <= L; i++)`: 准备一个计数器`i`,从`1`开始,只要它没超过`L`,就要一直重复循环。每做完一次,会把`i`加一。
* `if (i > 1)`: `i` 不仅仅是控制循环次数的工具,它还携带了“当前是第几次”的信息,我们可以直接用它来参与逻辑判断。
---
### 5. 常见问题与易错点
1. **无限循环**
* **代码**: `for (int i = 1; i <= 5; i--)`
* **错误**: 更新部分 `i--` 让 `i` 的值不断变小(1, 0, -1, ...),导致 `i <= 5` 这个条件永远为真。
* **修正**: 应改为 `i++`。
2. **循环一次都不执行 (零次循环)**
* **代码**: `for (int i = 5; i < 5; i++)`
* **分析**: 初始化 `i` 为5后,第一次进行条件判断 `i < 5`,即 `5 < 5`,结果为 `false`。因此,循环体一次都不会执行。这是 `for` 循环的正常行为。
3. **分号与逗号**
* **错误**: `for (int i = 1, i <= 10, i++)`
* **正确**: `for (int i = 1; i <= 10; i++)`
* **切记**:`for` 循环的三个部分之间必须用**分号 `;`** 分隔!
---
### **6. 总结**
* **`for` 循环** 非常适合用于处理**次数确定**的循环。
* **三要素**:**初始化** (只执行1次)、**条件判断** (每次循环前)、**更新** (每次循环后)。
* **执行流程**:初始化 -> (判断 -> 循环体 -> 更新) -> (判断 -> 循环体 -> 更新) -> ...
* **循环变量 `i`** 它既可以是**计数器**,也是**当前循环了多少次的信息**,可以参与循环体内部的运算和判断。
* **循环嵌套分支**:在 `for` 循环内部使用 `if` 可以解决很多“普遍规律中存在特殊情况”的问题。
@@ -0,0 +1,195 @@
## C++ `for` 循环(二) 讲义
### **1. 引言**
#### **问题引入**
在上一节课,我们学习了 `for` 循环,并用它解决了“重复打印N次”的问题,我们知道 `for` 循环是一个强大的**计数器**。但如果只是用它来数数,那可就太小看它了!
现在,我们来看另一个问题:
老师现在要大家一个一个地报上自己的学号,但我不知道咱们班有多少同学。在我听完所有人的学号后,我需要立刻说出那个**最小**的学号。
作为老师,我应该怎么做?需要把几十个学号都记在脑子里吗?太难了!我们的大脑会自动采用一种更聪明的策略:只记住“到目前为止听到的那个最小学号”,然后用新报上来的学号和这个“记录”进行比较。
#### **价值阐述**
今天我们将要学习的,是 `for` 循环在算法中最常见的三种应用模式:
1. **打擂台模式**:用于在一堆数据中寻找“最”值(最大、最小、最长、最强...)。
2. **累加模式**:用于对一连串数据进行求和、求积等累积性计算。
3. **分类统计模式**:用于遍历数据,并根据不同条件将它们归类、分别统计。
掌握了这三大模式,就能解决诸如数据分析、统计、模拟等一大批实际问题。
---
### **2. 核心思想与模式**
#### **模式一:打擂台算法 —— 寻找“最”值**
这是解决“求最大/最小值”问题的模板。让我们用书本上**例4-2** “找最小值”的例子,来学习这个算法。
**核心思想**:**==只记住当前最优值,然后不断与新的挑战者进行比较==**。
**步骤拆解**:
1. **【搭建擂台】**
* **比喻**:比赛开始前,我们需要一个擂主。为了保证第一个挑战者一定能赢,我们故意请一个最弱的虚拟擂主站在台上。
* **代码实现**:我们需要一个变量 `min_val` 来扮演“擂主”。因为我们要找最小值(越小越强),所以最弱就意味着一个**非常大**的数。
* `int min_val = 100000000;` // 一个足够大的初始值
2. **【挑战者依次上台】**
* **比喻**:n个选手排好队,一个接一个上台挑战。
* **代码实现**:我们用 `for` 循环n次,每次在循环体内读入一个新的“挑战者”。
* `for (int i = 0; i < n; i++) { cin >> tmp; ... }`
3. **【判断胜负,更新擂主】**
* **比喻**:如果挑战者比擂主“更强”(在这里是数值更小),他就挑战成功,成为新擂主。否则,擂主守擂成功。
* **代码实现**:在循环体内,用一个 `if` 语句来判断。
* `if (tmp < min_val) { min_val = tmp; }`
4. **【比赛结束,留在最后的就是冠军】**
* **比喻**:所有人都挑战完毕后,最后还站在台上的,就是真正的冠军。
* **代码实现**:当 `for` 循环结束后,`min_val` 变量里存储的值,就是我们要找的最小值。直接输出它即可。
---
#### **模式二:累加模式 —— 求和**
**核心思想**:准备一个容器变量,在循环中不断将自身和新的东西加在一起,最终越滚越大。
**步骤拆解(往存钱罐里放硬币)**:
1. **【准备一个空的存钱罐】**
* **比喻**:在开始存钱之前,你必须有一个**空**的存钱罐。
* **代码实现**:定义一个求和变量 `sum`,并**必须**将它初始化为 `0`。这个操作**必须在循环外**完成。
* `int sum = 0;`
2. **【确定要放哪些硬币】**
* **比喻**:我们要依次放入面值为 1, 2, 3, ..., 100 的硬币。
* **代码实现**:用 `for` 循环来生成 1 到 100 这一串数字。
* `for (int i = 1; i <= 100; i++) { ... }`
3. **【放硬币】**
* **比喻**:把当前这枚硬币 `i` 丢进存钱罐 `sum`。
* **代码实现**:存钱罐里新的总数,等于它原来的数,加上当前硬币的面值。
* `sum = sum + i;` (或简写为 `sum += i;`)
**关键**:求和变量 `sum` 的初始化**必须**在循环外部。如果放在循环内部,它每一轮都会被“清空”回0,那永远也攒不下钱!
---
### **3. 分类统计模式(重要)**
例如想象一个大型的快递分拣中心需要分类统计:
* 有一个**传送带**,上面源源不断地运来包裹。
* 传送带旁边站着一个**分拣员**,他的任务是看每个包裹的目的地,然后把它放到对应的城市货车里(比如北京、上海、广州)。
* 当所有包裹都处理完后,我们就可以很轻松的统计出每辆对应的货车里装了多少包裹。
在编程中,这个模式的逻辑是完全一样的,我们通过书本上实际的案例进行讲解。
#### **案例:例4-3 分类求平均数**
**问题**:从1到n这些数中,是k的倍数分为A类,不是的分为B类。求A、B两类数各自的平均数。
**分析**:`for` 循环像一个**传送带**,负责把1到n的数字一个一个送过来;循环内部的 `if/else` 像一个**分拣员**,判断这个数字该去A队还是B队。
**总结**起来就是:**分类统计模式中 ==`for` 循环负责遍历所有的数据,而 `if/else` 判断负责对这些数据挨个进行分类并依照规则进行处理==**。
#### **分步演示**
1. **准备工作(循环外)**
* 我们需要统计A、B两队的“总分”和“人数”,才能求平均分。所以,我们需要4个“存钱罐”!
* `double Asum = 0, Bsum = 0;`
* `int Acount = 0, Bcount = 0;`
2. **循环**
* 我们需要遍历从1到n的所有数字。
* `for (int i = 1; i <= n; i++) { ... }`
3. **分类进行处理**
* 每从传送带上拿到一个数字 `i`,就要判断它是不是k的倍数 (`i % k == 0`)。
* 使用 `if-else` 结构:
* 如果为真,是A队成员。就把 `i` 累加到 `Asum`,并让 `Acount` 加1。
* 否则,是B队成员。就把 `i` 累加到 `Bsum`,并让 `Bcount` 加1。
4. **计算结果(循环后)**
* 当循环结束,我们就得到了两队各自的总分和人数。
* 分别计算平均分 `Asum / Acount` 和 `Bsum / Bcount` 并输出。
---
### **4. 代码实现**
#### **找最小值(打擂台)**
```cpp
int main() {
int n;
cin >> n; // 输入数字的总个数
int min_val = 100000000; // 储存擂主,初始化为极大值,确保第一个人能打败它
int tmp; // 用于存放当前的挑战者
for (int i = 0; i < n; i++) {
cin >> tmp; // 读入一个挑战者
// 如果挑战者更小,就更新擂主
if (tmp < min_val) {
min_val = tmp;
}
}
// 循环结束,输出最终的擂主
cout << min_val << endl;
return 0;
}
```
#### **求1到100的和(累加)**
```cpp
int main() {
// 准备一个空的存钱罐
int sum = 0;
// 循环生成 1 到 100 的硬币
for (int i = 1; i <= 100; i++) {
// 把当前的硬币 i, 放入存钱罐 sum
sum += i;
}
// 循环结束后,输出存钱罐里的总数
cout << sum;
return 0;
}
```
---
### **5. 深化**
#### **变量的==作用域==**
这是一个非常重要的概念!
* **循环内的临时工**:在 `for (int i = 0; ...)` 中定义的变量 `i`,它的“生命周期”仅限于这个 `for` 循环。一旦循环结束,变量 `i` 就会消失,在循环外部无法使用它。
* **循环外的长期工:像 `sum`, `min_val`, `Acount` 这些需要在循环结束后仍然保留其计算结果的变量,**必须**在循环外部**定义。
#### **工具箱:`min()` 和 `max()` 函数**
对于打擂台算法中的找到两个数中的最小值,C++提供了一个更便捷的工具。`min(a, b)` 函数会直接返回a和b中较小的那一个。
* **使用前提**:需要包含头文件 `#include <algorithm>`。
* **简化代码**:打擂台算法中的核心判断 `if (tmp < min_val) { min_val = tmp; }` 可以被简化为一句话:
`min_val = min(min_val, tmp);`
* 这句话的意思是:“新的擂主将会被赋值为‘旧擂主’和‘挑战者’中更小的那一个”,逻辑等价,但代码更简洁。
---
### **6. 总结**
* **打擂台模式**:`初始化最值 -> 循环遍历打擂台 -> if判断更新`,用于求最大/最小值。
* **累加模式**:`初始化累加器=0 -> 循环遍历 -> sum += i`,用于求和。
* **分类统计模式**:`for` 循环提供数据,`if-else` 进行分类处理,而累加器分别统计不同的分类数量。
* **变量定义位置**:准备工作(初始化)在**循环外**,重复性处理在**循环内**。