C语言第9节:函数递归

1.递归的基本概念

1.1 什么是递归

递归是一种通过函数自我调用来解决问题的技术。递归函数在内部会多次调用自己,在每一次调用中,它将处理一个与原问题相似的子问题,但规模会逐渐减小。递归可以看作是将问题逐步简化至最小规模,从而实现对整个问题的解决。

递归调用通常包含两个部分:

  1. 递归基(Base Case):终止递归的条件,当满足此条件时,递归将停止。
  2. 递归关系(Recursive Case):即递归调用函数自身,通过递归关系逐步缩小问题的规模,最终达到递归基。

1.2 递归的结构

递归函数的结构非常规律,通常可以用以下形式表示:

返回类型 函数名(参数列表) {
    if (终止条件) {
        // 当满足终止条件时返回结果
        return 结果;
    } else {
        // 递归调用
        return 函数名(缩小后的参数);
    }
}

递归在书写的时候,有2个必要条件:

  • 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
  • 每次递归调用之后越来越接近这个限制条件。

2.递归的实际案例

2.1 阶乘的递归实现

一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。 自然数n的阶乘写作 n!。

题目:计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累积相乘。

阶乘是递归的一个经典示例。阶乘定义为:

  • 当 n = 0时,0!=1。
  • 当 n>0 时,n!=n×(n−1)!n!。

从这个公式不难看出:n的阶乘和n-1的阶乘是相似的问题,但是规模要少了n。有一种有特殊情况是:当n == 0 的时候,n的阶乘是1,而其余n的阶乘都是可以通过上面的公式计算。

在这里插入图片描述

2.1.1 调用过程分析

factorial(4)为例,函数的递归过程如下:

  1. factorial(4) 调用 4 * factorial(3)
  2. factorial(3) 调用 3 * factorial(2)
  3. factorial(2) 调用 2 * factorial(1)
  4. factorial(1) 调用 1 * factorial(0)
  5. factorial(0) 返回 1,停止递归

在递归基 factorial(0) 返回后,结果会逐步返回到上一层,直到最终计算完成:

  1. factorial(1) 计算结果 1 * 1 = 1,返回 1
  2. factorial(2) 计算结果 2 * 1 = 2,返回 2
  3. factorial(3) 计算结果 3 * 2 = 6,返回 6
  4. factorial(4) 计算结果 4 * 6 = 24,返回 24

最终结果为 4! = 24

2.1.2 实现代码

#include <stdio.h>
int factorial(int n) {
    if (n == 0) {
        return 1; // 递归基:当n为0时返回1
    } else {
        return n * factorial(n - 1); // 递归关系:n! = n * (n-1)!
    }
}

int main()
{
    printf("%d", factorial(4));
    return 0;
}

2.1.3 递归调用过程图示

factorial(4)
    └── 4 * factorial(3)
           └── 3 * factorial(2)
                  └── 2 * factorial(1)
                         └── 1 * factorial(0)
                                └── return 1  (递归基)

                         <- 1 * 1 = 1  返回 1
                  <- 2 * 1 = 2  返回 2
           <- 3 * 2 = 6  返回 6
    <- 4 * 6 = 24  返回 24

在这里插入图片描述
在这里插入图片描述

2.2 递归实现顺序打印一个整数的每一位

输入一个整数m,按照顺序打印整数的每一位。

比如:

输入:114514 输出:1 1 4 5 1 4

输入:5201314 输出:5 2 0 1 3 1 4

2.2.1 思路分析

递归实现的思路是:

  1. 通过递归调用来逐步缩小整数的规模,每次递归去掉最后一位,直到只剩下最高位。
  2. 当整数只剩下一位时,打印该位并返回。
  3. 在递归返回的过程中,逐步打印每一位剩余的数字。

2.2.2 步骤详细说明

  • 递归基:当整数 n 小于 10 时,说明 n 已经是个位数,可以直接打印。
  • 递归关系:在当前整数 n 大于等于 10 的情况下,先递归调用 n / 10,然后再打印 n % 10,即最后一位数字。

通过 n / 10 可以去掉当前的个位数字,并让问题缩小规模进入下一次递归调用;通过 n % 10 可以得到当前数字的个位数,打印出来。

2.2.3 代码实现

#include <stdio.h>

