首页
学习
活动
专区
圈层
工具
发布

《递归与迭代:从斐波那契到汉诺塔的算法精髓》

🔥个人主页:莉丝努力练剑 ❄专栏传送门:《C语言》、数据结构与算法》、C语言刷题12天IO强训、LeetCode代码强化刷题、谷刷题、C/C++基础知识知识强化补充、C/C++干货分享&学习过程记录 🍉学习方向:C/C++方向学习者 ⭐️人生格言:为天地立心,为生民立命,为往圣继绝学,为万世开太

前言:距离我们学完C语言已经过去一段时间了,不知道大家还记不记得博主挖过的一个坑——就是在【掌握递归】函数递归思想的形成及其应用中介绍斐波那契数列的时候提到的青蛙跳台阶、汉诺塔问题,博主回收伏笔啦!希望大家能够有所收获!


补充两者特点:递归VS迭代

0.1 递归

(1)会占用更多的内存空间,有可能导致栈溢出的问题; (2)性能的下降; (3)有的复杂问题使用递归描述会很简洁,写成代码也非常的方便。

0.2 迭代

(1)效率高; (2)迭代的方式有时候不容易想到。

0.3 学习数据结构之后

(1)写成递归程序,就几行代码; (2)但是,不允许使用递归的时候,可能得写几十行代码。


正文

一、递归 (Recursion)

比如这段代码就是递归——

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>

// 递归计算阶乘示例
int factorial(int n) {
    // 1. 递归终止条件
    if (n == 0 || n == 1) {
        return 1;
    }
    // 2. 递归调用:缩小问题规模
    return n * factorial(n - 1);
}

int main() 
{
    printf("5! = %d\n", factorial(5)); // 输出 120
    return 0;
}

1.1 什么是递归

递归是一种解决问题的方法,它把一个问题分解为一个或多个规模更小的同类子问题,直到子问题简单到可以直接解决。

递归的核心思想是自我调用。一个递归函数会直接或间接地调用自身。

比如这种——

再比如说像这种——

1.2 递归的三大要素

1、明确递归终止条件 (Base Case):必须有一个明确的递归结束条件,也称为“出口”。否则,函数会无限调用自己,导致栈溢出错误。

2、给出递归终止时的处理办法:当Base Case被触发时,应该直接返回一个明确的值。

3、提取重复逻辑,缩小问题规模 (Recursive Case):每次递归调用都应该是为了解决一个更小、更接近终止条件的子问题。

1.3 递归的优缺点

1.3.1 优点

代码简洁、清晰,对于解决一些本质就是递归定义的问题(如树、图的遍历)非常有效。

1.3.1 缺点

(1)栈溢出风险 (Stack Overflow):每次函数调用都会在内存栈中分配空间,递归深度过大会耗尽栈空间。 (2)重复计算:如斐波那契数列的递归实现,会进行大量重复计算,效率低下。 (3)时间和空间复杂度高:函数调用的开销比循环大。


二、迭代 (Iteration)

迭代代码演示:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>

// 迭代计算阶乘示例
int factorial_iter(int n) {
    int result = 1;
    for (int i = 1; i <= n; i++) {
        result *= i;
    }
    return result;
}

int main() 
{
    printf("5! = %d\n", factorial_iter(5)); // 输出 120
    return 0;
}

2.1 什么是迭代(简单理解就是循环)

迭代是一种利用循环结构(如 for,while,do...while)来重复执行一段代码,通过变量的不断更新来逐步逼近答案的方法。

2.2 迭代的要素

  • 循环条件:控制循环何时继续、何时终止。
  • 计数器/状态变量:在循环过程中不断更新,记录当前的状态或进度。
  • 循环体:需要重复执行的核心逻辑。

2.3 迭代 vs. 递归

特性

递归 (Recursion)

迭代 (Iteration)

实现

函数调用自身

循环结构

终止

终止条件 (Base Case)

循环条件

效率

通常较低(函数调用开销,栈空间占用)

通常较高(无额外函数调用开销)

应用

树、图遍历,分治,回溯等

大部分循环可以解决的问题

可读性

对于递归问题,代码更简洁易读

逻辑直白,但可能代码更长

结论所有递归问题都可以用迭代来实现,反之亦然。迭代通常效率更高,是递归的优化方向。选择哪种方式取决于问题的性质和对效率的要求。


三、斐波那契数列 (Fibonacci Sequence)

