跳转到内容

循环

在编写程序时,程序的逻辑构成,通常有三种基本的流程结构,包括:

  • 顺序(Sequence)
  • 分支(Condition)
  • 循环(Loop)

顺序结构是代码从上到下,依次执行;分支结构往往与条件判断结构结合起来使用,代码在执行的过程中会遇到岔路,需要根据条件来选择去走哪一条路;而循环结构是在执行的过程中,当满足循环的条件判断后,就会重复执行指定的某一段代码,直到条件不满足退出循环为止。

对于多次重复做同一件事情这类问题,都可以用循环结构去完成。

前面我们已经讲过顺序结构与分支结构,下面我们来看看循环结构。

循环结构

下面是循环结构的一个简单流程示意图。

很多时候,我们需要多次重复做一件事情时,就需要用到循环结构。例如,我们要将 1 ~ 100 的所有整数打印出来,我们总不能像下面这样一行一行的打印:

cout << 1 << endl;
cout << 2 << endl;
cout << 3 << endl;
...
cout << 100 << endl;

这样要写 100 次输出语句 ,那得累死了,循环结构就是为了解决这种麻烦而设计的。

在生活中,也有许多与循环执行相关的现象。比如说在操场跑步,一圈 400 米,如果要跑 10 公里,那就要绕着操场重复跑 25 圈,也就是重复跑 25 次 400 米。

在数学中,有一些运算符号最初也是为了解决重复运算的麻烦而引入的。比如说乘法,最简单的理解,乘法就是重复的加法,在引入乘法之前,4 + 4 + 4 + 4 + 4 就要写很多遍,而引入乘法符号可以直接写成 4 × 5,其实是代表 5 个 4 相加(或 4 个 5 相加),用重复来表述,就是重复加 5 次 4 。

在 C++ 中,有下面三种方式来实现循环结构。

  • while 循环
  • do while 循环
  • for 循环

这三种方式都有自己的适用场景,在学习时注意区分他们相同和不同的地方。

while 循环

循环的初始条件
while (满足循环的条件判断):
需要重复的逻辑功能代码
每一轮循环条件的变动

例如,像前面要输出 1 ~ 100 之间的所有整数,可以像下面这样写:

int i = 1; // 循环的初始条件
while (i <= 100) { // 满足循环的条件判断
cout << i << endl; // 需要重复的逻辑代码
i = i + 1; // 每一轮循环条件的变动
}

如果把上面的 100 改成 1000,那就是打印 1 ~ 1000 的所有整数了。是不是简单太多了?

while 循环的代码时,下面这三个条件需要注意:

  1. 初值,循环的初始条件,也就是这里的int i = 1
  2. 条件,满足并进入循环的条件,也就是这里的 i <= 100, 在这里表示只有当满足 i 小于等于 100 这个条件时,才会执行循环体内的代码;如果不满足这个条件,则退出循环。
  3. 步进,每轮循环条件变动,也就是这里的 i = i + 1,大部分情况下,都需要在每一轮循环中更改循环的条件,直接某一轮条件不满足,就退出循环。

下面列表显示了打印 1 ~ 100 的代码执行过程中的数据变化。

实际上,上面的三个条件缺一不可。刚开始学习编程的同学经常会忘记掉某一个条件,造成程序执行的错误。大家可以试试,将某一个条件去掉,例如去掉第 3 个条件,比如去掉 i = i + 1,执行时会出现什么情况呢?

int i = 1;
while (i <= 100) {
cout << i << endl;
}

很可能你会发现程序卡死没反应了,这是因为初始条件 int i = 1,而1永远小于 100,也就是进入循环的条件永远是满足的,这个程序会无限执行下去,程序进入所谓的**「死循环」**状态。也这是为什么必须要有第 3 个条件的原因。

下面我们尝试更改这三个条件中的一个或多个,来完成一些小练习。

上面演示了将 1 ~ 100 内的整数数字依次打印出来,稍微改动,如果要将 1 ~ 100 内的所有奇数依次打印出来,程序该怎么写呢?

实际上,只需要更改第 3 个条件,将 i = i + 1 更改为 i = i + 2 就行了,代码如下:

# 依次打印出 1 ~ 100 内的所有奇数
int i = 1;
while (i <= 100) {
cout << i << endl;
i = i + 2;
}

