文章目录
⭐️ 第02章:三大基本结构
📍 来自:中南林业科技大学软件协会学术部:谢添,林世荣,欧俊麟,霍涛
⏲ 时间:2022 - 10 - 5 至 2022 - 10 - 10
🏠 官网:https://www.csuftsap.cn/
✏️ 本章所有提供代码均已测试,读万卷书不如行万里路,一定要把代码都自己敲一遍并测试
💬 生活从未变得容易,只不过是我们变得更加坚强,你若不想做,总会找到借口,你若想做,总会找到方法,输不起的人,往往也赢不了人,当你勇敢跨出第一步的时候你就已经赢了,犹豫一千次都不如实践一次,所有的努力和付出一定会有收获。
0.什么是语句
C语言中,由一个分号 ;
隔开的代码就是一条语句。
#include<stdio.h>
int main() {
int a = 1, b = 3; //第一条语句
printf("a = %d\n", a); //第二条语句
printf("b = %d\n", b); //第三条语句
return 0; //第四条语句
}
1.顺序结构
顺序结构是程序中最简单最基本的流程控制,没有特定的语法结构,按照代码的先后顺序,依次进行,程序大多数的代码都是这样执行的。总的说:写在前面的先执行,写在后面的后执行。流程控制图如下图所示:
对于一个程序需要从上往下执行,在上图中需要先执行A语句,再执行B语句,然后执行C语句。
正常情况下,编译器的执行顺序顺序也是
自上而下
进行执行的。
所以当我们编写程序时,我们定义的变量和函数最好要在程序的最上面(只是一般情况,要按实际情况来),
❌ 错误的示例:
#include<stdio.h>
int main(){
scanf_s("%d",&n);
int n;
}
//这就是一个非常典型的错误。在上面的程序中,先scanf(输入),再定义变量n,执行就会报错,因为程序的执行是从上往下的。变量n是在scanf之后定义,当执行输入语句(scanf)时,编译器并不知道有n这一个变量,就无法将你从键盘输入的数据赋值给变量n了。
如果按上述代码那么写,会出现错误提示:
错误(活动) E0020 未定义标识符 “n”
✅ 正确的写法:
#include<stdio.h>
int main(){
int n;
scanf_s("%d",&n);
}
🚩 扩展:采用合法的前向引用
❌ 错误的示例:
//下面的就是一个典型的错误,函数的使用在定义之前
#include<stdio.h>
int main(){
fun();//使用fun函数输出1
}
void fun(){//定义了一个函数,它的作用是在 控制台 输出 1
printf("1\n");
}
由于程序自上而下执行的,因此进入main函数后,调用了fun(),但编译器却并未在main函数前面的发现fun()函数,因此你运行时编译器会报错提示fun函数未定义。
函数的定义必须要在使用之前(之上) 或者 进行了声明。(讲到函数会细说)
✅ 正确的写法:
#include<stdio.h>
void fun(){//定义了一个函数,它的作用是在 控制台 输出 1
printf("1\n");
}
#include<stdio.h>
int main(){
fun();//使用fun函数输出1
}
热热身,先看看关于循环结构和分支结构的流程图,它们也要依赖于顺序结构,都需要从上往下执行。
2.选择结构(分支语句)
2.1 if 语句
2.1.1 单分支
if (条件表达式)
{
语句 1;
语句 2;
...
}
如果条件表达式为 true 时,就会执行{}中的内容,如果为 false,就不执行。如果{}中只有一条语句,则可以不用写 {},但建议加上 {}。
📝 示例
#include<stdio.h>
int main()
{
if (1314 > 520) {
printf("你伴我一生一世远比你对我说我爱你浪漫");
}
return 0;
}
2.1.2 多分支
if(条件表达式)
{
代码块1(可以有多条);
}
else
{
代码块2(可以有多条);
}
当条件表达式成立,则执行代码块1,否则执行代码块2.如果执行代码块只有一条语句,则 {} 可以省略,否则不可以省略,但建议加上 {}。
if(条件表达式1)
{
代码块1(可以有多条语句);
}
else if(条件表达式2)
{
代码块2(可以有多条语句);
}
可以有多个 else if 结构 ...
else
{
代码块n;
}
🌿 说明
- 当 条件表达式1 成立时,执行 代码块1,整个分支语句结束。
- 如果 条件表达式1 不成立,才去判断 条件表达式2 是否成立。
- 如果 条件表达式2 成立,就执行 代码块2 ,整个分支语句结束。
- 以此类推,如果所有表达式都不成立,则执行 else 的代码块。
⚠️注意:分支语句最多只能有一个执行入口,即只可以执行一个括号内的代码块。
🌿 特别说明
- 多分支可以没有 else ,如果所有的条件表达式都不成立,则一个执行入口都没有。
- 如果有else,如果所有的条件表达式都不成立(即结果为false),则默认执行 else 代码块【兜底的】。
📝 示例
#include<stdio.h>
int main()
{
int score;
printf("输入你对软件协会的评分(整数):");
scanf_s("%d", &score);
if (score>=100) { //满足分数大于等于score
printf("我对软件协会超赞");
}
/*
做一个简单思考:下面这个 score>=60 如果成立为true 是说明了score在60到100之间
为什么呢?
因为 既然执行到了这个 else if,说明前面的if是没有成立的,即说明score是小于100的
*/
else if (score >= 60) {
printf("我觉得软件协会还不错哟");
}
else if (score >= 0) { //如果这个条件判断语句成立,说明 score是在0到60之间
printf("我觉得软件协会还需要努力");
}
else
{
printf("联系QQ3179167073,告诉我软件协会还需要改进的地方");
}
return 0;
}
2.1.3 两个经典问题的思考
❓ 这个题目的输出是什么呢?
#include <stdio.h>
int main()
{
int a = 0;
if (a == 1)
printf("a的值为:");
printf("%d", a);
return 0;
}
📝 似乎你觉得这道题目是什么也不输出,因为 a==1 是不成立的,但实际上,输出为:0
。
1️⃣ 首先我们知道 if、else if、else 后面如果不接中括号,那么只能有一条语句是能被包含在if、else if 或 else 中,一个分号就对应着一条语句,即我们可以写出这道题的等价代码:
#include <stdio.h>
int main()
{
int a = 0;
if (a == 1)
{
printf("a的值为:");//一条语句
}
printf("%d", a); //一条语句
return 0;
}
2️⃣ 那么这道题的结果应该很明朗了,if语句不成立,则会直接执行后面的printf("%d", a);
,所以输出结果就是0
。
❓ 这个题目的输出结果是多少,是输出 “对” ,还是输出 “错”?
#include <stdio.h>
int main()
{
int a = 0;
int b = 2;
if (a == 1)
if (b == 2)
printf("对\n");
else
printf("错\n");
return 0;
}
✏️ 其实啊,这道题的结果是什么也不输出。
1️⃣ 首先你得思考这个else到底是与第一个 if 匹配,还是与第二个 if 匹配呢?这里有个规则你需要知道,当有多个if语句时,else与if匹配规则:else与离它最近且未匹配的if进行匹配。因此else是与第二个 if 匹配的构成 一条语句
,这个语句是指if分支语句
,看做了整体。
2️⃣ 像刚才强调的,无论是单分支,还是多分支,我们都必须将它当做一条语句看待,而我们又知道,if、else if、else 后面如果只是接一条语句的话,是不需要括号的,因此,这道题的代码我们等价于:
#include <stdio.h>
int main()
{
int a = 0;
int b = 2;
if (a == 1)
{ //加上一对中括号
if (b == 2)
printf("对\n");
else
printf("错\n");
}
return 0;
}
3️⃣ 这样再看代码,应当明朗了许多,我们先判断 a==1 是否成立,很显然,由于a被赋值为0,因此 a == 1
是不成立的,即 if 不成立进入不到我们的{}中,也就无法执行中括号内的 if…else… 语句。所以最终什么也不输出。
2.2 switch 语句
2.2.1 基本语法
switch(表达式)
{
case 常量1:
语句块1(可以有多条语句);
break;
case 常量2:
语句块2(可以有多条语句);
break;
case 常量3:
语句块3(可以有多条语句);
break;
......(可以有多个 case )
case 常量n:
语句块n(可以有多条语句);
break;
default:
语句块n+1(可以有多条语句);
}
2.2.2 细节说明
- switch 关键字,表示 switch 分支。
- switch括号中的表达式对应一个值。
- case 常量1:当表达式的值等于常量1,就执行语句块1。
break
:表示退出 switch ,最好在每一个case后面都加一个break。- 如果
case 常量1
不匹配,就继续匹配case 常量2
。 - 如果表达的值与所有的case标签的值都不匹配,则所有的语句都被跳过。程序并不会终止,也不会报错。因为C语言不认为这种情况是错误。但是,如果你不想忽略不匹配所有标签的表达式的值时,
可以
使用default语句。当然,你也可以不使用default,使得一个case都没被匹配而退出了switch分支结构也是没有问题的。 - default放置顺序可以是switch内任意位置,但建议放在末尾。
- 如果一个都没有匹配上,则执行
default
后面的代码块。
2.2.3 注意事项
-
case 后面的值必须是
常量
,不能是变量 -
default 子句是
可选
的,当没有匹配的 case 时,执行 default -
break 子句用来在执行完一个case 分支后使程序跳出 switch 语句块。如果没有写 break,程序会顺序执行下面的case与default的冒号
:
后面的代码,直到switch结尾,除非遇到break才停止退出switch语句。 -
switch只针对基本数据类型使用,即switch后面括号内的表达式,其值得类型应为整数类型(包括字符型),也就是int,char,枚举,bool等基本数据类型,对于其他类型,应该考虑其他条件控制语句例如if等。
基本数据类型,我们又称为
基元类型
,为什么提起这个呢?因为我觉得基元类型
这个名字比基本数据类型要酷多了。
2.2.4 流程分析图
2.2.5 实例演示
📝 输入1-7中的任意数字,对应输出星期一到星期天。如果输入不是1-7,则提示“错误的输入”。
#include<stdio.h>
int main()
{
int day;
scanf_s("%d", &day);
switch (day)
{
case 1:
printf("星期一");
break;
case 2:
printf("星期二");
break;
case 3:
printf("星期三");
break;
case 4:
printf("星期四");
break;
case 5:
printf("星期五");
break;
case 6:
printf("星期六");
break;
case 7:
printf("星期天");
break;
default:
printf("错误的输入");
}
return 0;
}
2.3 switch和if的选择讨论
- 如果判断的具体数值不多,而且符合是基本数据类型的匹配,建议使用switch语句。
- 其它情况:对于区间判断,对结果为boolean类型判断,使用 if。
- if的使用范围其实比switch更广。
3.循环结构
学习本章之前:循环结构是结构化程序设计的三种基本结构之一,在程序设计中对于那些需要重复执行的操作应该采用循环结构来完成,利用循环结构处理各类重复操作既简单又方便。所以相较于前面两种结构的难度会有所提升,所以在此章节的代码和语法知识更需要花时间消化并且上机操作❤️
-
使用循环可以多次重复地执行多条语句,这里的“多条语句”称为循环体。在C语言中,可以使用三种循环,分别是:while、do…while、for
-
在这些语句中,循环体被重复执行的次数由循环条件控制,称为控制表达式(controlling expression)。
-
这是一个标量类型的表达式,也就是说,它属于一个算术表达式或指针表达式。如果控制表达式的值不等于 0,循环条件为 true,反之,循环条件为 false。
-
语句 break 和 continue 用于在一次循环还未执行完时,跳转出循环或返回到循环头部。
📍 无论是 while,do…while 还是 for,他们都具有四要素:
循环变量初始化
:循环的起始位置循环变量迭代
:所执行的步长,统计变化情况,识别是否继续循环条件判断
:标记循环继续的条件循环操作(循环体)
:循环所执行的内容
3.1 for 循环
3.1.1 基本语法
for(循环变量初始化;循环条件判断;循环变量迭代)
{
语句1;
语句2;
…………;
}
🚩在一个典型的 for 循环中,在循环体顶部,下述三个动作需要执行:
-
表达式 1:
循环变量初始化
只计算一次。在计算控制表达式之前,先计算一次表达式 1,以进行必要的初始化,后面不再计算它。
-
表达式 2:
循环条件判断
每轮循环前 都要进行循环条件判断,以判断是否需要继续本轮循环。当控制表达式的结果为
false
,结束循环。 -
表达式 3:
循环变量迭代
,也叫调节器
调节器(例如计数器自增)在每轮循环结束后 且表达式 2 计算前 执行。即,在运行了调节器后,再执行表达式 2进行判断。
📍 执行过程如图:
📝 我们来举个例子
#include<stdio.h>
int main()
{
int i;
/*在这里面:
1. 循环变量初始化:i = 0
2. 循环条件判断:i < 3
3. 循环变量迭代:i++
4. 循环体:printf("print the value of i =%d\n", i);
*/
for(i = 0; i < 3; i++)
{
printf("print the value of i =%d\n", i);
}
return 0;
}
运行结果:
📍 一次循环的执行过程:
- 循环变量赋初值: 将 i 的初始值变成 0
- 循环条件判断: 判断 i 是否小于3,若小于3则为true,其它为false
- 循环变量值变化: i 的值自动加1
3.1.2 中途终止循环 - return&break
在
第 3.5 节
会再次介绍 break,这里预热一波
1️⃣ 方式一:return
#include<stdio.h>
int main()
{
int i;
for(i = 0; i < 3; i++)
{
if(i == 1)
{
return 0;//直接退出程序,这也是中途终止循环的方式
}else
{
printf("%d", i);
}
}
printf("My love end");
return 0;
}
💬 当 i==1 时直接退出程序,因此这道题的输出结果就是 0 。因为 i为1的时候就直接退出程序了,并未printf打印结果,也并未打印My love end
。注意与第二种方式 break
进行比较哦。
2️⃣ 方式二:break
break语句用于终止某条语句块的执行,一般使用在 switch或者 循环(for,while,do…while)中,表示退出 switch 或
退出
循环。
#inlcude<stdio.h>
int main()
{
int i;
for(i = 0; i < 3; i++)
{
if(i == 1)
{
break;//退出for循环,但并未退出程序
}else
{
printf("%d", i);
}
}
printf("My love end");
return 0;
}
💬 当 i==1 时直接退出for循环,但并未退出程序,因此控制台的打印结果为0My love end
。
3.1.3 跳过本次循环,进入下次循环 - continue
在第
3.5
节会再次介绍continue,这里预热一波
除了break 语句可以结束循环体外,这里再介绍一个continue语句,这个语句跟break语句一样,也可以结束循环体,但不同的是,它只是结束本次循环,循环体会跳过continue后面的语句,直接去再次判断循环条件,进行下一次的循环操作。
📝 举个例子
#include<stdio.h>
int main()
{
int i;
for(i = 0; i < 3; i++)
{
//即当i等于1的时候,执行 continue语句,结束本次循环
//即当i等于1的时候不会打印信息,直接执行下一次循环
if(i == 1)
{
continue;
}
printf(" print the value of i = %d\n", i);
}
return 0;
}
运行结果如下:
通过以上例子,我们可以看到,运用 break
和 continue
语句,我们可以更加灵活的使用循环语句来控制程序的流程。
3.1.4 🚩 一个思考
for(表达式1;表达式2;表达式3)
{
循环操作;
}
我们已经知道了for循环的基本结构,但我们一定要遵循那个位置标准吗?这么说吧,我们一定要把循环变量初始化
放在表达式1
吗,一定要把循环条件判断
放在表达式2
的位置吗,一定要把循环变量迭代
放在表达式3
的位置吗?
✏️ 不是的,这些只是一些规范,你可以遵循,也可以不遵循。我们来举个例子,把位置改一改。
#include<stdio.h>
int main()
{
int i = 0; //循环变量初始化
for(;i < 3;)//表达式1和表达式2的位置空缺,甚至我们把 i < 3 的位置改到循环体中也是没问题的
{
printf("%d\n",i);
i++; //循环变量迭代
}
}
于是我们发现了,其实表达式1,2,3都可以不放任何东西,可以放到其它位置的。再来看一个更特殊的:
#include<stdio.h>
int main()
{
for(;;)//三个位置什么都不放,但两个分号无论如何是一定都要写的
{
printf("I love you ,软件协会");
}
}
你可以发现,循环变量初始化
、循环条件判断
、循环变量迭代
都没有,而且你应该意识到这个for循环居然无法退出循环(可以自己运行一下看看),这叫做**死循环**。这是我们必须避免的,在程序中绝对不能够出现死循环,带给程序的伤害比拒绝你表白的女孩给你的伤害更大!
3.2 while 循环
3.2.1 基本语法
循环变量初始化;
while(循环条件判断)
{
循环体;
循环变量迭代;
}
- 在while循环前,先进行循环变量初始化。
- 再判断while后边括号内的表达式是否为真,若为真即执行大括号内的语句,若为假,则跳过while循环结构执行大括号下方的第一条语句。
- 若为真,当执行完大括号内语句后,再次回到
2
去判断,执行。 - 直到while后小括号内表达式的表达式为假,结束循环。
📍 注意:若在while的大括号内执行了 break
语句,则立刻结束循环结构,开始执行大括号外下方的第一条语句。
3.2.2 实例演示
📝 例子1:
#include <stdio.h>
int main()
{
int a = 5, b = 10;
while(b > a) //即若b > a 为真就执行大括号内的语句,
{
//若为假则执行大括号下方语句
printf("while looping a = %d, b = %d\n", a, b);
a++;
}
printf("while loop over\n");
return 0;
}
运行结果:
即当,a的值等于10时,b>a 不成立,循环结束。
📝 例子2:
#include <stdio.h>
int main()
{
int a = 5, b = 10;
while(b > a) //即若b > a 为真就执行大括号内的语句,
{ //若为假则执行大括号下方语句
printf("while looping a = %d, b = %d\n", a, b);
if(a == 8)
{
break; //如果a等于8跳出while循环,注意哦,如果是return则就是结束整个程序
}
a++;
}
printf("while loop over\n");
return 0;
}
运行结果如下:
将例子1和例子2分别执行一下,查看打印输出信息的不同,体会一下while循环的使用,以及 break 的用法。
3.2.3 while死循环
不多说明,上代码演示最简单的while死循环,细品。
#include<stdio.h>
int main()
{
while(true)
{
}
}
3.2.4 注意事项
- 循环条件判断必须返回一个
布尔值
的表达式 - while循环是先循环条件判断再执行大括号中循环操作
3.3 do…while 循环
3.3.1 基本语法
不像 for
和 while
循环,它们是在循环头部 检查循环条件。在 C 语言中,do…while
循环是在循环的尾部 检查它的条件。
do…while 循环与 while 循环类似,但是 do…while 循环会确保至少执行一次循环。
循环变量初始化;
do
{
循环操作;
循环变量迭代;
}while(循环条件判断);
-
首先执行大括号内语句
-
判断while后边括号内的表达式是否为真,若为真回到第一步,若为假,则退出while循环结构,执行大括号下方的第一条语句。
3.3.2 实例演示
#include<stdio.h>
//输出结果:012【3】
int main() {
int i = 0;//循环变量初始化
do
{
printf("%d", i);//循环操作
i++;//循环变量迭代
} while (i < 3);//循环条件判断
printf("【%d】", i);
}
3.3.3 注意事项
- 循环条件判断必须返回一个
布尔值
的表达式 - do…while循环是先执行大括号中的循环操作再进行循环条件判断,因此循环操作会至少执行一次。
- 注意
while(循环条件判断)
的后面有一个;
,千万不要忘记写哦。
3.4 嵌套循环
3.4.1 基本介绍
- 将一个循环放在另一个循环体内,就形成了嵌套循环,其中
for
,while
,do...while
均可以作为外层循环和内层循环,建议一般使用两层即可,最多不要超过三层,否则代码的可读性很差。 - 实际上嵌套循环就是把内层循环当成外层循环的循环体,当只有内层循环的循环条件为false时,才会完全跳出内层循环,才可以结束外层的当次循环,开始下一次循环。
3.4.2 实例演示
📝 示例1
#include<stdio.h>
int main() {
for (int i = 1; i <= 3; i++)//外循环
{
for (int j = 1; j <= 4; j++)//内循环
{
printf("i = %d ; j = %d\n", i, j);
}
}
}
📝 示例2:输出三三乘法表
#include<stdio.h>
//三三乘法表
int main() {
for (int i = 1; i <= 3; i++)//外循环
{
for (int j = 1; j <= i; j++)//内循环
{
printf("%d * %d = %d\t", i, j, i * j);// \t是水平制表符,即向右移动8个单位
}
printf("\n");
}
}
运行结果:
3.5 跳转控制语句
循环控制语句改变你代码的执行顺序。通过它你可以实现代码的跳转。
📍 C 提供了下列的循环控制语句。
控制语句 | 描述 |
---|---|
break 语句 | 终止循环或 switch 语句,程序流将继续执行紧接着循环或 switch 的下一条语句。 |
continue 语句 | 告诉一个循环体立刻停止本次循环迭代,重新开始下次循环迭代。 |
goto 语句 | 将控制转移到被标记的语句。但是不建议在程序中使用 goto 语句。 |
3.5.1 break
C 语言中 break 语句有以下两种用法:
- 当 break 语句出现在一个循环内时,循环会立即终止,且程序流将继续执行紧接着循环的下一条语句。
- 它可用于终止 switch 语句中的一个 case。
📍如果小伙伴使用的是嵌套循环(即一个循环内嵌套另一个循环),break 语句会退出离自己最近的那层循环,然后开始执行该块之后的下一行代码。
3.5.2 continue
-
C 语言中的 continue 语句有点像 break 语句。但它不是强制终止,continue 会跳过当前循环中的代码,强迫开始下一次循环。
-
对于 for 循环,continue 语句执行后自增语句仍然会执行。对于 while 和 do…while 循环,continue 语句重新执行条件判断语句。
3.5.3 goto
💬goto语句在此不要求小伙伴们掌握,只需要了解,知道有这么个东西,并且要求大家非必要不使用goto语句。
📍在任何编程语言中,都不建议使用 goto 语句。因为它使得程序的控制流难以跟踪,使程序难以理解和难以修改。任何使用 goto 语句的程序可以改写成不需要使用 goto 语句的写法。原因如下:
-
goto语句是低级语言的表征。它很灵活,灵活到没有任何拘束,在函数体内直来直往。函数体内可能含有一些嵌套的循环,这就意味着goto可以跳进跳出循环而无所顾忌。当程序比较复杂时很容易造成程序流程的混乱。
-
滥用goto语句,程序无规律,可读性差。在团队开发中影响效率,以后调试和维护程序的过程会令同事和自己都头秃。
任何事物都有两面性,goto语句灵活性如果在适当的地方发挥出来确实非常有用的,毕竟现在有些地方需要无条件的跳转语句。尤其在汇编语言中,如果不使用类似于goto语句的jmp指令,那么其就无法实现循环和条件分支。所以在汇编语言中,goto语句用处还是很大的。使用goto可以使程序变得更加可扩展。当程序需要在错误处理时释放资源时,统一到goto处理最方便。这也是为什么很多大型项目,开源项目,包括Linux,都会大量的出现goto来处理错误!
但现在的编程趋势是要模块化、结构化、所以大多书上已经不建议使用无条件分支的goto语句了,尽管现在大多数语言还在支持它,但还是少用为妙,尤其是像C语言小白玩家,还是瑟瑟发抖的乖乖用break慢慢跳吧。
3.5.4 return
return 使用在方法中,表示跳出/退出所在的方法。如果 return 写在 main 方法中,则会退出程序。
3.6 总结
3.6.1 for循环与while循环对比
可以发现在while循环中依然存在循环的三个必须条件,但是由于风格的问题,使得三个部分很可能偏离较远,这样查找修改不够集中、方便。所以,for循环的风格更胜一筹。for循环使用的频率也最高。
3.6.2 while 与 do…while的区别
- while :先
循环条件判断
再执行循环操作
- do…while :先
执行循环操作
再循环条件判断
3.6.3 拓展练习
❓ 问下列语句循环多少次
#include <stdio.h>
int main()
{
int i = 0;
int k = 0;
for (i = 0, k = 0; k = 0; i++, k++)
{
k++;
}
return 0;
}
✏️ 循环0
次。 k = 0 是 赋值操作。而在C语言当中,0代表false,1代表true,因此 k = 0 是将 0 赋值给 k,这是k的值就为0了,因此不执行循环。
4.章节练习
拓展方案是算法优化,感兴趣的可以琢磨一下。如果现在看不懂也没关系,记得以后学到了某个地方突然想起了一定要回来看它,就像那个女孩依旧在原地等你,不要辜负她❤️,她其实愿意等你好久好久。
4.1 计算阶乘
🏠 原题链接: https://www.luogu.com.cn/problem/P5739
4.1.1 题目描述
求 n!,也就是 1×2×3⋯×n。
4.1.2 代码
#include<stdio.h>
int main() {
int n; //求 n 的阶乘
int res = 1; //阶乘的结果存储在res("result"的缩写)中,一定要赋值为1
scanf_s("%d", &n);
for (int i = 1; i <= n; i++) {
res = res * i;//将 res * i 的值赋给 res
}
printf("res = %d", res);
return 0;
}
4.1.3 拓展方案:函数递归
#include<stdio.h>
/*
fun()函数的大括号中(代码块)的内容也可以用三元运算符来简写:
return n > 1 ? n * fun(n - 1) : 1;
*/
int fun(int n)
{
if (n > 1)
return n * fun(n - 1);
else //n = 1时就结束递归,返回1即可
return 1;
}
int main() {
int n; //求 n 的阶乘
scanf_s("%d", &n);
int res = fun(n); //阶乘的结果存储在res("result"的缩写)中
printf("res = %d", res);
return 0;
}
❓ 留下一个问题,如果我们设置的 n 过大,比如为1000,那这个res显然用int是无法装下的,即使用 long long int 类型也是无法容纳下那么大一个数的,那应该怎么解决呢?
✏️ 这个问题,可以用
数组
来解决,因此我们放到数组章节再来探讨这道题目。
4.2 素数个数
🏠 原题链接:https://www.luogu.com.cn/problem/P3912
4.2.1 题目描述
求1到1000中素数的个数。
4.2.2 思路分析
判断一个数是不是素数,我们首先得知道素数的定义:除了1和它本身,不能被其它自然数整除的数是素数,因此遍历每一个数,再对每一个数进行除法运算,看它能否被除1和它自身的数整除。我们又知道素数肯定不是偶数,所以我们循环遍历的时候每次循环迭代+2
4.2.3 代码
4.2.3.1 简单方法
#include<stdio.h>
int main() {
int i, j;
int isPrimerNumber;
// 2 一定是素数,提前打印输出
printf("2\n");
// i = i + 2 保证了 i 一定是奇数
for (i = 3; i < 1000; i = i + 2)//也可以写 i += 2 [复合赋值运算符]
{
//默认1即true,表示为素数
isPrimerNumber = 1;
/*
1.对每个 i 进行判断是否为素数
2.循环变量初始化
j = 3:从3开始取模,为什么不为1和2
- j=1,则肯定能取模为0,且不管是否为素数,肯定能被1和自身整除
- j=2,我们已经在外层循环筛选出 i 肯定不为偶数因此没必要。
*/
for (j = 3; j <= sqrt(i); j++)
{
if (i % j == 0)// 取模运算符:%
{
//如果 i 能对 j 取模的结果为0,即说明j能被i整除,标记一下false,为非素数
isPrimerNumber = 0;
//退出内层循环,因为已经判断出了此时 i 不为素数,也没有继续循环下去的必要了
break;
}
}
if (isPrimerNumber)
{
//则说明 i 是素数
printf("%d\n", i);
}
}
return 0;
}
4.2.3.2 稍作优化:sqrt函数
判断 i 是否能被 2~ i \sqrt{i} i 间的整数整除:
- 输入的数n不能被 2~ i \sqrt{i} i 间的整数整除,说明是素数
- 输入的数n能被 2~ i \sqrt{i} i 间的整数整除,说明不是素数
❓ 为什么可以进行这样的范围缩小呢?
对一个数n,如果他能分解成n=pq,那么pq里必然有一个大于等于根号n一个小于等于根号n,也就是说一个合数n必然有一个因子是小于等于根号n的. 所以对一个数n,只要检验他有没有小于等于根号n的因子就可以了(检验小于等于n的因子使循环次数变少,这也是简化的原因)
#include<stdio.h>
#include<math.h> //使用sqrt开平方根函数一定要记得引入math库
int main() {
int i, j;
int isPrimerNumber;
// 2 一定是素数,提前打印输出
printf("2\n");
// i = i + 2 保证了 i 一定是奇数
for (i = 3; i < 1000; i = i + 2)//也可以写 i += 2 [复合赋值运算符]
{
//默认1即true,表示为素数
isPrimerNumber = 1;
/*
1.对每个 i 进行判断是否为素数
2.循环变量初始化
j = 3:从3开始取模,为什么不为1和2
- j=1,则肯定能取模为0,且不管是否为素数,肯定能被1和自身整除
- j=2,我们已经在外层循环筛选出 i 肯定不为偶数因此没必要
*/
for (j = 3; j <= sqrt(i); j++)
{
if (i % j == 0)// 取模运算符:%
{
//如果 i 能对 j 取模的结果为0,即说明j能被i整除,标记一下false,为非素数
isPrimerNumber = 0;
//退出内层循环,因为已经判断出了此时 i 不为素数,也没有继续循环下去的必要了
break;
}
}
if (isPrimerNumber)
{
//则说明 i 是素数
printf("%d\n", i);
}
}
return 0;
}
4.2.3.3 进一步优化:带一点欧拉筛算法的味道
#include<stdio.h>
#include<math.h> //使用sqrt开平方根函数一定要记得引入math库
int main() {
int i, j;
int isPrimerNumber;
// 2 一定是素数,提前打印输出
printf("2\n");
// i = i + 2 保证了 i 一定是奇数
for (i = 3; i < 1000; i = i + 2)//也可以写 i += 2 [复合赋值运算符]
{
//默认1即true,表示为素数
isPrimerNumber = 1;
/*
1.对每个 i 进行判断是否为素数
2.循环变量初始化
j = 3:从3开始取模,为什么不为1和2
- j=1,则肯定能取模为0,且不管是否为素数,肯定能被1和自身整除
- j=2,我们已经在外层循环筛选出 i 肯定不为偶数因此没必要
3.循环变量迭代(这里其实就有了一种名为 欧拉筛 算法的思想了)
j += 2:这导致了j一定为奇数,那为什么不考虑j为偶数的情况呢,
你想,如果 j 可以是偶数且能被 i 整除,那么 i 一定2的倍数
我们在外层循环又强调了 i 肯定是奇数,那是不是j为偶数没有意义了。
*/
for (j = 3; j <= sqrt(i); j += 2)
{
if (i % j == 0)// 取模运算符:%
{
//如果 i 能对 j 取模的结果为0,即说明j能被i整除,标记一下false,为非素数
isPrimerNumber = 0;
//退出内层循环,因为已经判断出了此时 i 不为素数,也没有继续循环下去的必要了
break;
}
}
if (isPrimerNumber)
{
//则说明 i 是素数
printf("%d\n", i);
}
}
return 0;
}
4.2.4 拓展方案:欧拉筛
同样的,这个欧拉筛算法的知识点我们放在数组来讲,是一种进一步优化求素数的算法,这里先做个大概了解。感兴趣的可以先去了解,还是把代码放下面,现在不做讲解。
#include<stdio.h>
int judge[10000]; //保存 这个数是否为素数
int prime[10000]; //用来保存已经找到的素数
int count; //统计素数的个数
int main()
{
int n;
scanf_s("%d",&n);
for(int i = 2;i <= n;i++)
{
if(!judge[i]) //如果judge[i] 没有被标记为合数
{
prime[count] = i; //将 i 保存进数组
count++;
} //i*prime[j] 为合数
for(int j = 0;j < count && i*prime[j] <= n;j++) //j 遍历的 是 已保存 的 素数
{
judge[i*prime[j]] = 1; // 将 i*prime[j] 进行标记为合数
if(i % prime[j] == 0)//如果 prime[j] 是 i 的最小质因子,跳出
break;
}
}
printf("%d\n",count);
}
4.3 核实登录
4.3.1 题目描述
模拟登录while+switch:输入1登陆成功,其他都登陆失败,需要重新登录,直到登陆成功
4.3.2 代码
#include<stdio.h>
int main()
{
int n;
while (1)
{
scanf_s("%d", &n);
switch (n) {
case 1:
printf("登陆成功\n");
return 0;
default:
printf("登陆失败\n");
}
}
return 0;
}
4.4 鸡兔同笼
4.4.1 题目描述
鸡兔同一个笼子中,已知头共 n 个,脚共 m 个,问有鸡兔各多少只,请用循环来解决该问题
4.4.2 思路分析
- 假设鸡有0只,那么兔子有(n-0)只,当满足 0 * 2 + n * 4 == m(脚的总个数) 时,说明我们假设成立,不用再向下假设,此题得解。
- 假设鸡有1只,那么兔子有(n-1)只,当满足 1 * 2 + (n-1) * 4 == m(脚的总个数) 时,说明我们假设成立,不用再向下假设,此题得解。
- 假设鸡有2只,那么兔子有(n-2)只,当满足 2 * 2 + (n-2) * 4 == m(脚的总个数) 时,说明我们假设成立,不用再向下假设,此题得解。
很明显,这样的假设顺序我们可以用循环来实现。
4.4.3 代码
#include<stdio.h>
int main()
{
//设置鸡共 i 只,兔共 j 只
int i, j;
//设置头共n个,脚共m个
int n, m;
scanf_s("%d %d", &n, &m);
for (i = 0; i <= n; i++)
{
//鸡 i 只,又已知头为 n 个,则兔子(n-i)个
j = n - i;
if (2 * i + 4 * j == m) {
printf("鸡 %d 个,兔 %d 个", i, j);
return 0;
}
}
printf("此题无解!");
return 0;
}
4.4.3 一些说明
很显然,如果用循环来做很复杂,因为设鸡有a只,兔有b只,则 a + b = n,2a + 4b = m,联解得 a = (4n-m)/2
,b = n-a
。输入了n和m,a和b我们可以直接求得,不需要循环,但这里只是一个对循环操作的练习,不用太在意。
4.5 斐波那契数列
4.5.1 题目描述
斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……
求斐波拉契数列第15个值。
4.5.2 思路
在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)
(n ≥ 2,n ∈ N*)。这也很容易转换为代码。
4.5.3 代码
#include<stdio.h>
int main()
{
int first = 1;//第一项
int second = 1;//第二项
int three;
for (int i = 0; i < 13; i++)
{
/*
记住:每次都是利用前两个数来求第三个数,
求完后前两个数都往后移动一位。
1.第一次循环:
- three = 2
- first = second =》 1
- second = three =》 2
2.第二次循环:
- three = 3
- first = second =》 2
- second = three =》 3
3.第三次循环:
- three = 5
- first = second =》 3
- second = three =》 5
*/
three = first + second; //第三项
first = second; //将第二项的值赋给第一项
second = three; //将第三项的值赋给第二项
}
printf("%d", three);
return 0;
}
4.5.4 拓展方案:函数递归
#include <stdio.h>
int fib(int input){
if (input <= 2) {
return 1;//前两项都为1,所以直接返回1即可
}
return fib(input - 1) + fib(input - 2);
}
void main() {
int input = 0;
scanf("%d",&input);
printf("第%d位斐波那契数为:%d",input,fib(input));
}
4.6 打印三角形
4.6.1 题目描述
请使用循环打印出如下三角形:
4.6.2 思路分析
打印出一个占四行的三角形,它的每一行前面会有空格,因此首先要算出空格多少的规律,以及每行的数目。设行数为 n ,在这里 n 就为4。
- 空格计算:
- 第一行:空格数量为 n - 1,即3
- 第二行:空格数量为 n - 2,即2
- …
- 第 i 行:空格数量为 n - i,因此进行
(n - i)
次printf(" ");
打印空格的操作
- 星星计算:
- 第一行:星星数量为 2 * 1 - 1,即1
- 第二行:星星数量为 2 * 2 - 1,即3
- 第三行:星星数量为 2 * 3 - 1,即5
- …
- 第 i 行:星星数量为 2 * i - 1,因此进行
(2 * i - 1)
次printf("*");
打印星星的操作
实际上,你从我的上述可以看出,这就是一个找数学规律的过程,然后我们把找到的规律转变为代码就行了。
4.6.3 代码
#include<stdio.h>
int main()
{
int n = 4;//表示三角形的总行数
int i, j;
for (i = 1; i <= n; i++)
{
//打印空格 n- i 次
for (j = 0; j < n-i; j++)
{
printf(" ");
}
//打印星星 2*i-1 次
for (j = 0;j < 2*i-1; j++)
{
printf("*");
}
//打完一行就换行,接着下次循环即打印下一行
printf("\n");
}
return 0;
}
4.6.4 拓展训练:打印菱形
💬 其实很简单,既然能够打印正三角形,而菱形又是由正三角形和倒三角形组成的,所以菱形应该很容易打印。自己先尝试一下吧。
#include<stdio.h>
int main()
{
int n = 4;//表示三角形的总行数
int i, j;
//打印正三角形
for (i = 1; i <= n; i++)
{
//打印空格 n- i 次
for (j = 0; j < n - i; j++)
{
printf(" ");
}
//打印星星 2*i-1 次
for (j = 0; j < 2 * i - 1; j++)
{
printf("*");
}
//打完一行就换行,接着下次循环即打印下一行
printf("\n");
}
//打印倒三角形
for (i = n - 1; i >= 1; i--)
{
//打印空格 n- i 次
for (j = 0; j < n-i; j++)
{
printf(" ");
}
//打印星星 2*i-1 次
for (j = 0; j < 2 * i - 1; j++)
{
printf("*");
}
//打完一行就换行,接着下次循环即打印下一行
printf("\n");
}
return 0;
}
🔖 小结
💬 本章我们了解了三大基本结构:顺序、分支、循环,第四节练习题很有必要做一做,遇到任何疑惑联系软件协会
🏠 了解更多关注软协官网:https://www.csuftsap.cn/
💚 来自软件协会编辑,注册会员即可获取全部开源.md资源,请勿转载,归软件协会所有。