本系列文章为浙江大学翁恺C语言程序设计学习笔记,前面的系列文章链接如下:
C语言程序设计学习笔记:P1-程序设计与C语言
C语言程序设计学习笔记:P2-计算
C语言程序设计学习笔记:P3-判断
C语言程序设计学习笔记:P4-循环
文章目录
一、第三种循环
1.1 for循环
问题: n n n的阶乘定义如下: n ! = 1 × 2 × 3 × . . . × n n! = 1 \times 2 \times 3 \times ... \times n n!=1×2×3×...×n。现在我要实现个功能,输入 n n n,输出 n n n的阶乘。
针对这个问题,我们进行思考,我们现在需要三个变量:
①读用户的输入需要一个int的n
②计算的结果需要用一个变量保存,可以是int的factor
③在计算中需要有一个变量不断地从1递增到n,那可以是int的i
于是我们使用学过的while循环写出代码:
#include <stdio.h>
int main()
{
int n;
scanf_s("%d", &n);
int fact = 1;
int i = 1;
while (i <= n) {
fact *= i;
i++;
}
printf("%d!=%d\n", n,fact);
return 0;
}
在代码中,我们首先初始化了一个 i i i,然后当 i ≤ n i \le n i≤n时,执行循环体内的操作并让 i + + i+ + i++。在C语言中,有另外一种语法来表达这种循环,那就是for循环。我们使用for循环重新写上面代码:
#include <stdio.h>
int main()
{
int n;
scanf_s("%d", &n);
int fact = 1;
int i = 1;
for(i=1; i<=n; i++)
{
fact *= i;
}
printf("%d!=%d\n", n,fact);
return 0;
}
我们进行调试,看for循环中每个变量每次的变化情况。我们可以看出,for循环像一个计数循环:设定一个计数器,初始化它,然后在计数器到达某值之前,重复执行循环体,而每执行一轮循环,计数器值以一定步进进行调整,比如加1或者减1。
我们可以把for念成对于。下面这个例子我们就读成:对于一开始的count=10,当
count>0时,重复做循环体,每一轮循环在做完循环体内语句后,使得count - -。
for ( count=10; count>0; count-- )
在上面求阶乘的代码中,有一些需要注意的地方:
1、变量初始值的定义:
做求和的程序时,记录结果的变量应该初始化为0(如之前记录求累加值的变量,其初始值为0)。
做求积的变量时,记录结果的变量应该初始化为1(如上面代码中我们使用fact记录结果,其初始值为1)。
2、将变量的定义写到for语句中(C99之前的标准不能这样写):
循环控制变量
i
i
i 只在循环里被使用了,在循环外面它没有任何用处。因此,我们可以把变量i的定义写到
f
o
r
for
for 语句里面去。
for(int i=1; i<=n; i++)
{
fact *= i;
}
3、一些没有意义的循环:
当我输入的
n
n
n 为1时,此时也要i节能型一次循环,而这次循环是没必要的。因此我们可以将for循环的初始条件改为int i=2。
for(int i=2; i<=n; i++)
{
fact *= i;
}
4、循环的方向:
除了可以从1乘到
n
n
n来计算
n
!
n!
n!,还可以从
n
n
n乘到1来计算。我们写出代码如下:
#include <stdio.h>
int main()
{
int n;
scanf_s("%d", &n);
int fact = 1;
int i = n;
for(; n>1; n--)
{
fact *= n;
}
printf("%d!=%d\n", i,fact);
return 0;
}
1.2 循环的计数和选择
我们经常使用这样一个循环语句:
for(int i=0;i<n;i++)
循环的次数是 n n n,而循环结束以后, i i i的值是 n n n。循环的控制变量 i i i是选择从0开始还是从1开始,是判断 i < n i<n i<n还是判断 i < = n i<=n i<=n,对循环的次数,循环结束后变量的值都有影响。我们来举个实例看看实际的结果。
#include <stdio.h>
int main()
{
int i;
for (i = 0; i < 5; i++)
{
printf("i=%d ",i);
}
printf("\n最后i=%d\n", i);
return 0;
}
我们运行来查看结果。可以看出循环了5次,
i
i
i最后的值为5,在循环体里面
i
i
i的值从0依次增加到4。
如果是这种情况:
for (i = 1; i <= 5; i++)
{
printf("i=%d ",i);
}
我们运行来查看结果。可以看出循环了5次,
i
i
i最后的值为6,在循环体里面
i
i
i的值从1依次增加到5。
值得注意的是,for循环是可以改造成对应的while循环的。
for (i = 1; i <= 5; i++)
{
fact *= i;
}
int i=1;
while (i<=n)
{
fatc *= i;
i++;
}
我们再来回顾下我们的for循环,其使用规则如下:
for ( 初始动作; 条件; 每轮的动作 )
{
}
for中的每一个表达式都是可以省略的,如for (; 条件; ) == while ( 条件 )。注意:分号不能省。
现在我们有for循环、while循环、do-while循环。当我们解决问题时,该如何选择合适的循环呢?一般而言,我们的Tips是这样的:
• 如果有固定次数,用for
• 如果必须执行一次,用do_while
• 其他情况用while
小测验
1、以下哪个循环和其他三条循环不等价(假设循环体都是一样的)?
A.for ( i=0; i<10; i++ ) {…}
B.for ( i=0; i<10; ++i ) {…}
C.for ( i=0; i++<10; ) {…}
D.for ( i=0; i<=9; i++ ) {…}
答案:C
2、以下代码段的输出是什么?
for ( int i=10; i> 1; i /=2 ) {
printf("%d ", i++);
}
答案:10 5 3 2
二、循环控制
2.1 循环控制
问题:素数就是只能被1和自己整除的数,不包括1。如:2、3、5、7、11、13、17、19…现在需要输入一个正整数,输出它是否是素数。
思路:我们需要用输入的数去除以1和它本身之外的数(即2到n-1),如果可以有可以整除的,说明就不是素数。现在我们需要一个循环,这个循环有明显的递增关系,且两边的边界也是确定的,因此我们选择使用for循环。我们写出代码如下:
#include <stdio.h>
int main()
{
int x;
scanf_s("%d", &x);
int i;
for (i = 2; i < x; i++)
{
if (x % i == 0)
{
printf("不是素数\n");
}
}
printf("是素数\n");
return 0;
}
从这段代码中我们可以看出一些问题:
①如果一个数不是素数,且有多个可以被整除的数,那么每次都会将其打印出来。
②同时,如果是素数怎么办,那说明循环走完了。如果直接在循环结束后打印是素数,这样也不对。
我们的解决办法为:我们可以新增一个变量isPrime,用于标记当前的数是否是素数。只要在循环中找到能够被整除的数,就将isPrime设置为0,说明不是素数。循环结束后根据isPrime的值来打印是否是素数。
#include <stdio.h>
int main()
{
int x;
scanf_s("%d", &x);
int i;
int isPrime = 1;
for (i = 2; i < x; i++)
{
if (x % i == 0)
{
isPrime=0;
}
}
if (isPrime == 1)
{
printf("是素数\n");
}
else
{
printf("不是素数\n");
}
return 0;
}
我们运行一下进行测试,可以看出结果正确。
这段代码有个明显的问题:当IsPrime为0了,我们就可以不用继续循环了,应该跳出循环。C语言提供了一种办法:break。break的作用就是跳出循环。因此我们在isPrime=0;的那一行代码下面加上break;
if (x % i == 0)
{
isPrime=0;
break;
}
我们来调试运行,可以看出当x可以整除i时,直接跳出了循环。
除了break,还有continue。continue的作用便是跳过这一轮循环剩下的语句,进入下一轮的循环。我们将上面代码中的break换成continue,并在后面添加一句打印的代码,进行测试:
#include <stdio.h>
int main()
{
int x=6;
int i;
int isPrime = 1;
for (i = 2; i < x; i++)
{
if (x % i == 0)
{
isPrime=0;
continue;
}
printf("%d", i);
}
return 0;
}
我们进行测试,可以看出当i=2时,可以被x整除,此时执行continue后直接跳过后面的printf,开始下一轮循环。
因此,break和continue的区别如下图所示:
2.2 嵌套的循环
问题:如何写出100以内的素数呢?
我们在2.1节中写出了如何判断一个正整数是否是素数,现在我们只需在其外面加个循环,对1-100中每个数都进行判断即可。
#include <stdio.h>
int main()
{
int x;
for (x = 1; x <= 100; x++) {
int i;
int isPrime = 1; // x是素数
for (i = 2; i < x; i++) {
if (x % i == 0) {
isPrime = 0;
break;
}
}
if (isPrime == 1) {
printf("%d ", x);
}
}
printf("\n");
return 0;
}
我们进行测试,可以看出结果正确。在这段代码中,一个for循环中出现了另一个for循环,这就叫做嵌套的循环。
问题:现在我想把程序改一下,输出前50个素数。
我们的思路如下:
1、需要有一个变量cnt,用于记录当前是第几个素数。
2、循环条件需要变化。第50个素数不一定在100内,因此循环条件不再是x<=100,而应该是cnt<50。
3、为了美观,输出的50个素数以5个为1行,共10列,同时每一列的开头需要对齐。
我们写出相应代码如下:
#include <stdio.h>
int main()
{
int x;
int cnt = 0;
x = 1;
while ( cnt <50 ) {
int i;
int isPrime = 1; // x是素数
for ( i=2; i<x; i++ ) {
if ( x % i == 0 ) {
isPrime = 0;
break;
}
}
if ( isPrime == 1 ) {
cnt ++;
printf("%d\t", x);
if ( cnt %5 == 0 ) {
printf("\n");
}
}
x++;
}
return 0;
}
我们运行一下,可以看出结果正确。我们用了一个\t,具体使用方法我们后面再深入了解。
2.3 从嵌套的循环中跳出
问题:如何用1角、2角和5角的硬币凑出10元以下的金额呢?
思考:计算机很擅长做这种计算,可以通过枚举来找出所有可能的结果。因此,我们可以使用三重循环来枚举所有结果。代码如下:
#include <stdio.h>
int main()
{
int x;
int one, two, five;
scanf("%d", &x);
for ( one = 1; one < x*10; one++ ) {
for ( two = 1; two < x*10/2; two++ ) {
for ( five = 1; five < x*10/5; five++ ) {
if ( one + two*2 + five*5 == x*10 ) {
printf("可以用%d个1角加%d个2角加%d个5角得到%d元\n",
one, two, five, x);
break;
}
}
}
}
return 0;
}
我们运行一下,可以看出所有可能的组合方式都打印出来了。
如果现在我只需要一种组合方式即可,那么第一次打印出组合方式的时候应该break。但是,break和continue都只能针对它所在的那层循环做,不能对更远的循环做。那我们可以试着在这三个for循环结束后都加上break。
#include <stdio.h>
int main()
{
int x;
int one, two, five;
scanf("%d", &x);
for ( one = 1; one < x*10; one++ ) {
for ( two = 1; two < x*10/2; two++ ) {
for ( five = 1; five < x*10/5; five++ ) {
if ( one + two*2 + five*5 == x*10 ) {
printf("可以用%d个1角加%d个2角加%d个5角得到%d元\n",
one, two, five, x);
break;
}
}
break;
}
break;
}
return 0;
}
这样明显也不对。因为我们需要最内层的break执行了才去执行最外层的两个break,而这里不管最内层的for循环怎么结束的,外面的两个break都会做。因此,我们需要另外一个变量exit来标记最内层的break要做。如果exit为1,那么久执行外面的两个break。我们写出代码如下,这种break结构叫做接力break。
#include <stdio.h>
int main()
{
int x;
int one, two, five;
int exit = 0;
scanf("%d", &x);
for ( one = 1; one < x*10; one++ ) {
for ( two = 1; two < x*10/2; two++ ) {
for ( five = 1; five < x*10/5; five++ ) {
if ( one + two*2 + five*5 == x*10 ) {
printf("可以用%d个1角加%d个2角加%d个5角得到%d元\n",
one, two, five, x);
exit = 1;
break;
}
}
if ( exit == 1 ) break;
}
if ( exit == 1 ) break;
}
return 0;
}
我们运行一下,可以看出结果正确。
上面的接力break是传统的方法,我们有一个新的方法,那就是goto。我们直接在需要离开循环的地方放上goto语句。goto后面接一个标号,这个标号是自己设置的,需要放在程序的某一个地方,以冒号:结尾。我们写对相应代码如下:
#include <stdio.h>
int main()
{
int x;
int one, two, five;
scanf("%d", &x);
for ( one = 1; one < x*10; one++ ) {
for ( two = 1; two < x*10/2; two++ ) {
for ( five = 1; five < x*10/5; five++ ) {
if ( one + two*2 + five*5 == x*10 ) {
printf("可以用%d个1角加%d个2角加%d个5角得到%d元\n",
one, two, five, x);
goto out;
}
}
}
}
out:
return 0;
}
运行一下,可以看出当条件满足时,直接跳到了out那个地方。goto最适合的场景便是需要在条件满足时从嵌套的循环内层跳到外面去,其它场合不要使用。
小测验
1、以下代码段的输出是:
int sum = 0;
for ( int i=0; i<10; i++ ) {
if ( i%2 ) continue;
sum += i;
}
printf("%d\n", sum);
答案:20
2、以下代码段的输出是:
int sum = 0;
for ( int i=0; i<10; i++ ) {
if ( i%2 ) break;
sum += i;
}
printf("%d\n", sum);
答案:0
三、循环应用
3.1 前n项求和
问题:现在我要求这样一个式子的和: f ( x ) = 1 + 1 2 + 1 3 + 1 4 + . . . . . . + 1 n f(x) = 1 + \frac{1}{2} + \frac{1}{3} + \frac{1}{4} + ...... + \frac{1}{n} f(x)=1+21+31+41+......+n1
由于循环的首位分别是1和n,且是递增的,那我们可以使用for循环。代码如下:
#include <stdio.h>
int main()
{
int n;
int i;
double sum = 0.0;
scanf("%d", &n);
for (i = 1; i <= n; i++) {
sum += 1.0 / i;
}
printf("f(%d)=%f\n", n,sum);
return 0;
}
运行一下,可以看出结果正确。
如果我把问题改一下:
f
(
x
)
=
1
−
1
2
+
1
3
−
1
4
+
.
.
.
.
.
.
+
1
n
f(x) = 1 - \frac{1}{2} + \frac{1}{3} - \frac{1}{4} + ...... + \frac{1}{n}
f(x)=1−21+31−41+......+n1。
我们可以设置一个变量sign,每一轮循环将其乘-1。因此第一轮便是1,第二轮是-1,第三轮是1,第四轮是-1…我们写出代码如下:
#include <stdio.h>
int main()
{
int n;
int i;
double ret = 0.0;
int sign = 1;
scanf("%d", &n);
for (i = 1; i <= n; i++) {
ret += 1.0*sign / i;
sign = -sign;
}
printf("%f\n", ret);
return 0;
}
运行,可以看出结果正确。
3.2 整数分解
问题:输入一个非负整数,正序输出它的每一位数字。输入13425,输出1 3 4 2 5。
在第四章我们做过逆序地输出数字。逆序比较好做,我们直接%10就能得到最后的那位数。现在我们需要正序地输出,而且每一位数字中间还要有个空格。现在我们开始来分析如何做。
1、现在我有了一个输入的x,我要取出每一位并在各个数字间打印出空格,我们的做法是:
#include <stdio.h>
int main()
{
int x = 13245;
//scanf_s("%d", &x);
//逆序地取各个数字
do {
int d = x%10;
printf("%d ", d); //打印数字和空格
x /= 10;
} while (x > 0);
printf("结束");
return 0;
}
我们进行调试,可以发现这里将其每一位数字进行了逆序输出,但是最后多了个空格。
2、当到最后一个数字时,我们不应该输出空格。由于最后一位数字一定是个位数,小于10,因此我们让x>=10时才输出空格。
#include <stdio.h>
int main()
{
int x = 13245;
//scanf_s("%d", &x);
do {
int d = x%10;
printf("%d", d); //这里就不输出空格了,在下面if中判断后输出
if (x > 9) //x不为个位数时打印空格
{
printf(" ");
}
x /= 10;
} while (x > 0);
printf("结束");
return 0;
}
运行一下,可以看出多余的空格去掉了。
3、现在这些数是逆序的,我们如何将其倒过来呢?我们之前还做个一件事,对整数求逆,如输入整数123,输出整数321。因此,我们在上面代码的前面部分加上整数求逆的代码。
#include <stdio.h>
int main()
{
int x = 13245; //先用这个数来测试
//scanf_s("%d", &x);
//整数求逆的部分
int t = 0;
do {
int d = x%10;
t = t*10+d;
x /= 10;
}while(x>0);
printf("x=%d,t=%d\n", x, t);
x = t; //此时x的值已经是0了,所以需要将逆序后的值给它
//打印输出的部分
do {
int d = x%10;
printf("%d", d);
if (x > 9)
{
printf(" ");
}
x /= 10;
} while (x > 0);
printf("\n");
return 0;
}
我们运行一下,可以看出结果正确。
4、但是,对于一些特殊情况,输出的值不正确。例如我输入700,输出为7。
因此,先逆序再逆序的操作只适用于末尾不是0的情况。我们还记得在求逆的那个题目中,我们如果输入为13425,要得到最高位,那么我们使用13425/10000就能得到1。 因此,我们的思路如下:
x=13425
13425 / 10000 ->1 依次把这个
13425 % 10000 ->3425
10000 / 10 ->10
3425 / 1000 ->3 这个
3425 % 1000 ->425
1000 / 10 ->100
425 / 100 ->4 这个
425 % 100 ->25
100 /10 ->10
25 /10 ->2 这个
25 % 10 ->5
10 / 10 ->1
5 / 1 ->5 这个拿出来,就是我们的结果。
5 % 1 ->5
1 / 10 ->0
我们开始重新写我们的代码。定义一个变量mask=10000,用于记录当前需要用于除和取余的除数。
#include <stdio.h>
int main()
{
int x = 13425;
//scanf("%d", &x);
int mask = 10000;
do {
int d = x / mask;
//printf("%d", d);
//if (x > 9)
//{
// printf(" ");
//}
x %= mask;
mask /= 10;
printf("x=%d,mask=%d,d=%d\n", x, mask, d);
} while (x > 0);
printf("\n");
return 0;
}
我们运行一下,看起来结果还不错。
5、但是如果我们输入一些特殊情况,如70000。可以看出结果出问题了,只输出了一个7。
我们进行分析,可以看出70000%10000=0。此时就结束循环了,但是mask还不为0,需要继续循环。因此,while循环的条件有问题,正确的循环条件应该是mask>0。
70000 / 10000 ->7
70000 % 10000 ->0
我们将while循环条件进行更改,再次测试,可以看出结果好像是对的。
6、于是我们将注释掉的if语句取消注释,然后看一下输出是怎样的。可以看出,结果不太对,后面的0之间没有用空格隔开。
这是因为x在后面一直为0,因此不能将x作为if的判断条件,应该以mask来判断。我们将if中的x修改为mask,可以看出结果正确。
7、但是还有个问题:我们代码中的mask是一个固定的值,如果对于四位数,mask应该为1000,对于三位数,mask应该为100。因此,我们可以通过判断输入数字的位数来设置mask的值。
#include <stdio.h>
int main()
{
int x = 13425;
//scanf("%d", &x);
int mask = 1;
do {
x /= 10;
mask *= 10;
}while(x>0);
printf("mask=%d\n",mask);
do {
int d = x / mask;
printf("%d", d);
if (mask > 9)
{
printf(" ");
}
x %= mask;
mask /= 10;
} while (mask > 0);
printf("\n");
return 0;
}
我们运行一下,发现mask后面多了1个0,因此我们让循环少跑一次,while里面的判断条件由x>0更改为x>9。此时结果正确。
8、但是当我们输入1时,结果不对。
我们可以看出,针对个位数我们的结果会错是因为我们用了do-while循环。不管怎么样,mask都会先乘10。因此,我们使用while循环。同时,在第一个while循环中,x的值被改变,但是x又需要以原先的值参与到后面一个while的运算中去。因此我们将x赋值给t,在第一个循环中用t去做计算。最终代码如下:
#include <stdio.h>
int main()
{
int x;
scanf("%d", &x);
int mask = 1;
int t=x;
while (t > 9)
{
t /= 10;
mask *= 10;
}
printf("x=%d,mask=%d\n",x,mask);
do {
int d = x / mask;
printf("%d", d);
if (mask > 9)
{
printf(" ");
}
x %= mask;
mask /= 10;
} while (mask > 0);
printf("\n");
return 0;
}
我们测试几个数字1、10、213。可以看出结果正确。
3.3 求最大公约数
问题:输入两个数a和b,输出它们的最大公约数。如输入12 18,输出6。
思路1:我们可以使用枚举来解决这个问题。
①令t为要求的最大公约数,初始值为2
②如果输入的两个数u和v都能被t整除,则记下这个t
③t加1后重复第2步,直到t等于u和v中较小的那个值
④曾经记下的最大的可以同时整除u和v的t就是最大公约数
代码如下:
#include <stdio.h>
int main()
{
int a,b;
int min;
scanf("%d %d", &a, &b);
if ( a<b ) {
min = a;
} else {
min = b;
}
int ret = 0;
int i;
//i从1开始是因为当两个数没有除1以外的公约数时,输出1
for ( i = 1; i < min; i++ ) {
if ( a%i == 0 ) {
if ( b%i == 0 ) {
ret = i;
}
}
}
printf("%d和%d的最大公约数是%d.\n", a, b, ret);
return 0;
}
运行,可以发现结果正确。
思路2:枚举虽然很容易理解,但是效率不高,因为我们要尝试所有的数。我们有效率更高的方法:辗转相除法。
辗转相除法:
1. 如果b等于0,计算结束,a就是最大公约数;
2. 否则,计算a除以b的余数,让a等于b,而b等于那个余数;
3. 回到第一步。
我们手动进行计算:
a b t(余数)
12 18 12
18 12 6
12 6 0
6 0
代码如下:
#include <stdio.h>
int main()
{
int a,b;
int t;
scanf("%d %d", &a, &b);
int origa = a;
int origb = b;
while ( b != 0 ) {
t = a%b;
a = b;
b = t;
printf("a=%d,b=%d,t=%d\n",a,b,t);
}
printf("%d和%d的最大公约数是%d.\n", origa, origb, a);
return 0;
}
进行测试,可以看出结果正确。