想一想是不是对的,i 的初始值是 1,第 1 次执行则打印出 1,紧跟着 i = i + 2 ,这句执行后 i 就变成 3 了,再进入循环则打印出 3,依次下去,就会将 1 ~ 100 中所有的奇数打印出来。

那如果要将 1 ~ 100 内的所有偶数依次打印出来呢?只需要在打印奇数的程序基础上,再更改第 1 个条件,将初始值更改为 2 就可以了。

# 依次打印出 1 ~ 100 内的所有偶数
int i = 2;
while (i <= 100) {
cout << i << endl;
i = i + 2;
}

有的同学说,我们前面学过如何判断偶数和奇数,我们可以在循环中用一个条件判断来控制打印奇数还是偶数,可以吗?

当然可以,用这种方法来打印偶数的代码如下:

// 方法二:依次打印出 1 ~ 100 内的所有偶数
int i = 1;
while (i <= 100) {
if (i % 2 == 0) {
cout << i << endl;
}
i = i + 1;
}

活学活用,非常棒!给大家留一个小问题:仅仅从这个题目的要求来说,你觉得方法一更好一点,还是方法二更好一点呢?说说你的理由。

好吧,虽然有点无聊,但还是要「刁难」一下大家,在上面几个程序的的基础上,再稍微变化一点,要求按倒序来打印出结果,例如,同样是打印 1 ~ 100 内的所有整数,但倒着来,从 100 开始输出,一直到 1 ,程序应该怎么修改呢?

// 倒序打印出 1 ~ 100 内的所有整数
int i = 100;
while (i >= 1) {
cout << i << endl;
i = i - 1;
}

来看一个例子。

示例:奇偶数判断

题目描述

输入 nn 个整数,如果是整数则输出 “Y”;如果是奇数则输出 “N”

输入描述

输入为两行。

  • 第一行为一个正整数 nn,代表接下来要输入的数字个数
  • 第二行为 nn 个正整数,代表要判断的数字

输出描述

输出为一行,根据题目要求输出 “Y” 或 “N”

输入输出样例

输入数据

5
8 9 2 5 1

输出数据

Y N Y N N

代码实现

#include <iostream>
using namespace std;
int main() {
int k;
cin >> k;
while (k--) {
int t;
cin >> t;
if (t % 2 == 0) cout << "Y" << " ";
else cout << "N" << " ";
}
return 0;
}

代码说明

注意这里 k-- 的用法,等价于下面的写法:

while (k) {
// 代码逻辑
k--
}

直接写在 while 条件里代码更简洁一些。

另外,在 C++ 中,任何非零的数如果直接作为条件,相当于 true ,0 相当于 false,这一点需要注意。

练习: 整数数字之和

题目描述

输入一个正整数 nn,求这个数的所有位上的数字之和

输入描述

输入为一行,一个正整数 nn

输出描述

输出为一行,一个正整数,代表输入数字的所有位上的数字之和

输入输出样例

输入数据1

1234

输出数据1

10

输入数据2

234729

输出数据2

27

代码实现

#include <iostream>
using namespace std;
int main() {
int n, s = 0;
cin >> n;
while (n > 0) {
s = s + n % 10;
n = n / 10;
}
cout << s << endl;
return 0;
}

练习:整除几次2

题目描述

请问一个正整数 nn 能够整除几次 2?

比如: 4 可以整除 2 次 2 ,100 可以整除 2 次 2 , 9 可以整除 0 次 2 。

输入

从键盘读入一个正整数 nn

输出

输出一个整数,代表 nn 能够整除 2 的次数。

输入输出样例

输入数据

8

输出数据

3

代码实现

#include <iostream>
using namespace std;
int main() {
int n, cnt = 0;
cin >> n;
while (n % 2 == 0) {
n = n / 2;
cnt++;
}
cout << cnt << endl;
return 0;
}

do while 循环

do ... while 循环是 while 循环的变形,形式如下:

do {
// 循环体
} while (条件);

while 循环最大的不同是:

  • while 是先判断条件再执行循环体的内容
  • do...while 是先执行后判断条件

也就是说,使用 do...while无论是否满足条件, 都一定会执行一次循环。

来看一个简单的例子:

int i = 1;
do {
i++;
} while (i > 5);
cout << i << endl;

结果为 2,尽管 i 不大于 5,但也会执行一次循环,从而导致 i 增加 1 。

对比一下 while 循环:

