C语言递归—函数界的“套娃大师”,带你轻松搞定青蛙跳台阶 & 汉诺塔

目录

一、递归是啥?函数界的 “套娃大师”

  1.栈溢出

 2.递归要想正常工作,必须满足两个 “保命条件”

二、递归实战 1:算 n 的阶乘,把大事化小

1. 递归公式先搞懂

2. 代码实现

3.画图推演​编辑

三、递归实战 2:顺序打印整数的每一位

  1.基本思路

2.代码实现

3.画图推演​编辑

四、递归的坑:别迷恋 “套娃”,迭代也要掌握

1. 递归写法的坑:重复计算到崩溃

2. 迭代写法:一个循环搞定,从前往后算,效率拉满

五、递归 vs 迭代:什么时候该用哪个?

六、实战拓展(青蛙跳台阶 & 汉诺塔)

  1.青蛙跳台阶问题

找规律:把 “n 级跳法” 拆成 “小问题”

迭代法优化

2.汉诺塔问题

1. 找规律:把 “n 个盘子” 拆成3 步

2. 代码实现:

七、总结


  家人们,你有没有过这种经历:给朋友讲题,讲着讲着发现 “要讲懂 A,得先懂 B;要懂 B,又得先懂 A,每一步都是缺一不可的 ”?在 C 语言里,函数也有这种操作 —— 自己调用自己,这就是咱们今天要聊的 “递归”。

别慌!递归看着玄乎,其实就像剥洋葱:一层一层往里面剥(递推),剥到最核心(终止条件)就开始往回退(回归)。今天咱们用最接地气的例子,把递归从 “看不懂” 变成 “随便用”!

一、递归是啥?函数界的 “套娃大师”

  先给递归下个定义:递归就是函数自己调用自己。简而言之,递归中的递就是递推的意思,归就是回归的意思。比如下面这段代码,main函数里又调用了main,就是最简单的递归(虽然会翻车):

#include <stdio.h>
int main() {
    printf("hehe\n");
    main(); // 自己调用自己,套娃开始!
    return 0;
}

  运行起来会不停打印 “hehe”,直到程序崩溃 —— 这就是 “死递归”,因为它没有停止的条件,最后导致栈溢出Stack overflow)。

  1.栈溢出

  每次调用函数时,程序会给这个函数在栈区 “占一块地方”,用来存函数的参数、局部变量(比如递归里的n)、返回地址(函数执行完要回到哪里继续跑)—— 这块地方叫 “栈帧”;函数执行完后,它占的栈帧会被 “清空”,栈区空间还给系统,就像快递取走后货架腾出来给下一个用。

而 “栈溢出”,就是这个 “临时货架” 被堆满了,再也塞不下新的栈帧 —— 就像快递站货架全摆满,新快递根本放不进去,只能报错。

 2.递归要想正常工作,必须满足两个 “保命条件”

  •   有终止条件:就像剥洋葱剥到芯就得停,递归到某个条件必须停止调用自己。
  •   每次调用都靠近终止条件:比如剥洋葱每次都剥掉一层,递归每次调用都得让问题规模变小,不然永远到不了终止条件。

二、递归实战 1:算 n 的阶乘,把大事化小

  想必大家在高中已经对阶乘有所了解,没错就是n 的阶乘(n!)就是 1×2×3×…×n,而且规定 0! = 1。比如 5! = 5×4×3×2×1。

  但用递归的思路想,5! 可以拆成 5×4!,4! 又能拆成 4×3!,一直拆到 0! = 1—— 这就是 “大事化小” 的精髓!

1. 递归公式先搞懂

