第2章:循环结构程序设计

顺序结构程序自上到下只执行 一遍,而分支结构中甚至有些语句可能一遍都执行不了。换句话说,为了让计算机执行大量 操作,必须编写大量的语句。能不能只编写少量语句,就让计算机做大量的工作呢?这就是 本章的主题。基本思路很简单:一条语句执行多次就可以了。但如何让这样的程序真正发挥 作用,可不是一件容易的事。


2.1 for循环


for循环的格式为:for(初始化;条件;调整)循环体

程序2-1 输出1,2,3,…,n的值

#include <stdio.h>
int main()
{

	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		printf("%d\n",i);
	}
	return 0;
/*
   下面的程序运行结果是什么?“!=”运算符表示“不相等”。
   提示:请上机实验,不要凭主观感觉回答。
	double i;
	for(i=0;i!=10;i+=0.1)
		printf("%.lf\n",i);
	return 0; 
	结果:无限循环.浮点数进行小数运算的时由于精度问题,会有很小的误差,
	然而用 = 或者 != 这样的运算符来比较,检测出这种误差的.所以导致结果的不正确. 
	多试一下,将循环条件改为 i != 0.1 或者 i != 0.2时,程序能够正常运行,得正常结果.
	但是当i != 0.3时,就是无限循环.显然,程序中,这种不确定的错误是不应该存在,
	在定义循环变量时,尽量采用int型及整数的加减
*/
}

程序2-2 输出所有形如aabb的4位完全平方数(即前两位数字相等,后两位数字也相等)。

分支和循环结合在一起时功能强大:下面枚举所有可能的aabb,然后判断它们是否为完 全平方数。注意,a的范围是1~9,但b可以是0。主程序如下:

for(int a = 1; a <= 9; a++)  
	for(int b = 0; b <= 9; b++)   
		if(aabb是完全平方数) printf("%d\n", aabb);

上面的程序并不完整——“aabb是完全平方数”是中文描述,而不是合法的C语言表达式,而aabb在C语言中也是另外一个变量,而不是把两个数字a和两个数字b拼在一起(C语言中的变量名可以由多个字母组成)。
思路一:如何判断n是否为完全平方数?第1章中用过“开平方”函 数,可以先求出其平方根,然后看它是否为整数,即用一个int型变量m存储sqrt(n)四舍五入后的整数,然后判断 m 2 m^2 m2是否等于n。函数floor(x)返回不超过x的最大整数。

程序2-2完整代码 7744问题(1)
//输出所有形如aabb的4位完全平方数(即前两位数字相等,后两位数字也相等)。
#include <stdio.h>
#include <math.h>
int main()
{
    for(int a=1;a<=9;a++)
        for(int b=0;b<=9;b++)
        {
//            if(aabb是完全平方数)
//            printf("%d\n",aabb);
              int n = a*1100+b*11;
              int m = floor(sqrt(n)+0.5);
              /*为了减小误差的影响,一般改成四舍五入,即floor(x+0.5)(2)。
			  如果难以理 解,可以想象成在数轴上把一个单位区间往左移动0.5个单位的距离。
			  floor(x)等于1的区间 为[1,2),
			  而floor(x+0.5)等于1的区间为[0.5,1.5)。
			  */
              if(m*m==n)printf("%d\n",n);
        }
	return 0;
}

你可能会问:可不可以这样写?if(sqrt(n)== floor(sqrt(n)))printf("%d\n",n);,即直接判断sqrt(n)是否为整数。理论上当然没问题,但这样写不保险,因为浮点数的运算(和函数)有可能存在误差。

思路二:枚举平方根x,从而避免开平方操作。

程序2-3完整代码 7744问题(2)
#include<stdio.h>
int main() 
{  
	for(int x = 1; ; x++){   
		int n = x * x;    
		if(n < 1000) continue;    
		if(n > 9999) break;    
		int hi = n / 100;   
		int lo = n % 100;    
		if(hi/10 == hi%10 && lo/10 == lo%10) printf("%d\n", n);  
	}  
	 return 0; 
}

此程序中的新知识是continue和break语句。continue是指跳回for循环的开始,执行调整语句并判断循环条件(即“直接进行下一次循环”),而break是指直接跳出循环。
这里的continue语句的作用是排除不足四位数的n,直接检查后面的数。当然,也可以直接从x=32开始枚举,但是continue可以帮助我们偷懒:不必求出循环的起始点。有了break, 连循环终点也不必指定——当n超过9999后会自动退出循环。注意,这里是“退出循环”而不 是“继续循环”(想一想,为什么),可以把break换成continue加以验证。
另外,注意到这里的for语句是“残缺”的:没有指定循环条件。事实上,3部分都是可以 省略的。没错,for(;;)就是一个死循环,如果不采取措施(如break),就永远不会结束。


2.2 while循环和do-while循环


while循环的格式为“while(条件)循环体;

程序2-4 3n+1问题:对于任意大于1的自然数n,若n为奇数,则将n变为3n+1,否则变为n的一半。 经过若干次这样的变换,一定会使n变为1。例如,3→10→5→16→8→4→2→1。

不难发现,程序完成的工作依然是重复性的:要么乘3加1,要么除以2,但和2.1的程序又不太一样:循环的次数是不确定的,而且n也不是“递增”式的循环。这样的情况很适合用while循环来实现。

程序2-4完整代码 3n+1问题(有bug)
#include <stdio.h>
int main()
{
    int n, count = 0;
    scanf("%d", &n);
    while(n>1)
    {
        if(n%2==1)
            n = 3 * n + 1;
        else
            n /= 2;
        count++;
    }
    printf("%d\n",count);
	printf("%d\n",n); 
    return 0;
}

这个程序是否正确?先来测试一下:输入“987654321”,看看结果是什么。很不幸,答 案等于1——这明显是错误的。题目中给出的范围是 n ≤ 1 0 9 n≤10^9 n109,这个987654321是合法的输入数据。
在给n做变换的语句后加一条输出语句printf("%d\n",n),将很快找到问题的所在:第一次输出为-1332004332,它不大于1,所以循环终止。所以我们知道是乘法溢出了。

3n+1问题修改版

我们修改为long long版本的代码,它避开了对long long的输入输出,并且成功算出n=987654321时的答案为180。

程序2-5完整代码 3n+1问题
#include <stdio.h>
int main()
{
	int n2,count=0;
	scanf("%d",&n2);
	long long n = n2;
	while(n>1){
		if(n%2==1)
            n = 3 * n + 1;
        else
            n /= 2;
        count++;
	}
	printf("%d\n",count);
	printf("%d\n",n); 
    return 0;
}

程序2-6 近似计算:计算 π / 4 = 1 − 1 / 3 + 1 / 5 − 1 / 7 + . . . π/4=1-1/3+1/5-1/7+... π/4=11/3+1/51/7+... ,直到最后一项小于 1 0 − 6 10^-6 106

本题也是重复计算,因此可以用循环实现。但不同的是,只有算完一 项之后才知道它是否小于10-6。也就是说,循环终止判断是在计算之后,而不是计算之前。 这样的情况很适合使用do-while循环

程序2-6完整代码 近似计算(使用for循环)

#include <stdio.h>
int main()
{
	double sum=0;
	int i;
	for(i=0; ;i++)
	{
		double term = 1.0/(i*2+1);
		if(i%2==0)
			sum += term;
		else
			sum -= term;
		if(term<1e-6)
			break;
	}
	printf("%.6f\n",sum);
	return 0;
}
程序2-6完整代码 近似计算(使用do-while循环)
#include <stdio.h>
int main()
{
	double sum=0;
	int n=1;
	float sgn = 1;
	do
	{
		sum += sgn/n;
		n += 2;
		sgn = -sgn;
	}while(1.0/n>1e-6);
	printf("%.6f\n",sum);
	return 0;
}

do-while循环的格式为“do{循环体}while(条件);”,其中循环体至少执行一次,每次执行完循环体后判断条件,当条件满足时继续循环。


2.3 循环的代价


程序2-7 阶乘之和:输入n,计算 S = 1 ! + 2 ! + 3 ! + … + n ! S=1!+2!+3!+…+n! S123n的末6位(不含前导0)。 n ≤ 1 0 6 n≤10^6 n106,n!表示前n个正整数之积。

这个任务并不难,引入累加变量S之后,核心算法只有for(int i=1;i<=n;i++)S +=i!。不过,C语言并没有阶乘运算符,所以这句话只是伪代码,而不是真正的代码。 事实上,还需要一次循环来计算i!,即for(int j=1;j<=i;j++)factorial*=j;

程序2-7完整代码 阶乘之和(1)
#include <stdio.h>
int main()
{
	int i,n,S=0;
	scanf("%d",&n);
	for(i=1;i<=n;i++)
	{
		int j,temp=1;
		for(j=1;j<=i;j++)
		{
			temp *= j;
		}
		S += temp;
	}
	printf("%d\n",S%1000000);
	return 0;
}

下面来测试一下这个程序:n=100时,输出-961703。直觉告诉我们:乘法又溢出了。这个直觉很容易通过“输出中间变量”法得到验证,但若要解决这个问 题,还需要一点数学知识。
要计算只包含加法、减法和乘法的整数表达式除以正整数n的余数,可以在 每步计算之后对n取余,结果不变。

程序2-8完整代码 阶乘之和(2)
#include <stdio.h>
#include <time.h>
int main()
{
	const int MOD=1000000;
	int i,n,S=0;
	scanf("%d",&n);
	if(n>25) n=25;
	for(i=1;i<=n;i++)
	{
		int j,temp=1;
		for(j=1;j<=i;j++)
			temp = temp*j%MOD;
		S = (S+temp)%MOD;
	}
	printf("%d\n",S);
	printf("Time used=%.2f\n",(double)clock()/CLOCKS_PER_SEC);
	return 0;
}

25!末尾有6个0,所以从第5项开始,后面的所有项都不会影响和的末6位数字——只需要在程序的最前面加一条语句if(n>25)n=25;,效率和溢出都将不存在问题。


2.4 算法竞赛中的输入输出框架


程序2-9 数据统计:输入一些整数,求出它们的最小值、最大值和平均值(保留3位小数)。输入保证这些数都是不超过1000的整数。

如果是先输入整数n,然后输入n个整数,相信读者能够写出程序。关键在于:整数的个 数是不确定的。下面直接给出程序:

程序2-9完整代码 数据统计(有bug)
#include <stdio.h>
int main()
{
	int x,n=0,min,max,s=0;
	while(scanf("%d",&x)==1)//scanf返回的值是成功输入的变量个数,当输入结束时,scanf函数无法再次读取x,将返回0 
	{
		s += x;
		if(x<min)
			min = x;
		if(x>max)
			max = x; //max可能一开始就等于其他很大的值,无法更新为比它小的值
						//解决方法:1、定义一个很大的常数,如INF=10000000,然后令max=-INF,min=INF
						//			2、先读取第一个整数x,然后令max=min=x 
		n++;	
	}
	printf("%d %d %.3f\n",min,max,(double)s/n);
	return 0;
}

下面进行测试。输入“2 8 3 5 1 7 3 6”,按Enter键,但未显示结果。难道程序速度太慢? 其实程序正在等待输入。还记得scanf的输入格式吗?空格、TAB和回车符都是无关紧要的, 所以按Enter键并不意味着输入的结束。那如何才能告诉程序输入结束了呢?
在Windows下,输入完毕后先按Enter键,再按Ctrl+Z键,最后再按Enter 键,即可结束输入。在Linux下,输入完毕后按Ctrl+D键即可结束输入。

输入终于结束了,但输出却是“1 2293624 4.375”。这个2293624是从何而来?当用-O2编 译(读者可阅读附录A了解-O2)后答案变成了1 10 4.375,和刚才不一样!换句话说,这个 程序的运行结果是不确定的。在读者自己的机器上,答案甚至可能和上述两个都不同。
根据“输出中间结果”的方法,读者不难验证下面的结论:变量max在一开始就等于 2293624(或者10),自然无法更新为比它小的8。修改方法已经注明在程序中。

上面的程序并不是很方便:每次测试都要手动输入许多数。尽管可以用前面讲的管道的 方法,但数据只是保存在命令行中,仍然不够方便。

注:一个好的方法是用文件——把输入数据保存在文件中,输出数据也保存在文件中。这 样,只要事先把输入数据保存在文件中,就不必每次重新输入了;数据输出在文件中也避免 了“输出太多,一卷屏前面的就看不见了”这样的尴尬,运行结束后,慢慢浏览输出文件即 可。如果有标准答案文件,还可以进行文件比较(9),而无须编程人员逐个检查输出是否正确。事实上,几乎所有算法竞赛的输入数据和标准答案都是保存在文件中的。

使用文件最简单的方法是使用输入输出重定向,只需在main函数的入口处加入以下两条
语句:

freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);

上述语句将使得scanf从文件input.txt读入,printf写入文件output.txt。事实上,不只是scanf 和printf,所有读键盘输入、写屏幕输出的函数都将改用文件。尽管这样做很方便,并不是所 有算法竞赛都允许用程序读写文件。甚至有的竞赛允许访问文件,但不允许用freopen这样的 重定向方式读写文件。

程序2-10完整代码 数据统计(重定向版)
#define LOCAL
#include <stdio.h>
#define INF 1000000000
int main()
{
#ifdef LOCAL
	freopen("data.in","r",stdin);
	freopen("data.out","w",stdout);
#endif
	int x,n=0,min=INF,max=-INF,s=0;
	while(scanf("%d",&x)==1)
	{
		s += x;
		if(x<min)
			min = x;
		if(x>max)
			max = x;
	//	printf("%d %d %.3f\n",x,min,max);  用于调试输出中间信息 
		n++;	
	}
	printf("%d %d %.3f\n",min,max,(double)s/n);
	return 0;
}

重定向的部分被写在了#ifdef和#endif中。其含义是:只有定义了符号LOCAL,才编译两条freopen语句。 输出中间结果的printf语句写在了注释中——它在最后版本的程序中不应该出现,但是又舍不得删除它(万一发现了新的bug,需要再次用它输出中间信息)。将其注释的好处是:一旦需要时,把注释符去掉即可。

如果比赛要求用文件输入输出,但禁止用重定向的方式,又当如何呢?程序如下:

程序2-11完整代码 数据统计(fopen版)
#include <stdio.h>
#define INF 1000000000
int main()
{
	FILE *fin,*fout;
	fin = fopen("data.in","rb");
	fout = fopen("data.out","wb");
	int x,n=0,max=-INF,min=INF,s=0;
	while(fscanf(fin,"%d",&x)==1) //注意是fscanf 
	{
		s += x;
		if(x<min)
			min = x;
		if(x>max)
			max = x;
		n++;
	} 
	fprintf(fout,"%d %d %.3f\n",min,max,(double)s/n);
	fclose(fin);//关闭文件 
	fclose(fout);
	return 0;
}

虽然新内容不少,但也很直观:先声明变量fin和fout(暂且不用考虑FILE*),把scanf改 成fscanf,第一个参数为fin;把printf改成fprintf,第一个参数为fout,最后执行fclose,关闭两个文件。

程序2-12 数据统计II:输入一些整数,求出它们的最小值、最大值和平均值(保留3位小数)。输入保证这些 数都是不超过1000的整数。输入包含多组数据,每组数据第一行是整数个数n,第二行是n个整数。n=0为输入结束 标记,程序应当忽略这组数据。相邻两组数据之间应输出一个空行。

本题和例题2-5本质相同,但是输入输出方式有了一定的变化。由于这样的格式在算法 竞赛中非常常见,这里直接给出代码:

程序2-12完整代码 数据统计II(有bug,修改办法已在代码注释里)
#include <stdio.h>
#define INF 100000000
int main()
{
	int x,n=0,min=INF,max=-INF,s=0,kcase=0;
	while(scanf("%d",&n)==1 && n)//鲁棒性,程序能自动处理错误,防止真实数据没有以0结尾情况出现 
	{
		int s = 0;
		//此处是重置min,max的值,如果不重置,第三组数据输出后会出现错误,min,max仍然是上次的值 
		//int min=INF,max=-INF;
		for(int i=0;i<n;i++){
			scanf("%d",&x);
			s += x;
			if(x<min) min=x;
			if(x>max) max=x;
		}
		if(kcase) printf("\n");
		printf("Case %d: %d %d %.3f\n",++kcase,min,max,(double)s/n);
	} 
	return 0;
}

首先是输入循环。题目说了n=0为输入标记,为什么还要判断scanf的返回值呢?答案是为了鲁棒性(robustness)。

算法竞赛中题目的输入输出是人设计的,难免会出错。有时会出现题目指明以n=0为结束标 记而真实数据忘记以n=0结尾的情形。虽然比赛中途往往会修改这一错误,但在ACM/ICPC 等时间紧迫的比赛中,如果程序能自动处理好有瑕疵的数据,会节约大量不必要的时间浪费。

接下来是找bug时间。上面的程序对于样例输入输出可以得到正确的结果,但它真的是 正确的吗?在样例输入的最后增加第3组数据:10,会看到这样的输出:
Case 3:-4 10 0.000
相信读者已经意识到问题出在哪里了:min和max没有“重置”,仍然是上个数据结束后的值。


2.5 注解与习题


习题2-1 水仙花数:输出100~999中的所有水仙花数。若3位数ABC满足 A B C = A 3 + B 3 + C 3 ABC=A^3+B^3+C^3 ABCA3B3C3,则称其为水仙花 数。例如 153 = 1 3 + 5 3 + 3 3 153=1^3+5^3+3^3 153135333,所以153是水仙花数。

#include <stdio.h>
int main()
{
	int A,B,C;
	for(int i=100;i<1000;i++){
		A = i/100;
		B = i/10%10;
		C = i%10;
		if(i==A*A*A+B*B*B+C*C*C)
			printf("%d是水仙花数\n",i);
	}
} 

习题2-2 韩信点兵:相传韩信才智过人,从不直接清点自己军队的人数,只要让士兵先后以三人一排、五人 一排、七人一排地变换队形,而他每次只掠一眼队伍的排尾就知道总人数了。输入包含多组 数据,每组数据包含3个非负整数a,b,c,表示每种队形排尾的人数(a<3,b<5,c< 7),输出总人数的最小值(或报告无解)。已知总人数不小于10,不超过100。输入到文件结束为止。

非文件版:
#include <stdio.h>
int main()
{
	int x,a=0,b=0,c=0,kcase=0;
	scanf("%d%d%d",&a,&b,&c);
	for(int i=10;i<=100;i++){
		if(i%3==a&&i%5==b&&i%7==c){
			printf("Case %d: %d\n",++kcase,i);
		}
		else if(i==101){
			printf("Case %d: No answer\n",++kcase);
		}
	}
	return 0;
}
文件版:
#define LOCAL
#include <stdio.h>
int main()
{
	//#ifdef LOCAL
	//
	//#endif // LOCAL
	FILE *fin, *fout;
	fin = fopen("data.in.txt", "rb");
	fout = fopen("data.out.txt", "wb");
 
	int x, S3 = 0, S5 = 0, S7 = 0, count = 0, n = 0;
	while (n < 3)//借此循环读取第一组数
	{
		int tip = fscanf(fin, "%d", &x);
		if (tip != 1)break;//判断文件里是否还有数据遗存
		if (n == 0)S3 = x;//读入
		else if (n == 1)S5 = x;
		else if (n == 2) {
			S7 = x;
			for (int i = 10; i <= 101; i++) {//找到最小值并输出
				if (i % 3 == S3 && i % 5 == S5 && i % 7 == S7 && i <= 100) {
					printf("Case %d: %d\n", ++count, i);
					//fprintf(fout,"Case %d: %d\n", count, i);//输出不换行
					n = -1;//这样会产生无限循环
					break;
				}
				else if (i == 101) {
					printf("Case %d: No answer\n", ++count);
					//fprintf(fout,"Case %d: No answer\n", count);
					n = -1;
					break;//可去掉
				}
			}
 
			//break;
		}
		n++;//n循环后需要重置
	}
	//fprintf(fout, "Case %d: = %d\n", count, S3);
	fclose(fin);
	fclose(fout);
	return 0;
}