int i = 1;
while (i > 5) {
i++;
}
cout << i << endl;

输出结果为 1,由于条件不满足,永远不会进入循环。

for 循环

for 循环是另一种常见的循环结构形式,它的语法格式为:

for (初值; 条件; 步进) {
// 循环的功能逻辑
}

这里的 初值条件步进 分别对应于 while 循环中的三个条件。

不同的是 while 里这三个条件是分开给出的,而 for...in 是放在一起的,在写法上比较简练,更不容易出错。

whilefor 的适用场景

whilefor 都是循环,大多时候都能做同样的事,那它们之间有什么差异呢?

  • 当明确知道循环执行的次数时,用 for 循环
  • 当不知道明确的循环执行次数时,用 while 循环

通过下面这个纸张对折的问题,大家也可以体会一下这两者的不同。

示例: 纸张对折问题

题目要求,如果给你一张无限大的白纸,白纸的厚度为 0.0001 米,请问,将对纸对折 20 次后,对折后的纸有多厚呢(以米为单位,结果保留到小数后两位)?

建议大家可以拿一张普通 A4 白纸,尽最大可能试一试,看看你能将这张纸对折多少次呢?

这个问题的核心逻辑是,每对折一次,其厚度翻一倍,通过循环可以完成,实现代码如下:

#include <iostream>
#include <cstdio>
using namespace std;
int main() {
int n;
cin >> n;
double h = 0.0001;
for (int i = 1; i <= n; i++) {
h = h * 2;
}
printf("%.2f", h);
return 0;
}

输入数据

20

输出数据

104.86

有点吓人,竟然有一百多米厚呢。

同样的对折问题,换个角度呢,我们知道,位于中国与尼泊尔边境的珠穆朗玛峰是世界最高峰,海拔高度是8848.86米。请编写程序计算一下,将纸张对折多少次后,厚度就会超过珠穆朗玛峰呢?

#include <iostream>
using namespace std;
int main() {
double k;
cin >> k;
double h = 0.0001;
int i = 0;
while (h < k) {
h = h * 2;
i = i + 1;
}
cout << i << endl;
return 0;
}

输入数据

8848.86

输出数据

27

编程对于我们理解特别大的数,以及理解数的增长是非常有用的,因为在现实生活中,我们很难去模拟出增长所带来的变化,而且编程给我们提供了这样一个无限潜能的模拟工具!

练习: 求 1 + 2 + … + n 的和

题目描述

输入一个正整数 n,求 1 + 2 + 3 + … + n 的和

输入输出样例

输入数据1

100

输出数据1

5050

输入数据2

1000

输出数据2

500500

实现代码

#include <iostream>
using namespace std;
int main() {
int n, sum = 0;
cin >> n;
for (int i = 1; i <= n; i++) {
sum = sum + i;
}
cout << sum << endl;
return 0;
}

注意,这里的最终求和的结果sum 应该放在循环外,循环是用来累加,累加完成得到最终结果再输出。

当然,也还是可以配合条件判断来完成求 1 ~ n 之间所有偶数的和,代码如下:

#include <iostream>
using namespace std;
int main() {
int n, sum = 0;
cin >> n;
for (int i = 1; i <= n; i++) {
if (i % 2 == 0) {
sum = sum + i;
}
}
cout << sum << endl;
return 0;
}

练习:求 1 + 1/2 + … + 1/n 的和

【补充内容】

练习:整除几次2

题目描述

请问一个正整数 nn 能够整除几次 2?

比如: 4 可以整除 2 次 2 ,100 可以整除 2 次 2 , 9 可以整除 0 次 2 。

输入

从键盘读入一个正整数 nn

输出

输出一个整数,代表 nn 能够整除 2 的次数。

输入输出样例

输入数据

8

输出数据

3

代码实现

#include <iostream>
using namespace std;
int main() {
int n, cnt = 0;
cin >> n;
while (n % 2 == 0) {
n = n / 2;
cnt++;
}
cout << cnt << endl;
return 0;
}

练习: Fibonacci 数列的第 n 项

题目描述

输入正整数 n,求斐波那契数列的第 n 项,并打印出来

什么是斐波那契数列:

斐波那契数列(Fibonacci sequence),又称黄金分割数列,这个数列是由意大利数学家莱昂纳多·斐波那契(Leonardo Fibonacci)在他的《算盘书》中提出而得名。