void printDigits(int n) {
    if (n < 10) {
        // 递归基:如果 n 是个位数,直接打印
        printf("%d ", n);
    } else {
        // 递归关系:先打印前面几位
        printDigits(n / 10);
        // 打印当前的个位数
        printf("%d ", n % 10);
    }
}

int main() {
    int number = 12345;
    printf("顺序打印数字:");
    printDigits(number);
    return 0;
}

2.2.4 递归调用过程的图示

可以将递归调用过程理解为一个从上到下的逐步分解过程,最终到达递归基,然后再逐层返回,打印每一位数字:

printDigits(12345)
    └── printDigits(1234)
          └── printDigits(123)
                └── printDigits(12)
                      └── printDigits(1) -> 打印 "1 "
                <- 打印 "2 "
          <- 打印 "3 "
    <- 打印 "4 "
<- 打印 "5 "

2.3 斐波那契数列的递归实现

斐波那契数列也是递归的经典应用,其定义为:

  • F(0)=0
  • F(1)=1
  • F(n)=F(n−1)+F(n−2),当 n≥2 时。

2.3.1 调用过程分析

当调用 fibonacci(4) 时,递归调用会按照以下步骤进行:

  1. fibonacci(4) 调用 fibonacci(3) + fibonacci(2)
  2. fibonacci(3) 调用 fibonacci(2) + fibonacci(1)
  3. fibonacci(2) 调用 fibonacci(1) + fibonacci(0)
  4. fibonacci(1) 返回 1(递归基)
  5. fibonacci(0) 返回 0(递归基)
  6. fibonacci(2) 计算结果为 1 + 0 = 1,返回 1
  7. fibonacci(1) 返回 1(递归基)
  8. fibonacci(3) 计算结果为 1 + 1 = 2,返回 2
  9. fibonacci(2) 调用 fibonacci(1) + fibonacci(0)
  10. fibonacci(1) 返回 1(递归基)
  11. fibonacci(0) 返回 0(递归基)
  12. fibonacci(2) 计算结果为 1 + 0 = 1,返回 1
  13. fibonacci(4) 计算结果为 2 + 1 = 3,返回 3

最终结果为 fibonacci(4) = 3

2.3.2 代码实现

int fibonacci(int n) {
    if (n == 0) return 0; // 递归基1
    if (n == 1) return 1; // 递归基2
    return fibonacci(n - 1) + fibonacci(n - 2); // 递归关系
}

2.3.3递归调用过程图示

下面是 fibonacci(4) 的递归调用过程图示:

fibonacci(4)
    └── fibonacci(3) + fibonacci(2)
           ├── fibonacci(3)
           │     └── fibonacci(2) + fibonacci(1)
           │            ├── fibonacci(2)
           │            │     └── fibonacci(1) + fibonacci(0)
           │            │            ├── fibonacci(1) -> 1
           │            │            └── fibonacci(0) -> 0
           │            │     <- 返回 1 + 0 = 1
           │            └── fibonacci(1) -> 1
           │     <- 返回 1 + 1 = 2
           └── fibonacci(2)
                  └── fibonacci(1) + fibonacci(0)
                         ├── fibonacci(1) -> 1
                         └── fibonacci(0) -> 0
                  <- 返回 1 + 0 = 1
    <- 返回 2 + 1 = 3

在这里插入图片描述

3. 递归的应用场景

递归非常适合处理那些可以分解为多个相似子问题的问题,常见的递归应用场景包括:

  1. 数学运算:如阶乘、斐波那契数列、幂运算等。
  2. 分治算法:递归特别适用于分治法,如快速排序和归并排序。
  3. 树和图的遍历:递归适合用来遍历树结构,因为树本质上具有层次关系,每一层都可以看作是相似的子树。
  4. 回溯算法:在解决需要回溯的问题时,如迷宫问题、八皇后问题等,递归能有效地实现回溯过程。

示例:快速排序

快速排序是一种高效的排序算法,它通过递归的方式将数组分为两个子数组,分别对每个子数组进行排序。快速排序的递归实现如下:

void quickSort(int arr[], int low, int high) {
    if (low < high) {
        int pivot = partition(arr, low, high);
        quickSort(arr, low, pivot - 1);  // 对左子数组排序
        quickSort(arr, pivot + 1, high); // 对右子数组排序
    }
}

4. 递归的优缺点

4.1 优点

  • 代码简洁:递归函数通常比迭代更简单直观,尤其是对于分治类问题,递归可以自然表达问题的分解过程。
  • 符合逻辑:递归算法更贴近问题的数学定义和逻辑,因此更容易理解和实现。

