【C语言-算法-穷举与迭代】1. 最基础的“热身算法”——穷举与迭代

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 191
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

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之间所有满足某条件的数”),你能自然地想到用循环遍历所有数,会拆位,会设标志变量判断条件。给你一个迭代类的需求(比如“用迭代法算某个递推公式”),你能找到初始值和递推关系,用循环让结果一步一步逼近目标。做到这两样,穷举和迭代你就彻底拿下了。