在数学上,斐波那契数是以递归的方法来定义,数学表达如下所示:

{F0=0F1=1Fn=Fn1+Fn2\begin{cases} & F_0=0 \\ & F_1=1 \\ & F_n=F_{n-1} + F_{n-2}\end{cases}

用文字来说,就是斐波那契数列是由 0 和 1 开始,之后的斐波那契数是由之前的两数相加而得。

前面的几个斐波那契数是:

1、1、2、3、5、8 …

需要注意的是,0 是第零项,而不是第一项

输入输出样例

输入数据:

8

输出数据:

21

代码实现

#include <iostream>
using namespace std;
int main() {
int n, f;
cin >> n;
int f0 = 0;
int f1 = 1;
for (int i = 2; i <= n; i++) {
f = f0 + f1; // 每项等于前两项的和
f0 = f1; // 接力往后传递 f0
f1 = f; // 接力往后传递 f1
}
if (n == 0) f = f0; // 第 0 项特殊处理
else if (n == 1) f = f1; // 第 1 项特殊处理
cout << f << endl;
return 0;
}

多层循环

多层循环通常也叫嵌套循环,也就是循环里还有循环。在几乎所有编程语言中,无论是条件分支结构,还是循环结构,都是可以多层嵌套使用的。

嵌套循环在有的时候是非常有用的,但为了便于阅读和理解代码,循环嵌套的层数不宜过多,一般来说不超过三层

下面来看一个例子。

示例: 打印九九乘法表

题目描述

利用嵌套循环打印输出九九乘法表

代码实现

这里为了显示方便,只打印了其中一部分乘法表

#include <iostream>
using namespace std;
int main() {
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= i; j++) {
printf("%d*%d=%d\t", j, i, i*j);
}
printf("\n");
}
return 0;
}

输出结果

1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
1*4=4 2*4=8 3*4=12 4*4=16
1*5=5 2*5=10 3*5=15 4*5=20 5*5=25

代码说明

本题中使用 printf() 输出更加方便一些,其中 \t 是每输出一个乘法算式后输出一个制表符(tab),制表符宽度根据不同的环境会有所不同,一般来说是 4 个或 2 个空格。

示例: 打印数字方阵

题目描述

输入一个正整数 n,输出 n 行,每行都是从1到 n 的所有正整数。

输入输出样例

输入数据:

5

输出数据:

1 2 3 4 5
1 2 3 4 5
1 2 3 4 5
1 2 3 4 5
1 2 3 4 5

实现代码

#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cout << j << " ";
}
cout << endl; // 每一行输出完后,换行
}
return 0;
}

代码说明

在双层循环中,通常外层循环用于控制有多少行,内层循环用于控制每一行应该输出什么。

参考「模式与编程」章节,可以看到更多利用双层循环输入数字或符号图案的练习。

练习: 三色球问题

题目描述

一个口袋里有 12 个球,其中有三个红球,三个白球,六个黑球 从其中取 8 个球,问有多少种颜色搭配?

实现代码

#include <iostream>
using namespace std;
int main() {
int cnt = 0;
// r 代表红球
// w 代表白球
// b 代表黑球
for (int r = 0; r <= 3; r++) {
for (int w = 0; w <= 3; w++) {
for (int b = 0; b <= 6; b++) {
if (r + w + b == 8) {
cnt++;
}
}
}
}
cout << cnt << endl;
return 0;
}

运行结果

13

代码说明

这里实际上是一种**枚举(暴力求解)**的方法,就是让程序挨着去试,只要满足 8 个球的搭配方式就计数一次,人要这样去枚举是很难的,而对于计算机来说,最适合这样「笨」的重复计算了。

有些细心同学会发现,要取 8 个球的话,至少都会取 2 个黑球,因此哪怕把红球和白球全取了,也只有 6 个球,所以最里层的循环 b 从 2 开始就行了。

for (int r = 0; r <= 3; r++) {
for (int w = 0; w <= 3; w++) {
for (int b = 2; b <= 6; b++) {
if (r + w + b == 8) {
cnt++;
}
}
}
}

确实是这样的,这样循环的次数就会少一些,对于数据量比较大的组合问题来说,改后的程序效率肯定会好一些,不过对于这样简单的组合问题来说,我们直接全量枚举即可,影响不大,反而可以避免你在考虑从多少开始时计算错误。