4.2 缺点

  • 效率低 开销大

    • 在C语言中每一次函数调用,都需要为本次函数调用在内存的栈区,申请一块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧
    • 函数不返回,函数对应的栈帧空间就一直占用,所以如果函数调用中存在递归调用的话,每一次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
    • 所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起**栈溢出(stack overflow)**的问题。
  • 重复计算:在某些递归中会出现重复计算问题,如斐波那契数列的递归计算会重复求解某些子问题。

为了解决这些问题,可以使用“记忆化”(将计算结果缓存以避免重复计算)或通过迭代进行优化。

5. 递归与迭代的比较

递归和迭代都是实现重复操作的手段,但在实现上各有特点:

  • 递归:递归通过函数的自我调用实现,代码简洁,逻辑清晰,适合树、图等层次结构的遍历。
  • 迭代:迭代通过循环实现,通常效率更高,适合计算密集型问题。

5.1 示例:斐波拉契数列

斐波那契数列可以通过递归和迭代两种方式来实现,但这两种实现方式在性能、内存消耗以及复杂度等方面有明显差异。下面我们来比较迭代与递归实现斐波那契数列的优缺点,并展示具体的代码实现和效率分析。

5.1.1 递归实现(略)

5.1.1.1 递归的特点
  • 简洁性:代码非常简洁,直接按照数学定义进行实现,符合斐波那契数列的数学递推公式。
  • 逻辑清晰:递归实现方式容易理解,代码结构紧凑,逻辑直观。
5.1.1.2 递归的缺点
  • 效率低:递归实现的时间复杂度为 O(2^n),因为每次调用 fibonacci(n) 都会再次调用 fibonacci(n-1)fibonacci(n-2),导致大量的重复计算。例如,在计算 fibonacci(5) 时,会多次计算 fibonacci(3)fibonacci(2)
  • 内存消耗大:每次递归调用都需要保存当前的函数状态在栈中,如果递归层数过深,可能导致栈溢出。

5.1.2 迭代实现

迭代实现则采用循环的方式,按顺序计算每一个斐波那契数。

int fibonacci_iterative(int n) {
    if (n == 0) return 0;
    int a = 0, b = 1, c;
    for (int i = 2; i <= n; i++) {
        c = a + b;
        a = b;
        b = c;
    }
    return b;
}
5.1.2.1 迭代的特点
  • 高效:迭代的时间复杂度为 O(n)。每个斐波那契数只需计算一次,因此避免了重复计算。
  • 低内存消耗:迭代实现只需要常量级的空间来保存当前和前一个斐波那契数,因此空间复杂度为 O(1),不会出现栈溢出的问题。
5.1.2.2 迭代的缺点
  • 代码略复杂:相比递归实现,迭代实现需要引入额外的变量来存储中间结果,代码相对较长,逻辑上不如递归简洁。

5.1.3 递归与迭代的比较总结

特性递归实现迭代实现
代码简洁性简洁、符合数学定义略复杂,需要引入额外变量
效率时间复杂度为 O(2^n),低效时间复杂度为 O(n),高效
内存消耗占用栈空间,递归层次深时容易栈溢出常量空间 O(1),不占用栈空间
适用情况适用于小规模或递归结构较浅的情况适用于大规模计算,性能更高

六、编写递归函数的注意事项

编写递归函数时,注意以下几个方面:

  1. 明确递归基:递归基是递归停止的条件,确保递归能在有限次数内结束。
  2. 减少重复计算:对于斐波那契等容易出现重复计算的递归,可以使用记忆化技术将已计算的值缓存起来。
  3. 控制递归深度:递归深度太深会导致栈溢出,通常可以设定最大递归深度,或者选择迭代替代递归。
  4. 调试技巧:递归调用时的参数和返回值变化较多,可以通过调试打印输出每次调用的参数和返回值,有助于分析问题所在。

七、总结

递归是一种功能强大的编程技巧,它能够将复杂的问题转化为较小的子问题,从而逐步求解。虽然递归实现的代码通常简洁,但在一些场景下会带来性能问题。掌握递归的基本原理和实现方法,理解递归和迭代的差异,对于程序的优化与调试至关重要。在实际应用中,选择递归或迭代需要结合具体问题的特点,合理运用递归技术,可以使代码简洁且高效。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值