C语言程序设计学习笔记:P5-循环控制


一、第三种循环

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 in时,执行循环体内的操作并让 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)=121+3141+......+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;
}

进行测试,可以看出结果正确。
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/InnerPeaceHQ/article/details/121381405
今日推荐