排列与组合问题,实际上会涉及到高中的数学知识,但有了编程这个厉害的工具,小学的孩子也可以很好的理解这类问题。

退出循环

前面都是讲的循环,但在有的时候,在循环内部,当满足一定条件时,我们希望直接退出循环,该怎么办呢?

在 C++ 中,有两种退出循环的方式:

  • break ,直接终止循环,继续执行循环以后的代码
  • continue,跳过当前这一轮循环(continue之后的代码都不会执行了),继续下一轮

1. break 终止循环

来看一个简单的例子

for(int i = 1; i <= 10; i++) {
if (i % 3 == 0) break;
cout << i << endl;
}

程序运行结果:

1
2

如果没有第 2、3 行,这段代码运行的结果是直接输出 1 ~ 10,但加上第 2、3 行, 表示一旦遇到 i 是 3 的倍数 ,循环就终止了。而第 1 次遇到 i 是 3 的倍数也就是当 i 等于 3 的时候,因此程序的运行结果只会输出 1 和 2 。

2. continue 退出本轮循环

continue 也是一种退出循环的方式,用于跳过当前一轮循环,跳过后继续进行后面的循环。

将上面示例中的 break 更改为 continue,来看看两个程序的输出有什么不同。

for(int i = 1; i <= 10; i++) {
if (i % 3 == 0) continue;
cout << i << endl;
}

程序运行结果:

1
2
4
5
7
8
10

下面再来看一个比较典型的退出循环相关的例子。

3. return 退出

严格意义上 return 并不算标准退出循环方式(但它在某些时候能起到退出循环的作用),回想一下,我们在写第 1 个 C++ 程序的时候讲过,return 0 代表程序正常结束。

也就是说,一旦遇到 return 0中,整个程序就结束了,当然任何循环也就强制结束了。

看下面这个程序:

#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
if (i % 3 == 0) {
return 0;
}
cout << i << endl;
}
cout << "bye" << endl;
return 0;
}

程序运行结果:

1
2

注意,我们程序里多加了一个,输出字符串 “bye”,但最终的结果是没有的,也正好说明了 return 0 的作用,是结束整个程序,在 return 0 后的所有代码都不会再执行了。

示例: 质数判断

题目描述

判断一个大于 1 的数是否是质数,如果是质数则输出 “Y”,否则输出 “N”。

题目分析

回顾一下质数的定义,质数(Prime number),又称素数,是指在大于 1 的自然数中,除了 1 和该自数自身外,无法被其它自然数整除的数。也就是说,一个质数,只能被 1 和它本身整除

代码实现

#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
bool is_prime = true;
for (int i = 2; i < n; i++) {
if (n % i == 0) {
is_prime = false;
break;
}
}
if (is_prime) {
cout << "Y" << endl;
} else {
cout << "N" << endl;
}
return 0;
}

这里的判断的核心逻辑在于代码中的 for 循环中的代码。

for 循环中,i 取值从 2 ~ n-1,也就是除 1 和 本身以外的数的范围,一旦有任何一次被这个范围内的数整除的情况出现,那这个数一定不是质数,剩下的就不用再判断了,直接使用 break 直接退出循环。

标志位 is_prime 是为了后面根据该标志位来输出最终判断的结果。

想一想: 如果这里把程序中的 break 这条语句去掉,程序运行的结果会有不同吗?程序的执行过程与去掉前有什么不同呢?

示例:含 0 的数有多少个

题目描述

请求出 1 ~ nn 中含有数字 0 的数,共有多少个?

输入描述

一个整数 n(n999n \le 999

输出描述

一个整数,代表 1 ~nn 中含有数字 0 的数的个数。

输入输出样例

输入数据 1

80

输出数据 1

8

输入数据 2

999

输出数据 2

180

题目解析

[待补充]

代码实现

#include <iostream>
using namespace std;
int main() {
int n, cnt = 0;
cin >> n;
for (int i = 1; i <= n; i++) {
int j = i;
while (j > 0) {
if (j % 10 == 0) {
cnt++;
break;
}
j = j / 10;
}
}
cout << cnt << endl;
return 0;
}

小结

循环结构是编程中最常用的控制结构之一,使用循环结构可以简化代码,避免重复编写相同的代码段,还可以提高代码的可读性和可维护性。同时,合理使用循环结构可以实现迭代、遍历、计数等各种功能,为程序的实现提供灵活性和效率。