n! 的递归公式长这样:

  • 当 n = 0 时,n! = 1(终止条件)
  • 当 n > 0 时,n! = n × (n-1)!(递推关系) 

    2. 代码实现

    根据公式,咱们写个递归函数Fact算阶乘:

    #include <stdio.h>
    
    // 算n的阶乘
    int Fact(int n) {
        // 终止条件:n=0时返回1
        if (n == 0) {
            return 1;
        } else {
            // 递推:n! = n × (n-1)!
            return n * Fact(n - 1);
        }
    }
    
    int main() {
        int n = 0;
        scanf("%d", &n);
        int result = Fact(n);
        printf("%d! = %d\n", n, result);
        return 0;
    }

    运行结果:

  • 比如输入 5,运行结果就是 “5! = 120”。咱们拆解一下Fact(5)的计算过程:

  • Fact(5) = 5 × Fact(4)
  • Fact(4) = 4 × Fact(3)
  • Fact(3) = 3 × Fact(2)
  • Fact(2) = 2 × Fact(1)
  • Fact(1) = 1 × Fact(0)
  • Fact(0) = 1(终止条件)

然后往回算:1×1=1 → 2×1=2 → 3×2=6 → 4×6=24 → 5×24=120

3.画图推演

三、递归实战 2:顺序打印整数的每一位

  1.基本思路

  再来看个例子:输入一个整数(比如 1234),按顺序打印每一位(1 2 3 4)。

  直接想的话,怎么拿到 1234 的第一位 “1”?好像有点难。但反过来想,拿到最后一位 “4” 很简单 —— 用 1234%10 就行!那递归的思路就来了:

  要打印 1234 的每一位,先打印 123 的每一位,再打印 4;

  要打印 123 的每一位,先打印 12 的每一位,再打印 3;

  要打印 12 的每一位,先打印 1 的每一位,再打印 2;

  要打印 1 的每一位,直接打印 1(终止条件:n 是个位数)。

2.代码实现

#include <stdio.h>

// 按顺序打印n的每一位
void Print(int n) {
    // 终止条件:n是个位数,直接打印
    if (n > 9) {
        // 递推:先打印n/10的每一位(去掉最后一位)
        Print(n / 10);
    }
    // 回归:打印当前n的最后一位
    printf("%d ", n % 10);
}

int main() {
    int m = 0;
    scanf("%d", &m);
    Print(m);
    return 0;
}

输入和输出结果:

3.画图推演

四、递归的坑:别迷恋 “套娃”,迭代也要掌握

  递归虽然好用,但不是万能的,有时候也会给你挖坑。最典型的例子就是 “求第 n 个斐波那契数”

斐波那契数的规律是:前两个数都是 1,从第三个数开始,每个数等于前两个数的和(1,1,2,3,5,8...)。用递归公式写就是:

  • 当 n<=2 时,斐波那契数 = 1
  • 当 n>2 时,斐波那契数 = Fib (n-1)+Fib (n-2)

这时候就有人会说了,这不一眼递归吗,于是就有

1. 递归写法的坑:重复计算到崩溃

如果直接写递归函数:

#include <stdio.h>

int Fib(int n) {
    if (n <= 2) {
        return 1;
    } else {
        return Fib(n-1) + Fib(n-2);
    }
}

int main() {
    int n = 0;
    scanf("%d", &n);
    printf("%d\n", Fib(n));
    return 0;
}

  当 n=5 的时候还好,很快算出结果;但当 n=50 的时候,你会发现程序卡半天都出不来结果 —— 为啥?

因为递归会重复计算大量相同的值!比如算 Fib (5):

  • Fib(5) = Fib(4) + Fib(3)
  • Fib(4) = Fib(3) + Fib(2)
  • Fib(3) = Fib(2) + Fib(1)

这里 Fib (3) 被计算了 2 次,Fib (2) 被计算了 3 次。当 n 越大,重复计算的次数就越多 —— 有数据统计,算 Fib (40) 时,Fib (3) 会被计算 39088169 次!这纯属浪费时间和内存。

2. 迭代写法:一个循环搞定,从前往后算,效率拉满

#include <stdio.h>

int Fib(int n) {
    // 前两个斐波那契数都是1
    int a = 1, b = 1;
    int c = 1; // 用来存第n个斐波那契数,默认n<=2时是1
    // 从第3个数开始算,直到第n个
    while (n > 2) {
        c = a + b; // 第3个数=第1个+第2个
        a = b;     // 把第2个数变成新的第1个
        b = c;     // 把第3个数变成新的第2个
        n--;       // 计数器减1,直到n=2
    }
    return c;
}

