第 9 讲:循环的嵌套
嵌套?
什么是嵌套(Nest)?嵌套是 C 语言和其它众多编程语言里的一个重要的语法术语,它表示执行的控制流语句(if
、switch
、while
、do-while
和 for
语句统称控制流语句,它们控制着整个程序的执行流程:是条件判断来走不同分支,还是反复执行相同的执行语句)的内部继续使用这些控制流语句的写法。比如前文介绍的 if
内还有 if
的情况:
比如这个写法格式里的 j
的结果是 3,它先走了 i == 2
这个 if
条件的 else
部分,然后发现它也不满足 i < 2
的情况,所以它最终走了 i < 2
这个 if
条件里的 else
部分,获得了 j = 3
的赋值。
很有意思的是,循环也是可以嵌套使用的。正因为如此,循环和条件判断的各种语句才会产生各种不同的组合模式,产生不同的代码书写技巧,比如枚举法(Enumeration)。不过,我们先要介绍,代码到底是怎么运算执行嵌套循环的。
实例:质数的判断
最好理解的、最基础的例子就是求质数了。
质数的定义是,在把一个大于等于 2 的整数分解为多个正整数的乘积时,如果只能分解为 1 × 自己
的话,这个数就是质数(也称素数,Prime),一般常见的质数有 2、3、5、7、11、13、17、19 等。
现在我们尝试输出所有 100 以内的所有质数。我们的代码怎么写呢?
先实现判别质数的功能
首先,我们尝试用爆破的方式,对一个整数尝试进行挨个数字的试除。如果这个整数无法被整除掉,说明它只能被 1 整除掉,故这个数肯定就是质数了。比如,我们要计算 17 是否是质数(即使我们背了表,知道它是质数),逻辑大致就是从 2 开始,一直计算到 16(17 除以 17 显然等于 1,所以这个除法已经没有意义了,所以我们只要求计算到 16 即可),让 17 挨个去除以这个数,如果每一次除法得到的结果都不能被整除,说明这个数肯定就是质数了):
我们来大致看下逻辑。首先,我们为需要用到的条件进行变量的声明(定义),然后设置一个结果量 isPrime
,来表示它是否是质数(1 表示这个数是质数,0 表示这个数不是质数)。
然后考虑 while
循环里的内容。我们肯定要让 n
从 2 计算到 digit - 1
,然后尝试记录每一次试除得到的余数 remainder
。记录余数有什么用呢?在 C 语言里,判断一个数是否能被整除,用的方法是得到余数,看余数是否为 0。比如 12 % 2
的结果为 0,因为 12 是可以整除 2 的。
如果 remainder
为 0,说明被整除了。在除法过程里得到完全整除的现象,就标志着这个数肯定就不是质数了。比如在计算 15(digit
是 15)的时候,如果 n
此时是 3,那么 remainder
就是 0 了,所以 digit
不是质数,我们需要将结果量 isPrime
改为 0。 标志着这个数肯定不是质数了。
不过,如果在得到
remainder
为 0 的情况时,就标志着这个数不是质数了。既然它已经肯定不是质数了,所以此时就不必再往下算了,所以我们此时想要尝试跳到循环外面,继续执行下面的语句。不过,我们目前还没有讲解这一点,所以我们先不考虑这一点的优化。哪怕后面再次遇到余数 0 的情况,因为循环里只有对isPrime = 0
的赋值,所以局面完全不可能被反转,即isPrime
完全不可能被重新改为 1,所以大不了多几次被赋值为 0 罢了。另外,循环条件其实也可以优化,不过为了逻辑的清晰易懂,所以这里我们不提及任何有关优化的部分。
当试除循环执行完毕,发现 isPrime
还是为初始赋值的情况 1,就说明在我们试除的循环里完全没有遇到余数为 0,即被整除的情况。所以这个数 digit
应为质数,所以我们认为这个数就是质数,输出 digit
是一个质数的信息,反之输出不是质数的信息。
所以,前面给出的算法逻辑是完整的、正确的。
为遍历 2 到 100 写一层循环
那么,我们既然做出了这个模块,那么,要想输出所有质数的信息,只需要把最后 isPrime
的判断的 if
条件去掉 else
部分,然后把输出语句改为直接输出这个数字,最后再在整个执行流程的外部加入一个 while
循环,计算 2 到 100 的所有数字即可。
可以看到,Old code
代码块里面的这一大堆代码都是前文用过的代码,被我原封不动地复制过来了。我们把 digit
从 17 改为了 2。因为我们这个题目的真正目的是为了遍历前面 100 个正整数,而 0 和 1 既不是质数也不是合数,所以我们直接从 2 开始计算。
然后,程序执行到 digit <= 100
时,显然满足条件,所以进入到循环里。然后为 n
和 isPrime
进行初始化的赋值。然后进入之前的判断质数的代码块里。如果 digit
是质数,则肯定会进入到上述代码第 23 行代码里,并输出这个数字,然后紧跟一个空格,为了保证后面可能会输出的数字会和这个当前数字之间有一个空格的分隔。
然后,最后到达第 27 行代码,digit
会增大一个单位,比如 digit
从 2 变为 3,整个循环执行完毕。接着,倒回 digit <= 100
的条件处判断。显然,它还是满足要求,所以重新进入到循环里。然后又一次对 n
和 isPrime
执行初始化的赋值。
实际上,从这里就可以看出来,把这两句初始赋值写在循环里的原因。因为每一个不同的数字判断是否是质数的时候,都采用的是
n
和isPrime
两个变量。重新赋值初始值是为了避免计算过后,更改了这两个变量的数值,而我们没有重新修正数值,就会影响到程序执行,并出现没有达到预期结果的 bug。
可以看到,这一则实例用到了两层 while
循环,外层 while
循环表示的意思是,想要从 2 计算到 100,把每一个数字都判断一遍,它们是否是质数;而内层的 while
循环在这里表示的意思是,尝试把外层循环得到的当前需要计算的数字 digit
去逐个用 2 开始试除,看看这个数是否能被内层循环得到的这个数字所除尽。
其它循环的嵌套
for
循环的嵌套
while
循环的嵌套还是很简单的,不过 for
循环嵌套的理解难点就在于它的小括号的三个部分的执行顺序。只要你不要忘了 for
循环的执行顺序:初始值、条件、循环体、增量、条件、循环体、增量、条件、循环体、增量……
实际上,改写代码后,代码量更少了,逻辑也清晰了,只是代码可能会稍微难理解一些。但只要你每一步按部就班地来,就不会出现逻辑执行的顺序错误。
do-while
循环的嵌套
用得最少的就是 do-while
循环了,所以它的嵌套很多时候,虽然简单,但难理解。不过一般我们都用不上,所以这里就随便举了个例,然后改写成 do-while
循环书写。
这个例子的输出结果是
和 if
语句一样,当执行循环的语句部分只有一句话的时候,我们可以去掉大括号不写,比如:
它等价于
for
循环嵌套了例如 if
和大括号,或者 if-else
可以简写为
只要你觉得看起来条理清晰的时候,就可以这么书写。
编码技巧:枚举法
枚举法就是采用大量循环的嵌套,来枚举出每一个循环的执行逻辑的一种情况,然后挨个判断条件是否满足的逻辑。这种逻辑虽然很暴力,但枚举出的所有情况保证了程序执行的正确性。
想必很多人听说过水仙花数字,它是一个三位数,每一个位置的数字的三次方的和,等于它自己的数字。我们既然找不到解决方式,就干脆枚举每一个情况,让每一个变量表示这个数的每一个位置,大不了用三个变量 a
、b
和 c
表示百位、十位和个位。不过要注意,a
作为百位数,是不可能为 0 的,否则它就不是一个合理的三位数了。
我相信,这样的书写模式一定会让你对多重循环有一个更好的认知:首先进入 a = 1
的初始值赋值,然后显然满足条件,进入 b = 0
的初始赋值,显然满足条件,所以进入最内层的循环,c = 0
的初始值和条件。
然后全部满足,所以我们开始整合这个整数,把每一个整数合并起来变为一个整数。然后尝试对这三个数字 a
、b
和 c
执行三次方计算再求和。如果它等于 digit
,便满足条件,所以此时就算满足了水仙花数的要求,输出它即可。否则,我们就计算下一次循环。注意,此时循环还在最内层,所以执行完毕这一次循环的时候,应该走增量 c++
的这个部分,此时 c
表示的个位变为 1。然后再判断条件 c <= 9
。满足条件,进入循环,再一次判断条件是否满足。
直到 c
计算到 9 的时候,增量使得 c
变为 10,此时条件不满足。于是整个最内层的 for
循环执行完毕,发现它被包含在 b
计算的循环的这坨循环里,而这坨循环里只有关于 c
变量的循环一个部分,所以这便意味着 b
循环执行完一次操作,此时 b
才会增大一个单位(进入 b++
增量,使之变为 1)。然后再次进入 c = 0
的初始赋值处。
如果用人为的逻辑理解的话,就是把它们三个整理为 100、101、102、103、……、109、110、111、……、999 的顺序执行每一个数字。
大致逻辑如下:
一些练习
嵌套循环还需要多多练习和巩固,因为它看起来简单,实际上每一步执行起来还是费工夫的。所以我们来一些练习,希望你们能够明白和看懂它们的执行。
方阵
将会输出一个星星矩阵:
将会是一个左上三角:
将会是一个左下三角:
右上三角
将会是一个右上三角:
这样便会输出一个九九乘法表:
菱形
最难的就是这个例子了。我们尝试输出一个菱形图案。
将会得到一个菱形图案: