目录
前言
知识积累
for语句和while语句
for循环语句
while循环语句
do-while循环语句
多重循环嵌套
数字三角形
阶乘
数字出现次数
循环结构程序设计经典案例
省略循环体:级数求和(P1035)
省略循环条件:金币(P2669)
自加自减语句:数列求和(P5722)
浮点数误差与死循环:数列求和
质数、break语句和continue语句:质数口袋(P5723)
课后习题答案
习题4-1
习题4-2
习题4-3 数字反转(P1037)
习题4-4 斐波那契数列(P1720)
习题4-5 求极差(P5724)
习题4-6 最长连号(P1420)
习题4-7 质因数分解(P1075)
习题4-8 求三角形(P5725)
习题4-9 打分(P5726)
习题4-10 Davor(P4956)
习题4-11 津津的储蓄计划(P1089)
今天是10月24日,程序员节。想到未来的我大概率也是个程序员,那今天也差不多是我的节日了!在这样一个有意义的日子里,不学点什么都对不起未来的自己,于是我趁下午没有线下课,赶紧到图书馆复习一下循环语句,为接下来学数组函数这些打好基础,以下就是我对洛谷深基里的题目和代码的理解。
for循环语句的一般形式如下:
for ( 循环变量初始值;循环条件;每轮循环结束操作 ) {
循环体}
实质上,for后面括号里的语句存在的意义就是规定循环次数,下面用示例分析这一功能。
例1. 多行输出苹果
张三每吃一个苹果,都会在日记里写下自己今天到现在吃了几个苹果,为了巩固他的英语,他打算用英文来写,比如,他在吃完今天第五个苹果后,会写下“Today,I ate 5 apples.”。其它以此类推。要求写一个程序,当输入整数 n 时,输出 Today,I ate 5 apples. 。注意单复数变化。
#include
using namespace std;
int main(){
int n;
cin>>n;
for(int i=0;i
通过这道题,就可以理解for循环语句的意思了,循环语句括号里定义的变量 i 的用法之一就是计数,用来规定循环的次数,如果要循环 n 次,可以有 3 种写法,有时使用第三种会更方便:
for ( int i = 0 ; i < n ; i + + ) { }
for ( int i = 1 ; i < = n ; i + + ) { }
for ( int i = n ; i > 1 ; i - - ) { }
例2. 最值打擂(P5718)
题目描述:
给出 n 和 n 个整数,求这 n 个整数中最小值是什么。
输入格式:
第一行输入一个正整数 n,表示数字个数。
第二行输入 n 个非负整数,以空格隔开。
输出格式:
输出一个非负整数,表示这 n 个非负整数中的最小值。
#include
using namespace std;
int main(){
int n,num,Min;
cin>>n;
cin>>Min;//第二行输入的第一个数;
for(int i=0;i>num;
Min=min(num,Min);
//第二行输入的第一个数作为擂主(Min),
//接下来输入的数和他打擂,
//也就是比谁小,
//谁小谁就是新的擂主(Min);
}
cout<
从这道题中,我们学到的不仅是如何通过循环来判断最值,还有更重要的,一次多输入的实现,这道题要求输入第二行是n个数,一次性输入n个数,然后让循环语句来判断最值,for循环语句会重复执行循环体里的输入,实际上是读取,每次读取一个数,这是一个很重要的方法,通过在for循环体里使用 cin 来实现单次交互的多个输入,利用for循环语句每次循环依次读取的特点,实现重复判断后只有单次输出。
例3. 分类平均(P5719)
题目描述:
给定 n 和 k,将从 1 到 n 之间的所有正整数可以分为两类:A 类数可以被 k 整除(也就是说是 k 的倍数),而 B 类数不能。请输出这两类数的平均数,精确到小数点后 1 位,用空格隔开。
数据保证两类数的个数都不会是 0。
输入格式:
输入两个正整数 n 与 k。
输出格式:
输出一行,两个实数,分别表示 A 类数与 B 类数的平均数。精确到小数点后一位。
#include
//由于本题需要输出数字保留小数,
//所以用C语言风格可以减少代码量;
using namespace std;
int main(){
int n,k;
int Asum=0,Bsum=0;
//循环体里需要的变量,
//一定要先赋值,
//否则没有初值就会报错;
scanf("%d %d",&n,&k);
for(int i=k;i<=n;i+=k)
/*定义的变量i在这里就不是用来计数的了,
理解这个地方需要先分析一下题目,
题目要求的是在从1到n的整数里,
找到所有等于k的整数倍的数,
所以,循环没有必要从1开始,
可以直接从k开始,因为k是k的1倍,
此后每经过一次循环,加一个k,
这样所有的被赋值的i就都是k的倍数,
减少了很多没必要的循环,值得学习。*/
Asum+=i;
Bsum=(1+n)*n/2-Asum;
//这里是算从1到n里不是k的倍数的数之和,
//巧妙地应用了求和公式,值得学习;
printf("%.1f ",double(Asum)/(n/k));
//这里的除数是 n/k ,
//其实就是这里所有k的倍数的数量,
//这样又避免了使用循环;
printf("%.1f",double(Bsum)/(n-n/k));
//这两行输出体现了C语言的优越性,
//使用占位符 %.1f ,
//将浮点数保留一位小数再输出,
//比起C++中使用setprecision()函数要方便得多。
return 0;
}
通过这道题,可以知道 for 语句括号里的变量 i 还可以有其他的使用方式,不一定要用来计数,可以通过赋给 i 题目要求输入的值,利用每轮结束操作来对 i 进行适当的处理,以期简化代码。
通过上面两题难度的对比,我们可知,for循环语句更适合明确知道重复次数的循环,这里的知道,指的是通过输入得知的循环次数,以及题目中暗含的循环次数。
while循环语句的一般形式如下:
while ( 循环成立条件 ){
循环体
}
相较于for循环语句的形式,while循环语句变得更简单,少了变量 i 和循环结束处理,循环结束处理其实可以放在循环体中,无伤大雅。但是,while循环最大的优点,其实是它更适用于重复次数未知的循环,仅需要条件判断来进行循环。while语句的功能通过下例来说明。
例4. 一尺之棰(P5720)
题目描述:
《庄子》中说到,“一尺之棰,日取其半,万世不竭”。第一天有一根长度为 a 的木棍,从第二天开始,每天都要将这根木棍锯掉一半(每次除 2,向下取整)。第几天的时候木棍的长度会变为 1?
输入格式:
输入一个正整数 a,表示木棍长度。
输出格式:
输出一个正整数,表示要第几天的时候木棍长度会变为 1。
#include
using namespace std;
int main(){
int a,days=1;//因为第一天木棍不用锯,所以不参与循环;
cin>>a;
while(a>1)
/*当a比1大时就可以进入循环,
进行循环期间,
由于循环体里有对于a的操作,
所以a的值会改变,
第二次进入循环的a其实是被第一次循环处理过的a,
而非上面输入的a;
当某一次循环后,a不符合a>1的条件时,
就会跳出循环,执行循环下面的代码;*/
days++,a/=2;
/*题目里说的向下取整,
其实就是省略每次除以2的余数,
直接对整数用 / 就可以了;
这里还用到了逗号表达式,简化了代码;*/
cout<
在这道题的代码里,运用了逗号表达式,逗号表达式就是用逗号将两个不同的表达式连在一起,变成了一条语句,这样就可以不需要使用花括号了,减少了代码量;
另外,这道题还可以使用for循环语句来做,不过要稍微复杂一点,如下:
#include
using namespace std;
int main(){
int a,n=1;
cin>>a;
for(int i=a;i>1;i/=2)
n+=1;
cout<
事实上,while循环语句一直被当做for循环语句的简化版。还要注意的是,如果循环语句永远成立,就变成了死循环,运行不了,因此要特别注意执行循环的判断条件。
do-while语句其实就是while语句的一种变体,while语句是先判断再执行循环体,而do-while语句则是先执行循环体再判断,判断条件成立就继续下一次循环,条件不成立就跳出循环,执行循环下面的语句。do-while循环语句的一般形式如下:
do {
循环体
} while ( 循环成立条件 );
和while语句的最大不同在于,do-while语句至少会执行一次循环体,而while语句的循环体可能一次也不会执行。下面是一个较为复杂的例子。
例5. 猜数游戏
小洛机器人和你完猜数游戏!小洛随机选择并默默记下一个1~100的整数,你需要不断猜测这个数字是什么并输入验证。如果你输入的数字比小洛选择的数字小,小洛会输出“Too small!”;如果比小洛选择的数字大,小洛输出“Too large!”;如果刚好猜对,小洛输出“You are right!!”。如果一次没有猜中,就继续猜,直到猜中为止。
#include
#include
//包含产生随机数函数的头文件;
#include
//产生随机数需要配套的用于使随机数真正随机的头文件;
using namespace std;
int main(){
int ans,guess;
srand(time(0));
//使得多个随机数在连续产生时不相等,更加随机;
ans=rand()%100+1;
/*rand()%n 就是一个随机数,
从1到n-1的随机一个数,
只要上面有一个srand,
这个格式就可以多次使用;*/
do{
cin>>guess;
if(guessans) cout<<"Too large!"<
从上面代码可以得知,do-while语句最适合用于可持续输入判断类的题目,这种题目只有一个正确答案,输入正确答案后就会结束运行。
这段代码里的随机数生成也需要好好学习一下,主要需要记下以下代码:
#include
#include
{
int a,b;
srand(time(0));
a=rand()%n;
b=rand()%n;
}
例6. 数字直角三角形(P5721)
题目描述:
给出 n,请输出一个直角边长度是 n 的数字直角三角形。所有数字都是 2 位组成的,如果没有 2 位则加上前导 0。
输入格式:
输入一个正整数 n。
输出格式:
输出如题目要求的数字直角三角形。
例如:n=5
输出:
0102030405 06070809 101112 1314 15
#include
using namespace std;
int main(){
int cnt=0,n;//用cnt来记录输出到哪个数字了;
scanf("%d",&n);
for(int i=1;i<=n;i++){//显而易见,要从第1行开始,输出n行;
for(int j=1;j<=n+1-i;j++)
/*也可以看出要输出的数字和j有关,
这里是结合了三角形输出特点来写的,
比方说输入的n为5,就可以列出这样一个表:
行数 这一行的数字个数
1 5
2 4
3 3
4 2
5 1
而不同的行数就是外层循环定义的i(外层循环定义的数,在内层循环里一样能用),
每行的数字个数就是j,这里的i就是行数,j就是在这行输出的数的个数,
通过观察也直到每次循环的行数和数字个数之和恒等于6,也就是n+1,
通过总结i,j,n+1三者的数量关系,可以得出 j=n+1-i,
每次循环只要输出(n+1-i)个数即可;
这里还需注意的是,不可定义j=1,因为j代表的是一行输出数字的个数,而非只是用来计数的。*/
printf("%02d",cnt++);
//这里用了占位符%0nd,n代表的是这个数所占的位数,若数字长度不足n,则在前面用0补齐;
printf("\n");
}
return 0;
}
例7. 阶乘之和(P1009)
题目描述:
计算出 S=1!+2!+3!+⋯+n!(n<=20)。
输入格式:
一个正整数 n。
输出格式:
一个正整数 S,表示计算结果。
#include
using namespace std;
int main(){
long long n,ans=0;
//由于题目要求的n的值为20,
//求阶乘之和一定非常大,
//所以用上最大的整数类型;
cin>>n;
for(int i=1;i<=n;i++){
//外层循环进行n次,
//就是n个阶乘相加;
long long factor=1;
//在内外循环之间,
//定义一个变量来表示单个数的阶乘,
//这样内外循环都可以用;
for(int j=1;j<=i;j++)
//内层循环进行i次;
factor*=j;
//这样写比 factor=factor*i 更简洁,
//以后对一个数进行操作再更新,
//要按照这个格式来写;
ans+=factor;
}
cout<
法2,在看课本写代码时发现了自己常犯的错误,于是写下解法二警醒自己:
#include
#include
using namespace std;
int main(){
int n,sum=0;
cin>>n;
for(int i=1,ji=1;i<=n;i++){
for(int j=1;j<=i;j++){
ji*=j;
}
sum+=ji;
ji=1;
}
cout<
例8. 计数问题(P1980)
试计算在区间1到n(n<=10^6)的所有整数中,数字x(0<=x<=9)共出现了多少次?
#include
using namespace std;
int main(){
int n,x,ans=0;
cin>>n>>x;//输入区间最右端的数和想要统计个数的数字;
for(int i=1;i<=n;i++){//进行n次循环,也就是依次读取从1到n这n个数字;
int tmp=i,num;//用tmp来表示外层每次读取的数字;
while(tmp!=0){//当tmp被赋值为0时才退出这个内层循环;
num=tmp%10;//这就是内层读取的每个数位上的数;
if(num==x)
ans++;
tmp/=10;
//完成个位数的读取识别后,整数除以10,就会将个位数删去,百位变成了个位,以此类推;
}
}
cout<
已知Sn=1+1/2+1/3+1/4+···+1/n 。现在给出一个整数k(1<=k<=15),要求计算出一个最小的n,使得Sn>k。
//没有循环体的示例
#include
using namespace std;
int main(){
int k,ans=0;
cin>>k;
for(double Sn=0;Sn<=k;ans++,Sn+=1.0/ans);
/*当需要求的答案是题目中类似计数变量的n时,
可以转换思路,将循环变量设为和n相关的数列之和,
这在数列求和的题目中通用;
这里省略了循环体,用每轮结束操作来代替,
这种方法在循环体为简单的数字增减时可以使用,
在每轮结束操作中用逗号表达式来缩短代码;*/
cout<
国王将金币作为工资,发放给忠诚的骑士。第一天,骑士收到一枚金币;之后两天(第二天和第三天),每天收到两枚金币;之后三天(第四、五、六天),每天收到三枚金币;之后四天(第七、八、九、十天),每天收到四枚金币……;这种工资发放模式会一直这样延续下去:当连续 n 天每天收到 n 枚金币后,骑士会在之后的连续 n+1 天里,每天收到 n+1 枚金币。
请计算在前 k 天里,骑士一共获得了多少金币。
//没有循环条件的示例
#include
using namespace std;
int main(){
int k,coin=0,day=0;
cin>>k;
for(int i=1;;i++)
for(int j=1,j<=i;j++){//每轮大循环里,小循环i次;
coin+=i;day++;
if(day==k){//终止循环的条件放在小循环里,就可以在某次小循环后退出循环;
cout<
上面这种格式我将其命名为嵌套循环终止条件模板,可以在循环嵌套里使用,尤其是题目需要在某次小循环后就结束并输出,十分地适用!
计算从1到n的和,不允许用等差数列求和公式。
//for循环简单做法
#include
using namespace std;
int main(){
int n,sum=0;
cin>>n;
for(int i=1;i<=n;i++) sum+=i;
cout<
using namespace std;
int main(){
int a=0,b=0,n;
cin>>n;
while(n--)
/*这里是先判断n是否为0,不为0时,若是其他数字,则都认为是真(1),
就会顺着while语句n自减1;为0则跳出循环; */
b+=++a;//可以看作a=a+1,b=b+a;这里用++a而不用a++,其中有门道。
cout<
之前说过,i++ 和 ++i 的效果相同(i--和--i同理),都可以看成是 i=i+1,单独成句没有问题,但若与其他运算一起使用,则需要注意自加自减符号在不同位置的优先级差别。具体解释如下:
#include
using namespace std;
int main(){
int a=0,b=0,A,B;
A=a++;
B=++b;
cout<
计算0.1+0.2+0.3+···+(n-0.2)+(n-0.1)的值,其中n为不大于100的整数。
这题看起来和上面那道差不多,只是把整数累加改成小数累加了,所以有些人可能会像下面这样做:
#include
using namespace std;
int main(){
int n,sum=0;
cin>>n;
for(double i=0.1;i!=n;i+=0.1){
sum+=i;
cout<
但上面是错的!!错在循环条件判断,当i不等于n时就继续循环,就意味着当i等于n时结束循环,但是众所周知,整数和浮点数 由于计算机二进制储存浮点数存在误差 不能比较,只能用:
#include
fabs ( i - n ) < 1 e -6;
用上面两句对错误代码进行适当地插入和替换,来判断i和n的差值是不是足够小,如果足够小,就近似看作二者相等。这样程序就可以正常运行而不会出现i和n一直不相等造成的死循环了。
死循环就是输入数据后程序一直运行停不下来的状态,上面的错误代码运行后就进入了死循环,但我们看到的是一直没有输出,其实还可以用一种更直观的办法看到死循环,那就是在循环体的最后加上一句 cout << i << endl; 这样就可以看到每次加0.1的i在不断输出,甚至都超过了10,奔向了无穷大!
产生死循环的程序在OJ里是不能通过的,但是解决起来也很简单:检查条件变量是否正常变化,一般是再写一句输出条件变量来查看,然后检查判断条件是否可以成立,比如有没有浮点数误差。
如果不想用这种数学函数的方法,还可以对条件里的大小判断稍加修改,将 i!=n改为i+0.01 其实,遇到这种题目,最好首先把小数化成整数,到最后输出时再化回浮点数,这样就可以避免很多不必要的麻烦,具体做法如下: 小 A 有一个质数口袋,里面可以装各个质数。他从 2 开始,依次判断各个自然数是不是质数,如果是质数就会把这个数字装入口袋。口袋的负载量就是口袋里的所有数字之和。但是口袋的承重量有限,不能装得下总和超过 L 的质数。给出 L,请问口袋里能装下几个质数?将这些质数从小往大输出,然后输出最多能装下的质数个数,每行输出一个数。 科普 什么是质数? 一般来说,无判断条件的for循环会在循环体里加一些可以跳出循环的语句,若输出较简单,可以在合适的地方直接写 return 0; ,但设计循环的题目往往不会这么简单,因此会用到break语句和continue语句。 break语句:跳出一层循环。 continue语句:跳出本次循环中未执行的语句,重新开始一轮新的循环。#include
质数、break语句和continue语句:质数口袋(P5723)
#include
课后习题答案
习题4-1
#include
习题4-2
#include
习题4-3 数字反转(P1037)
习题4-4 斐波那契数列(P1720)
习题4-5 求极差(P5724)
习题4-6 最长连号(P1420)
习题4-7 质因数分解(P1075)
习题4-8 求三角形(P5725)
习题4-9 打分(P5726)
习题4-10 Davor(P4956)
习题4-11 津津的储蓄计划(P1089)