循环

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

  • 顺序(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,求这个数的所有位上的数字之和。

例如,输入数字 12345,因为 1+2+3+4+5=15,因此输出 15

代码实现

#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;
}

二、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 的初始值为 1,不大于 5,但也会执行一次循环,从而导致 i 增加 1 。

对比一下 while 循环:

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

cout << i << endl; // 输出:1

由于条件(i > 5)不满足,永远不会进入循环。

三、for 循环

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

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

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

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

四、选择while 还是for

大多数情况下,能用 while 实现的循环,也可以用 for 循环完成,反过来也是。

当然,既然设计了这两种不同的循环类别,一定有不同的适用场景。

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

相对来说,for 更规范一些,循环的三个条件一般是显示给出;而 while 循环更灵活一些,更关心进入循环的那个条件。

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

1. 纸张对折问题 - for

题目要求,如果给你一张无限大的白纸,白纸的厚度为 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

结果超过想像,竟然有一百多米厚。

2. 纸张对折问题 - while

同样的对折问题,换个角度来看,同样的白纸,请编写程序计算一下,对折多少次后,厚度就会超过珠穆朗玛峰呢?

我们知道,位于中国与尼泊尔边境的珠穆朗玛峰是世界最高峰,海拔高度是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

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

五、多层循环

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

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

下面来看一个例子。

示例: 打印九九乘法表

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

代码实现

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

#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;
}

代码说明

在双层循环中,通常外层循环用于控制有多少行,内层循环用于控制每一行的所有列。

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

六、循环跳转

前面都是讲的循环,但在有的时候,在循环内部,当满足一定条件时,我们希望打破正常的循环流程,该怎么办?

在 C++ 中,提供了下面几种打破正常循环流程的跳转方式:

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

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 并不算标准退出循环方式,主要用于直接返回函数的值,将控制权交给调用函数的地方,由于我们写的 C++ 本身就是在一个主函数(main)内,因此也可以用 return 来实现跳出循环的作用。

回想一下,我们在 main 函数的最后一行 return 0,当遇到这一行时,就表示直接返回 main 函数的值了(将控制权交给调用 main 函数的 C++ 编译器了,告诉调用方,该函数正常结束了)。

看下面这个程序:

#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 + 2 + ... + n 的和

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

输入数据

100

输出数据

5050

参考代码

#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;
}

理解了累加求和的做法,还可以配合条件判断来筛选,完成不同情况下的数字之和,例如,下面代码求 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 的数是否是质数,如果是质数则输出 "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 这条语句去掉,程序运行的结果会有不同吗?程序的执行过程与去掉前有什么不同呢?

九、任务练习

1. 整除几次2

题目描述

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

例如:

  • 输入 n = 4,由于 4 可以整除 2 次 2,因此输出 2
  • 输入 n = 9,由于 9 不能被 2 整除 ,因此输出 0

代码实现

#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;
}

2. 求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;
}

3. 含 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;
}

十、小结

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