3.1 问题描述

斐波那契数列的定义是一个递归定义:

  • F(0) = 0
  • F(1) = 1
  • F(n) = F(n-1) + F(n-2) (n >= 2)

数列:0, 1, 1, 2, 3, 5, 8, 13, 21...

3.2 斐波那契数列实现

3.2.1 方法一:递归实现 (不推荐)

这是最直观的实现,但效率极低。计算 fib(5) 的过程如下图所示,充满了重复计算,时间复杂度为O(2^n)

代码演示:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>

int fib_recursive(int n) {
    // 递归终止条件
    if (n < 2) {
        return n;
    }
    // 递归调用:缩小规模
    return fib_recursive(n - 1) + fib_recursive(n - 2);
}

int main() 
{
    printf("fib(6) = %d\n", fib_recursive(6)); // 输出 8
    return 0;
}
3.2.2 方法二:迭代实现 (推荐)

使用循环,从底向上计算,只需记录前两个状态,时间复杂度为 O(n),空间复杂度为 O(1)

代码演示:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>

int fib_iterative(int n) {
    if (n < 2) {
        return n;
    }
    
    int a = 0, b = 1; // 初始化前两个数
    int temp;
    
    for (int i = 2; i <= n; i++) {
        temp = a + b; // 计算下一个数
        a = b;        // 更新a
        b = temp;     // 更新b
    }
    return b;
}

int main() 
{
    printf("fib(6) = %d\n", fib_iterative(6)); // 输出 8
    return 0;
}

四、汉诺塔问题 (Tower of Hanoi)

4.1 问题描述

有三根柱子(A, B, C),A柱子上从下到上按从大到小的顺序摞着n个圆盘。要求借助B柱子,将A柱子上的所有圆盘移动到C柱子上,并且每次只能移动一个盘子,且大盘子不能放在小盘子上面

4.2 递归思路

4.2.1 Base Case

如果只有1个盘子,直接将它从 A 移动到 C。

4.2.2 Recursive Case

a. 将 A 上面的 n-1 个盘子借助 C 移动到 B。 b. 将 A 最底下的第 n 个盘子直接移动到 C。 c. 将 B 上的 n-1 个盘子借助 A 移动到 C。

时间复杂度O(2^n),移动步数是 2^n - 1。

4.3 汉诺塔问题实现

C语言代码演示:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>

/**
 * @param n: 盘子数量
 * @param source: 源柱子 (起点)
 * @param auxiliary: 辅助柱子 (中转站)
 * @param target: 目标柱子 (终点)
 */
void hanoi(int n, char source, char auxiliary, char target) {
    if (n == 1) {
        // 递归终止条件:只有一个盘子,直接移动
        printf("Move disk 1 from %c to %c\n", source, target);
        return;
    }
    
    // Step 1: 将 n-1 个盘子从 source 借助 target 移动到 auxiliary
    hanoi(n - 1, source, target, auxiliary);
    
    // Step 2: 将第 n 个盘子从 source 直接移动到 target
    printf("Move disk %d from %c to %c\n", n, source, target);
    
    // Step 3: 将 n-1 个盘子从 auxiliary 借助 source 移动到 target
    hanoi(n - 1, auxiliary, source, target);
}

int main() 
{
    // 移动3个盘子,从A借助B到C
    hanoi(3, 'A', 'B', 'C');
    return 0;
}

这是一个典型的接口型代码。

输出

代码语言:javascript
代码运行次数:0
复制
Move disk 1 from A to C
Move disk 2 from A to B
Move disk 1 from C to B
Move disk 3 from A to C
Move disk 1 from B to A
Move disk 2 from B to C
Move disk 1 from A to C

五、青蛙跳台阶问题

5.1 问题描述

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

5.2 问题分析

这其实是一个斐波那契数列的变种问题。

  • 假设 f(n) 是跳上 n 级台阶的跳法总数。
  • 最后一步只有两种可能:
代码语言:txt
复制
1.  跳1级台阶:那么前面 `n-1` 级台阶有 `f(n-1)` 种跳法。
代码语言:txt
复制
2.  跳2级台阶:那么前面 `n-2` 级台阶有 `f(n-2)` 种跳法。
  • 因此,f(n) = f(n-1) + f(n-2)
  • Base Case:
代码语言:txt
复制
-  `f(1) = 1` (跳1级:一种方法)
代码语言:txt
复制
-  `f(2) = 2` (跳2级:一次跳2级,或分两次每次跳1级)

可以发现,除了初始项 f(1)=1, f(2)=2,递推公式和斐波那契数列完全一样。

5.3 代码实现 (迭代,推荐)

同样,为了避免递归的低效,我们使用迭代方法。

代码演示:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>

int jump_floor(int n) {
    if (n <= 2) {
        return n;
    }
    
    int a = 1, b = 2; // a代表f(n-2),b代表f(n-1)
    int temp;
    
    for (int i = 3; i <= n; i++) {
        temp = a + b; // 计算f(n)
        a = b;        // 更新a为f(n-2)
        b = temp;     // 更新b为f(n-1)
    }
    return b;
}

int main() 
{
    printf("跳5级台阶的方法数: %d\n", jump_floor(5)); // 输出 8
    return 0;
}
5.4 变种问题

如果青蛙一次可以跳 1级、2级... 或 n级,那么跳法总数是 2^(n-1),可以用数学归纳法证明。


六、总结环节(以表格形式呈现)

问题/概念

核心思想

推荐解法

关键点

递归

自我调用,分解为子问题

-

牢记三大要素,尤其是终止条件

迭代

循环结构,更新状态变量

-

效率通常高于递归

斐波那契数列

F(n)=F(n-1)+F(n-2)

迭代

递归法有大量重复计算,必须优化

汉诺塔

分治思想,分解移动步骤

递归

理解“借助”的含义,递归思路最清晰

青蛙跳台阶

动态规划/斐波那契数列

迭代

分析最后一步的可能性,得出递推式


七、测试

在最后的最后,有了前面内容的铺垫,我们写一个完整的测试程序。

代码演示——

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>

// 函数声明
int factorial(int n);
int factorial_iter(int n);
int fib_recursive(int n);
int fib_iterative(int n);
void hanoi(int n, char source, char auxiliary, char target);
int jump_floor(int n);

int main() {
    printf("=== 递归和迭代算法示例 ===\n\n");
    
    printf("1. 递归阶乘: 5! = %d\n", factorial(5));
    printf("2. 迭代阶乘: 5! = %d\n\n", factorial_iter(5));
    
    printf("3. 递归斐波那契(fib(6)): %d\n", fib_recursive(6));
    printf("4. 迭代斐波那契(fib(6)): %d\n\n", fib_iterative(6));
    
    printf("5. 青蛙跳5级台阶的方法数: %d\n\n", jump_floor(5));
    
    printf("6. 汉诺塔问题 (3个盘子):\n");
    hanoi(3, 'A', 'B', 'C');
    
    return 0;
}

// 函数定义
int factorial(int n) {
    if (n == 0 || n == 1) 
    {
        return 1;
    }
    return n * factorial(n - 1);
}

int factorial_iter(int n) {
    int result = 1;
    for (int i = 1; i <= n; i++) 
    {
        result *= i;
    }
    return result;
}

int fib_recursive(int n) {
    if (n < 2) {
        return n;
    }
    return fib_recursive(n - 1) + fib_recursive(n - 2);
}

int fib_iterative(int n) {
    if (n < 2) {
        return n;
    }
    
    int a = 0, b = 1;
    int temp;
    
    for (int i = 2; i <= n; i++) 
    {
        temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

void hanoi(int n, char source, char auxiliary, char target) {
    if (n == 1) 
    {
        printf("Move disk 1 from %c to %c\n", source, target);
        return;
    }
    
    hanoi(n - 1, source, target, auxiliary);
    printf("Move disk %d from %c to %c\n", n, source, target);
    hanoi(n - 1, auxiliary, source, target);
}

int jump_floor(int n) 
{
    if (n <= 2) 
    {
        return n;
    }
    
    int a = 1, b = 2;
    int temp;
    
    for (int i = 3; i <= n; i++) 
    {
        temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

编译和运行一下:

代码语言:javascript
代码运行次数:0
复制
gcc -o recursion_examples recursion_examples.c
./recursion_examples

最后,博主想说,理解这些经典问题,对于掌握算法设计和分析的基本思想至关重要。


结尾

往期回顾:

深入详解C语言数组:承上启下——从C语言数组基础到数据结构衔接

结语:感谢大家的阅读,记得给博主“一键四连”,感谢友友们的支持和鼓励!

下一篇
举报
领券