C语言在线编程环境:https://sky315.com:8080/c/。
很多同学刚开始学算法的时候,总觉得“算法”这俩字听起来特别高大上,好像得是那种看不懂的数学公式、绕来绕去的逻辑才算。其实真不是。C语言入门阶段最常用的算法,说白了就是“暴力试”和“反复算”。
“暴力试”叫穷举,就是把所有可能性挨个试一遍,哪个对了就选哪个。“反复算”叫迭代,就是用一个公式或者规则,不断地用旧值算出新值,一步一步逼近最终结果。
这两种算法有一个共同特点:思路极其朴素,但特别吃你的循环基本功。你循环学得扎不扎实,在这类算法里一试就知道。
例1:判断一个数是不是素数——穷举的经典入门
素数这题,基本上每个学C语言的人都写过。题目要求很简单:输入一个正整数n,判断它是不是素数(质数)。素数的定义是只能被1和它本身整除,大于1的自然数。
那你怎么判断一个数是不是素数?最朴素的思路就是:从2开始,一直到n-1,挨个试一遍,看n能不能被其中某个数整除。 如果能,那n就不是素数;如果全都不能,那n就是素数。
这就是穷举——把所有可能成为“因子”的数都试一遍。
代码写出来是这样的:
#include <stdio.h> int main() { int n, i; int isPrime = 1; // 先假设n是素数,1代表是,0代表不是 printf("请输入一个正整数:"); scanf("%d", &n); // 小于等于1的数不是素数,直接排除 if (n <= 1) { isPrime = 0; } else { // 从2试到n-1,看看有没有能整除的 for (i = 2; i < n; i++) { if (n % i == 0) { isPrime = 0; // 找到一个因子,不是素数 break; // 不用再找了,赶紧退出循环 } } } if (isPrime == 1) { printf("%d是素数\n", n); } else { printf("%d不是素数\n", n); } return 0; }
咱们一行一行拆开看:
-
int isPrime = 1;这一步是设了个“标志变量”。先假设n是素数,后面如果找到了反例(能被整除),就把这个标志改成0。这种“先假设再推翻”的写法在穷举算法里特别常见。 -
if (n <= 1)处理边界情况。1和0还有负数都不是素数,这得单独拎出来。很多新手忘了这一步,输入1的时候程序说1是素数,那就闹笑话了。 -
for (i = 2; i < n; i++)这是穷举的核心。i从2开始,一直到n-1,挨个试。如果n是10,i就会试2、3、4、5、6、7、8、9,看10能不能被其中某个整除。 -
if (n % i == 0)取余运算,如果余数是0,说明i能整除n,i就是n的一个因子。既然找到了因子,n就不是素数,把isPrime改成0,然后用break跳出循环——因为已经确定不是素数了,后面的数没必要再试了。 -
循环结束之后,根据isPrime的值输出结果。
这个算法有什么问题?
如果你输入的是101,那循环要从2试到100,足足试99次。如果输入的是1000003(一个比较大的素数),那得试将近一百万次。虽然计算机算得快,但这种“傻试”的效率确实不高。
优化的思路很简单:不需要试到n-1,只需要试到√n(n的平方根)就够了。 因为如果n有一个大于√n的因子,那必然有一个小于√n的因子跟它配对。比如n=36,√36=6,36能被9整除,但9>6,同时36也能被4整除,4<6。所以你只要试到6,就能发现4这个因子,不用再试9了。
把循环条件改成for (i = 2; i * i <= n; i++),效率一下子就上去了。这个优化你最好现在就记住,因为判断素数的题考试必考,你写i < n能过,但老师看了会摇头,写i * i <= n才说明你动了脑子。
例2:最大公约数——迭代里的“辗转相除法”
最大公约数(GCD)是迭代算法的经典代表。题目:输入两个正整数a和b,求它们的最大公约数。
最笨的办法也是穷举:从min(a,b)往下试,一个一个减,看哪个能同时整除a和b。但这样做效率太低。更聪明的方法是欧几里得算法,也叫辗转相除法。
辗转相除法的核心逻辑就一句话:a和b的最大公约数,等于b和a%b的最大公约数。 反复应用这句话,直到a%b变成0,这时候b就是最大公约数。
代码写出来极其简洁:
#include <stdio.h> int main() { int a, b, remainder; printf("请输入两个正整数:"); scanf("%d%d", &a, &b); // 辗转相除,直到余数为0 while (b != 0) { remainder = a % b; a = b; b = remainder; } printf("最大公约数是:%d\n", a); return 0; }
拆开看执行过程:
假设a=48,b=18。
第一轮循环:b=18,不等于0,进来。remainder = 48 % 18 = 12。然后a = 18,b = 12。
第二轮:b=12,不等于0。remainder = 18 % 12 = 6。然后a = 12,b = 6。
第三轮:b=6,不等于0。remainder = 12 % 6 = 0。然后a = 6,b = 0。
第四轮:b=0,循环条件不满足,退出来。这时候a=6,就是最大公约数。
这就是迭代——不断地用新的余数替换原来的两个数,一步接一步,直到满足终止条件。 你不用提前知道要循环多少次,循环自己会停。这就是while循环比for循环更适合迭代的地方——你只关心“什么时候停”,不关心“总共走几步”。
这个算法你最好背下来,不是死记硬背代码,而是记住那个逻辑:“用大数除以小数,取余数,把小数和余数作为新的一组数,反复做,直到余数为0。”脑子里有这个画面,代码随手就写出来了。
例3:水仙花数——穷举加拆数字
水仙花数是指一个三位数,它每个位上的数字的立方之和等于它本身。比如153 = 1³ + 5³ + 3³。
这题本质上还是穷举——把所有三位数从100到999挨个检查一遍,把每个数的百位、十位、个位拆出来,算立方和,看等不等于原数。
#include <stdio.h> int main() { int i, a, b, c; for (i = 100; i < 1000; i++) { a = i / 100; // 百位 b = (i / 10) % 10; // 十位 c = i % 10; // 个位 if (a * a * a + b * b * b + c * c * c == i) { printf("%d ", i); } } printf("\n"); return 0; }
这个例子告诉你:穷举不仅仅是“试数”,往往还伴随着“把数拆开”的操作。 拆位的基本功就是整除和取余。你学会了拆三位数,以后拆四位数、拆任意位数,道理都一样——不断除以10取余数,直到商变成0。
三位水仙花数只有四个:153、370、371、407。你可以自己跑一下代码验证。
变种:如果题目改成“输入一个三位数,判断它是不是水仙花数”,那就是把循环去掉,只算输入的那个数。核心逻辑还是拆位和立方和,一模一样。
例4:百钱百鸡——多重循环的穷举
这个题比较经典,也稍微上点难度。题目:公鸡5文钱一只,母鸡3文钱一只,小鸡1文钱三只。用100文钱买100只鸡,问公鸡、母鸡、小鸡各买多少只?
这个题的难点在于三个未知数,你得同时满足两个条件:钱数加起来等于100,只数加起来等于100。
最直接的想法就是:把公鸡的数量、母鸡的数量、小鸡的数量全部用循环试一遍。公鸡最多20只(5×20=100),母鸡最多33只(3×33≈100),小鸡最多100只。
#include <stdio.h> int main() { int x, y, z; for (x = 0; x <= 20; x++) { for (y = 0; y <= 33; y++) { for (z = 0; z <= 100; z++) { if (x + y + z == 100 && 5 * x + 3 * y + z / 3.0 == 100) { printf("公鸡%d只,母鸡%d只,小鸡%d只\n", x, y, z); } } } } return 0; }
拆开看:
-
三层循环,分别遍历公鸡、母鸡、小鸡的数量。
-
if条件里两个判断:总数是不是100只,总钱数是不是100文。 -
注意
z / 3.0,这里写了3.0而不是3,目的是让除法变成浮点数除法,避免整数除法截断。如果你写成z / 3,那z=2的时候结果是0,但实际小鸡2只的价格是2/3文钱,这样会漏掉正确解。这种细节不注意,结果就差了十万八千里。
但这个三重循环效率不高:21×34×101 ≈ 72000次判断,虽然不算多,但可以优化。能不能减少一层循环?
答案是肯定的——只要知道了公鸡和母鸡的数量,小鸡的数量就是100 - x - y,不需要再循环了。 这样就把三重循环降成了两重循环。
#include <stdio.h> int main() { int x, y, z; for (x = 0; x <= 20; x++) { for (y = 0; y <= 33; y++) { z = 100 - x - y; if (z >= 0 && 5 * x + 3 * y + z / 3.0 == 100) { printf("公鸡%d只,母鸡%d只,小鸡%d只\n", x, y, z); } } } return 0; }
这个优化思路很重要:穷举不是傻乎乎地把所有变量都拿来循环,能用等式推出来的,就别循环。 减少一重循环,代码效率提升一大截。
输出结果有4组解:
-
公鸡0只,母鸡25只,小鸡75只
-
公鸡4只,母鸡18只,小鸡78只
-
公鸡8只,母鸡11只,小鸡81只
-
公鸡12只,母鸡4只,小鸡84只
例5:斐波那契数列——迭代的另一个典型
斐波那契数列长这样:1、1、2、3、5、8、13、21……从第三项开始,每一项等于前两项之和。
这题用数组做也行,用迭代做更省内存。思路就是不断更新前两个数的值,把新的和算出来。
#include <stdio.h> int main() { int n, i; int a = 1, b = 1, c; printf("请输入要输出的项数:"); scanf("%d", &n); if (n >= 1) printf("%d ", a); if (n >= 2) printf("%d ", b); for (i = 3; i <= n; i++) { c = a + b; printf("%d ", c); a = b; b = c; } printf("\n"); return 0; }
执行过程拆解:
假设n=6。
先输出前两项:1 1。然后进入循环:
i=3:c=1+1=2,输出2,a变成1,b变成2。
i=4:c=1+2=3,输出3,a变成2,b变成3。
i=5:c=2+3=5,输出5,a变成3,b变成5。
i=6:c=3+5=8,输出8,a变成5,b变成8。
输出结果:1 1 2 3 5 8
这就是迭代——用三个变量不断滚动,a和b始终保持着“前两项”的状态,c是它们的新和。 每一次循环结束,a和b更新为下一对相邻项,为下一次计算做准备。
这个“滚动更新”的思路在很多迭代算法里都会用到,比如算阶乘、算累加和,本质上都是把上一步的结果拿来加工,生成下一步的结果。
最后说两句
穷举和迭代,一个是“挨个试”,一个是“反复算”。它们俩有一个共同点——都依赖循环结构。你的循环写不写得熟练,直接决定了这类算法你能不能写得顺手。
你把这几个例子自己敲一遍,跑出正确结果之后,改改数字看看输出有什么变化,把循环条件改错试试程序会怎样,出几次错再修好,这些知识就真正是你的了。
练到什么程度算过关? 给你一个穷举类的需求(比如“找出100到999之间所有满足某条件的数”),你能自然地想到用循环遍历所有数,会拆位,会设标志变量判断条件。给你一个迭代类的需求(比如“用迭代法算某个递推公式”),你能找到初始值和递推关系,用循环让结果一步一步逼近目标。做到这两样,穷举和迭代你就彻底拿下了。