习题2-3 倒三角形:

输入正整数n≤20,输出一个n层的倒三角形。例如,n=5时输出如下:

						#########
						 #######  
						  #####   
						   ###    
						    #
#include <stdio.h>
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=n;i>0;i--)
	{
		for(int j=0;j<n-i;j++){
			printf(" ");
		}
		for(int k=0;k<2*i-1;k++){
			printf("#");
		}
		printf("\n");
	}
}

习题2-4 子序列的和:输入两个正整数n<m< 1 0 6 10^6 106,输出 1 / n 2 + 1 / ( n + 1 ) 2 + . . . + 1 / m 2 1/n^2+1/(n+1)^2+...+1/m^2 1/n2+1/(n+1)2+...+1/m2 ,保留5位小数。输入包含多组数据, 结束标记为n=m=0。提示:本题有陷阱。

#include <stdio.h>
int main()
{
	int n,m,temp;
	double sum = 0;
	scanf("%d%d",&n,&m);
	if(n>m)
	{
		temp = n;
		n = m;
		m = temp;
	}
	for(int i=n;i<=m;i++){
		sum += 1.0/i/i;
	}
	printf("%.5lf",sum);
}

习题2-5 分数化小数:输入正整数a,b,c,输出a/b的小数形式,精确到小数点后c位。 a , b ≤ 1 0 6 a,b≤10^6 ab106,c≤100。输 入包含多组数据,结束标记为a=b=c=0。

#include <stdio.h>
int main()
{
	int a,b,c;
	scanf("%d%d%d",&a,&b,&c);
	printf("%.*lf\n",c,(double)a/b);
	return 0;
}

习题2-6 排列:用1,2,3,…,9组成3个三位数abc,def和ghi,每个数字恰好使用一次,要 求abc:def:ghi=1:2:3。按照“abc def ghi”的格式输出所有解,每行一个解。

#include <stdio.h>
#include <stdlib.h>
void result(int num, int *result_add, int *result_mul)
{
    int i, j, k;

    i = num / 100;        //百位
    j = num / 10 % 10;    //十位
    k = num % 10;         //个位

    *result_add += i + j + k;    //分解出来的位数相加
    *result_mul *= i * j * k;    //相乘
}
/* 整体思路:i 最小只能是123, 最大只能是333(因为最大数字只能是999)

第2个地方的实现, 我们可以用数学的方法来实现

1~9加起来真能是45, 1~9乘起来只能是362880

所以我们可以将前面的i, j, k分别分解出来的9位数字相加, 相乘, 

看最后的结果是不是45,362880 */
int main()
{
    int i, j, k;
    int result_add, result_mul;
    for(i = 123; i <=333; i++)
    {
        j = i * 2;
        k = i * 3;
        result_add = 0;
        result_mul = 1;
        result(i,&result_add,&result_mul);
        result(j,&result_add,&result_mul);
        result(k,&result_add,&result_mul);
        if(result_add == 45 && result_mul == 362880)
            printf("%d %d %d\n", i, j, k);
    }
    return 0;
}

2.6 小结


循环的出现让程序逻辑复杂了许多。在很多情况下,仔细研究程序的执行流程能够很好地帮助理解算法,特别是“当前行”和变量的改变。有些变量是特别值得关注的,如计数器、 累加器,以及“当前最小/最大值”这样的中间变量。很多时候,用printf输出一些关键的中间变量能有效地帮助读者了解程序执行过程、发现错误,就像本章中多次使用的一样。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值