int main() {
    int n = 0;
    scanf("%d", &n);
    printf("%d\n", Fib(n));
    return 0;
}

  有时候,递归虽好,但是也会引⼊⼀些问题,所以我们⼀定不要迷恋递归,适可⽽⽌就好。

五、递归 vs 迭代:什么时候该用哪个?

总结一下递归和迭代的区别,帮你选对方法:

场景递归适合迭代适合
问题复杂度问题复杂,用递归思路更清晰(比如汉诺塔)问题简单,循环就能解决(比如阶乘、斐波那契)
效率要求效率要求不高,递归代码更简洁效率要求高,迭代没有重复计算和栈帧开销
递归深度递归深度浅(比如打印 10 位数以内的整数)递归深度深(比如算第 100 个斐波那契数)

六、实战拓展(青蛙跳台阶 & 汉诺塔

  1.青蛙跳台阶问题

   先看题目:一只青蛙要跳上 n 级台阶,它每次能跳 1 级或 2 级台阶,问这只青蛙有多少种跳法?比如 n=1 时,只有 1 种(跳 1 级);n=2 时,有 2 种(1+1 或直接跳 2 级);n=3 时,有 3 种(1+1+1、1+2、2+1)—— 那 n=100 时呢?总不能一个个数吧?这时候递归就派上用场了!

  • 找规律:把 “n 级跳法” 拆成 “小问题”

要解决 n 级台阶的跳法,先想最后一步:青蛙跳到第 n 级台阶,只有两种可能:

  • 最后一步跳了 1 级:那之前它已经跳完了 n-1 级台阶,跳法数就是 “n-1 级的跳法数”;
  • 最后一步跳了 2 级:那之前它已经跳完了 n-2 级台阶,跳法数就是 “n-2 级的跳法数”。

所以,n 级台阶的总跳法数 = n-1 级的跳法数 + n-2 级的跳法数—— 是不是和斐波那契数有点像?再加上两个 “终止条件”:

  • 当 n=1 时,只有 1 种跳法;
  • 当 n=2 时,有 2 种跳法。

代码实现:

#include <stdio.h>

// 计算n级台阶的跳法数
int jump_floor(int n) {
    // 终止条件:n=1返回1,n=2返回2
    if (n == 1) return 1;
    if (n == 2) return 2;
    // 递推:n级跳法 = n-1级 + n-2级
    return jump_floor(n-1) + jump_floor(n-2);
}

int main() {
    int n;
    printf("请输入台阶数:");
    scanf("%d", &n);
    printf("青蛙有%d种跳法\n", jump_floor(n));
    return 0;
}
  • 迭代法优化

  和斐波那契数一样,上面的递归写法会有大量重复计算(比如算 n=5 时,n=3 会被算 2 次)。如果 n 很大(比如 n=40),会有点慢。这时候可以用 “迭代法” 优化,思路和斐波那契迭代一致:

int jump_floor_iter(int n) {
    if (n == 1) return 1;
    if (n == 2) return 2;
    int a = 1, b = 2, c = 0; // a= n-2级,b= n-1级,c= n级
    for (int i = 3; i <= n; i++) {
        c = a + b;
        a = b;     // 下次循环的n-2级 = 这次的n-1级
        b = c;     // 下次循环的n-1级 = 这次的n级
    }
    return c;
}

2.汉诺塔问题

  汉诺塔是递归的 “经典中的经典”,题目是这样的:有 A、B、C 三根柱子,A 柱上有 n 个大小不同的盘子(大盘在下,小盘在上),要求把 A 柱的盘子全部移到 C 柱,移动规则是:

  1. 每次只能移动一个盘子;
  2. 任何时候都不能让大盘压在小盘上。

比如 n=1 时,直接把 A 的盘子移到 C,1 步搞定;n=2 时,先把 A 的小盘移到 B,再把 A 的大盘移到 C,最后把 B 的小盘移到 C,3 步搞定 —— 那 n=3、n=10 时该怎么移?

1. 找规律:把 “n 个盘子” 拆成3 步

汉诺塔的核心思路是 “大事化小”:要把 A 柱的 n 个盘子移到 C 柱,只需要 3 个关键步骤(重点记!):

  1. 第一步:先把 A 柱最上面的 n-1 个盘子,从 A 移到 B 柱(借助 C 柱当 “过渡”);
  2. 第二步:把 A 柱剩下的 1 个大盘子(最底下那个),直接从 A 移到 C 柱;
  3. 第三步:再把 B 柱上的 n-1 个盘子,从 B 移到 C 柱(借助 A 柱当 “过渡”)。

而 “把 n-1 个盘子从 A 移到 B”“把 n-1 个盘子从 B 移到 C”,又是和原问题一样的 “小汉诺塔问题”,可以继续用同样的逻辑拆分,直到拆分到 “n=1”(终止条件:只有 1 个盘子时,直接从源柱移到目标柱)。

举个 n=3 的例子,帮你理解:

  • 原问题:把 A 的 3 个盘子移到 C → 拆成 3 步:
    1. 把 A 的 2 个盘子移到 B(借助 C);
    2. 把 A 的 1 个大盘移到 C;
    3. 把 B 的 2 个盘子移到 C(借助 A)。
  • 其中 “把 A 的 2 个盘子移到 B” 又拆成 3 步:
    1. 把 A 的 1 个小盘移到 C;
    2. 把 A 的 1 个中盘移到 B;
    3. 把 C 的 1 个小盘移到 B。
  • 最后所有步骤汇总,就是 n=3 的完整移动流程,共 7 步(2³ - 1 步,n 个盘子的总步数是 2ⁿ - 1)。

2. 代码实现:

根据上面的 3 步思路,我们可以写一个递归函数hanoi,需要传入 4 个参数:

  • n:要移动的盘子数量;
  • source:源柱子(比如 A);
  • temp:过渡柱子(比如 B);
  • target:目标柱子(比如 C)。

函数的核心是 “打印每一步的移动动作”,代码如下:

#include <stdio.h>

// 汉诺塔递归函数:把n个盘子从A移到C,借助B
void hanoi(int n, char A, char B, char C) {
    // 终止条件:只有1个盘子,直接从A移到B
    if (n == 1) {
        printf("把盘子从%c移到%c\n", A, C);
        return;
    }
    // 第一步:把n-1个盘子从A移到B,借助C
    hanoi(n-1, A, C, B);
    // 第二步:把剩下的1个大盘从A移到C
    printf("把盘子从%c移到%c\n", A, C);
    // 第三步:把n-1个盘子从B移到C,借助A
    hanoi(n-1, B,A, C);
}

int main() {
    int n;
    printf("请输入汉诺塔的盘子数量:");
    scanf("%d", &n);
    printf("汉诺塔移动步骤:\n");
    hanoi(n, 'A', 'B', 'C'); // A是源,B是过渡,C是目标
    return 0;
}

测试结果:

七、总结

  不管是青蛙跳台阶还是汉诺塔,递归的解题思路其实都一样,记住这 3 步:

  1. 找终止条件:找到 “最小的问题”,直接给出答案(比如青蛙跳台阶的 n=1、n=2,汉诺塔的 n=1);
  2. 拆递推关系:把 “n 个问题” 拆成 “n-1 个问题 + 1 个简单动作”(比如青蛙跳台阶的 n 级 = n-1 级 + n-2 级,汉诺塔的 n 个盘子 = 移 n-1 个 + 移 1 个 + 移 n-1 个);
  3. 写递归函数:把递推关系翻译成代码,让函数自己调用自己。

  好了,今天的递归教学就到这。赶紧去试试写个 递归函数,感受一下 “套娃” 的快乐吧!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值