首页 > 其他分享 >深入理解递归与递归实战

深入理解递归与递归实战

时间:2024-10-22 18:16:48浏览次数:3  
标签:实战 递归 int rod unsigned factorial 深入 fibonacci

在这里插入图片描述

递归的基本概念

递归是一种强大的编程技术,它允许一个函数调用自身来解决问题。递归的基本思想是将复杂的问题分解成更小的子问题,直到问题足够简单可以直接解决为止。递归通常包含两个主要部分:

  1. 基本情况(Base Case):这是递归调用的结束条件,也就是最简单的情况,可以直接得到答案。
  2. 递归步骤(Recursive Step):这是函数调用自身的地方,每次调用时问题规模都会减小。
递归的原理

递归利用了函数调用栈的特性。当一个函数调用自身时,新的局部变量和参数会被压入栈中,等待当前函数执行完毕后恢复现场并返回上一层。这种机制保证了递归过程中各个层级的数据独立,并且能够正确地返回到之前的调用点。

  • 栈帧:每当函数被调用时,都会在栈上创建一个新的栈帧,用于存储函数的局部变量、参数以及返回地址等信息。
  • 栈帧的生命周期:当函数调用完成并返回时,相应的栈帧就会被弹出栈,释放所占用的内存。
常见的递归算法
阶乘函数

阶乘是一个经典的递归示例。n! 表示 1 × 2 × ... × n

unsigned long factorial(unsigned int n) {
    if (n == 0) { // 基本情况
        return 1;
    }
    return n * factorial(n - 1); // 递归步骤
}
  • 原理:阶乘函数通过递归不断减小问题规模,直到达到基本情况 n == 0
  • 示例:计算 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 (基本情况)
    6. 从内向外计算结果:1 * 1 = 1, 2 * 1 = 2, 3 * 2 = 6, 4 * 6 = 24
斐波那契数列

斐波那契数列是一个数列,其中每个数字是前两个数字的和。

unsigned long fibonacci(unsigned int n) {
    if (n <= 1) { // 基本情况
        return n;
    }
    return fibonacci(n - 1) + fibonacci(n - 2); // 递归步骤
}
  • 原理:斐波那契数列通过递归计算前两项的和,直到达到基本情况 n <= 1
  • 示例:计算 fibonacci(5),调用过程如下:
    1. fibonacci(5) -> fibonacci(4) + fibonacci(3)
    2. fibonacci(4) -> fibonacci(3) + fibonacci(2)
    3. fibonacci(3) -> fibonacci(2) + fibonacci(1)
    4. fibonacci(2) -> fibonacci(1) + fibonacci(0)
    5. fibonacci(1) -> 1 (基本情况)
    6. fibonacci(0) -> 0 (基本情况)
    7. 从内向外计算结果:0 + 1 = 1, 1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5
汉诺塔问题

汉诺塔是一个古老的数学游戏,目标是将所有盘子从一根柱子移动到另一根柱子上,每次只能移动一个盘子,而且任何时候都不能把小盘子放在大盘子下面。

void hanoi(unsigned int n, char from_rod, char to_rod, char aux_rod) {
    if (n == 1) { // 基本情况
        printf("Move disk 1 from rod %c to rod %c\n", from_rod, to_rod);
        return;
    }
    hanoi(n - 1, from_rod, aux_rod, to_rod); // 递归步骤1
    printf("Move disk %u from rod %c to rod %c\n", n, from_rod, to_rod);
    hanoi(n - 1, aux_rod, to_rod, from_rod); // 递归步骤2
}
  • 原理:汉诺塔问题通过递归解决了将盘子从一根柱子移动到另一根柱子的问题。每次递归都分为两步,第一步将 n-1 个盘子移动到辅助柱子上,第二步将最大的盘子移动到目标柱子,第三步将 n-1 个盘子移动到目标柱子上。
  • 示例:计算 hanoi(3, 'A', 'C', 'B'),调用过程如下:
    1. hanoi(3, 'A', 'C', 'B')
    2. hanoi(2, 'A', 'B', 'C')
    3. hanoi(1, 'A', 'C', 'B')
    4. 移动第一个盘子:Move disk 1 from rod A to rod C
    5. hanoi(1, 'A', 'B', 'C')
    6. 移动第二个盘子:Move disk 1 from rod A to rod B
    7. hanoi(1, 'C', 'B', 'A')
    8. 移动第三个盘子:Move disk 1 from rod C to rod B
    9. 移动最大的盘子:Move disk 3 from rod A to rod C
    10. hanoi(1, 'B', 'A', 'C')
    11. 移动第二个盘子:Move disk 1 from rod B to rod A
    12. hanoi(1, 'B', 'C', 'A')
    13. 移动第一个盘子:Move disk 1 from rod B to rod C
    14. hanoi(1, 'A', 'C', 'B')
    15. 移动第二个盘子:Move disk 1 from rod A to rod C
进阶话题

在这里插入图片描述

尾递归

尾递归是一种特殊的递归形式,其中递归调用是函数中的最后一个操作。尾递归可以被编译器优化为迭代,从而避免堆栈溢出的风险。

unsigned long factorial_tail(unsigned int n, unsigned long accumulator) {
    if (n == 0) {
        return accumulator;
    }
    return factorial_tail(n - 1, n * accumulator);
}

unsigned long factorial(unsigned int n) {
    return factorial_tail(n, 1);
}
  • 原理:尾递归函数的每次调用都在函数末尾进行,这样编译器可以重用当前栈帧,而不是创建新的栈帧。这使得尾递归在某些情况下可以像循环一样高效。
  • 示例:计算 factorial(4),调用过程如下:
    1. factorial(4) -> factorial_tail(4, 1)
    2. factorial_tail(4, 1) -> factorial_tail(3, 4)
    3. factorial_tail(3, 4) -> factorial_tail(2, 12)
    4. factorial_tail(2, 12) -> factorial_tail(1, 24)
    5. factorial_tail(1, 24) -> factorial_tail(0, 24)
    6. factorial_tail(0, 24) -> 24 (基本情况)
递归与动态规划

递归算法在某些情况下效率较低,因为它会重复计算相同的子问题。动态规划通过存储之前计算的结果来避免重复计算,从而提高效率。

unsigned long fibonacci_dp(unsigned int n) {
    static unsigned long cache[100] = {0}; // 缓存结果
    if (cache[n]) {
        return cache[n];
    }
    if (n <= 1) {
        return n;
    }
    cache[n] = fibonacci_dp(n - 1) + fibonacci_dp(n - 2);
    return cache[n];
}
  • 原理:动态规划通过记录已经计算过的子问题结果来避免重复计算。这种方法通常使用数组或哈希表来缓存结果。
  • 示例:计算 fibonacci_dp(5),调用过程如下:
    1. fibonacci_dp(5) -> fibonacci_dp(4) + fibonacci_dp(3)
    2. fibonacci_dp(4) -> fibonacci_dp(3) + fibonacci_dp(2)
    3. fibonacci_dp(3) -> fibonacci_dp(2) + fibonacci_dp(1)
    4. fibonacci_dp(2) -> fibonacci_dp(1) + fibonacci_dp(0)
    5. fibonacci_dp(1) -> 1 (基本情况)
    6. fibonacci_dp(0) -> 0 (基本情况)
    7. 从内向外计算结果:0 + 1 = 1, 1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5
    8. 每次计算的结果都会被缓存起来,避免重复计算。
递归与分治法

分治法是一种将问题分成若干个较小的子问题来求解的策略,通常可以使用递归来实现。分治法在排序算法(如快速排序和归并排序)、搜索算法(如二分查找)等方面都有广泛的应用。

void merge_sort(int array[], int start, int end) {
    if (start >= end) {
        return;
    }
    int mid = (start + end) / 2;
    merge_sort(array, start, mid); // 排序左半边
    merge_sort(array, mid + 1, end); // 排序右半边
    merge(array, start, mid, end); // 合并两个已排序的部分
}

void merge(int array[], int start, int mid, int end) {
    int size_left = mid - start + 1;
    int size_right = end - mid;
    int left[size_left], right[size_right];

    for (int i = 0; i < size_left; i++) {
        left[i] = array[start + i];
    }
    for (int j = 0; j < size_right; j++) {
        right[j] = array[mid + 1 + j];
    }

    int i = 0, j = 0, k = start;

    while (i < size_left && j < size_right) {
        if (left[i] <= right[j]) {
            array[k++] = left[i++];
        } else {
            array[k++] = right[j++];
        }
    }

    while (i < size_left) {
        array[k++] = left[i++];
    }

    while (j < size_right) {
        array[k++] = right[j++];
    }
}
  • 原理:分治法将原问题分成两个或多个子问题,递归地解决子问题,最后合并子问题的解来构造原问题的解。
  • 示例:使用归并排序对数组 {4, 3, 1, 5, 2} 进行排序。
    1. merge_sort({4, 3, 1, 5, 2})
    2. merge_sort({4, 3})
    3. merge_sort({1, 5})
    4. merge_sort({2}) (基本情况)
    5. 合并 {2}
    6. merge_sort({1, 5})
    7. 合并 {1, 5}
    8. merge_sort({4, 3})
    9. 合并 {3, 4}
    10. merge_sort({4, 3, 1, 5})
    11. 合并 {1, 3, 4, 5}
    12. merge_sort({4, 3, 1, 5, 2})
    13. 合并 {1, 2, 3, 4, 5}
递归的优缺点
  • 优点

    • 代码简洁清晰,易于理解和维护。
    • 对于某些问题(如树和图的遍历),递归是自然的解决方法。
  • 缺点

    • 递归可能导致大量的函数调用,消耗更多的内存空间。
    • 在没有优化的情况下,递归可能会导致堆栈溢出。
    • 递归的效率有时低于非递归版本(如迭代版本)。
递归的调试技巧
  • 使用断言检查基本情况是否正确。
  • 打印递归的调用层次,帮助理解递归过程。
  • 使用调试工具逐步执行递归函数,观察变量的变化。
实践案例分析
计算斐波那契数列
#include <stdio.h>

unsigned long fibonacci(unsigned int n);

int main() {
    unsigned int n;
    printf("Enter the number of terms in Fibonacci sequence: ");
    scanf("%u", &n);

    printf("The first %u terms of the Fibonacci sequence are:\n", n);
    for (int i = 0; i < n; i++) {
        printf("%lu ", fibonacci(i));
    }
    printf("\n");

    return 0;
}

unsigned long fibonacci(unsigned int n) {
    if (n <= 1) {
        return n;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}
  • 运行示例
    输入:10
    输出:0 1 1 2 3 5 8 13 21 34
汉诺塔问题
#include <stdio.h>

void hanoi(unsigned int n, char from_rod, char to_rod, char aux_rod);

int main() {
    unsigned int n;
    printf("Enter the number of disks: ");
    scanf("%u", &n);

    printf("Solution to move %u disks from A to C using B as auxiliary:\n", n);
    hanoi(n, 'A', 'C', 'B');

    return 0;
}

void hanoi(unsigned int n, char from_rod, char to_rod, char aux_rod) {
    if (n == 1) {
        printf("Move disk 1 from rod %c to rod %c\n", from_rod, to_rod);
        return;
    }
    hanoi(n - 1, from_rod, aux_rod, to_rod);
    printf("Move disk %u from rod %c to rod %c\n", n, from_rod, to_rod);
    hanoi(n - 1, aux_rod, to_rod, from_rod);
}
  • 运行示例
    输入:3
    输出:
    Move disk 1 from rod A to rod C
    Move disk 1 from rod A to rod B
    Move disk 1 from rod C to rod B
    Move disk 3 from rod A to rod C
    Move disk 1 from rod B to rod A
    Move disk 1 from rod B to rod C
    Move disk 1 from rod A to rod C
    
更深层次的探讨
递归与尾调用优化
  • 尾调用优化:在支持尾调用优化的语言中,编译器可以将尾递归转化为循环,从而避免栈溢出。
  • 原理:尾调用优化的关键在于递归调用是函数的最后一项操作,因此编译器可以重用当前栈帧,而不是创建新的栈帧。
  • 示例:考虑以下尾递归函数:
    unsigned long factorial_tail(unsigned int n, unsigned long accumulator) {
        if (n == 0) {
            return accumulator;
        }
        return factorial_tail(n - 1, n * accumulator);
    }
    
递归与内存管理
  • 栈溢出:递归深度过大时,可能会导致栈溢出。这是因为每次函数调用都会占用一定的栈空间。
  • 优化方法
    • 尾调用优化:如前所述。
    • 迭代替代:将递归改写为迭代形式。
    • 增加栈大小:在某些情况下,可以通过编译器选项或操作系统配置来增加栈的大小。
  • 示例:使用迭代替代递归计算阶乘:
    unsigned long factorial_iterative(unsigned int n) {
        unsigned long result = 1;
        for (unsigned int i = 1; i <= n; i++) {
            result *= i;
        }
        return result;
    }
    
递归与算法复杂度
  • 时间复杂度:递归算法的时间复杂度通常取决于递归的层数和每层的计算量。
  • 空间复杂度:递归算法的空间复杂度取决于递归的层数,因为每次递归调用都会占用一定的栈空间。
  • 示例:考虑斐波那契数列的递归实现:
    • 时间复杂度O(2^n),因为每个调用几乎都会产生两个新的调用。
    • 空间复杂度O(n),因为递归的深度是 n
递归与并行计算
  • 并行递归:在多核处理器时代,可以利用并行计算加速递归算法。
  • 原理:将递归分解为可以并行执行的任务。
  • 示例:考虑以下使用 OpenMP 实现的并行斐波那契计算:
    #include <omp.h>
    unsigned long fibonacci_parallel(unsigned int n) {
        if (n <= 1) {
            return n;
        }
        #pragma omp parallel sections reduction(+:result)
        {
            #pragma omp section
            {
                unsigned long f1 = fibonacci_parallel(n - 1);
                #pragma omp critical
                {
                    result += f1;
                }
            }
            #pragma omp section
            {
                unsigned long f2 = fibonacci_parallel(n - 2);
                #pragma omp critical
                {
                    result += f2;
                }
            }
        }
        return result;
    }
    
总结

本文详细介绍了 C 语言中的递归技术,包括基本概念、原理、常见的递归算法、进阶话题以及调试技巧。递归是一种非常强大的工具,能够简化复杂问题的解决方案。然而,递归也存在潜在的风险,因此在实际应用中需要谨慎使用。

标签:实战,递归,int,rod,unsigned,factorial,深入,fibonacci
From: https://blog.csdn.net/suifengme/article/details/141690076

相关文章

  • micro-app【微前端实战】主应用 vue3 + vite 子应用 vue3+vite
    micro-app官方文档为https://micro-zoe.github.io/micro-app/docs.html#/zh-cn/framework/vite子应用无需任何修改,直接启动子应用即可。主应用1.安装微前端框架microAppnpmi@micro-zoe/micro-app--save2.导入并启用微前端框架microAppsrc/main.tsimp......
  • 玄机蓝队靶场_应急响应_71:实战evtx-文件分析
    windows日志排查工具:https://www.cnblogs.com/starrys/p/17129993.htmlwindows日志事件ID,参考文章:https://peterpan.blog.csdn.net/article/details/139887217下载日志分析工具FullEventLogView.exehttps://www.nirsoft.net/utils/fulleventlogview-x64.zip分别打开三个......
  • 鸿蒙Flutter实战:02-Windows环境搭建踩坑指南
    鸿蒙Flutter实战:02-Windows环境搭建踩坑指南环境搭建1.下载FlutterSDK,配置环境变量鸿蒙FlutterSDK需要在Gitee下载。目前建议下载dev分支代码。需要配置以下用户变量注意鸿蒙开发需要安装Java和配置相关变量#fluttersdk镜像FLUTTER_STORAGE_BASE_URL=https://s......
  • 【Linux线程】Linux多线程实践:深入生产者消费者模型
    ......
  • 深入理解 Bitmap 应用于缓存穿透与解决方案
    文章目录常见的解决方案方案一:ID校验(检查ID是否小于零)方案二:缓存空结果进阶方案:列表验证合法性使用**Bitmap**优化存储空间Java实现示例:优化提示:结合布隆过滤器减少误判方案总结缓存穿透问题表面上看似复杂,实际上它的本质非常简单:当请求数据库中不存在的数据......
  • LLM应用实战: OpenAI多代理新作-Swarm
    1.背景本qiang~关注到OpenAI两周前发布的轻量级多代理框架Swarm,因此想要深入了解了一下,运行了官方提供的例子,整理并总结一些心得体会~源码非常简单,各位看官们可以小读一下,本文采用gpt-4o-mini进行验证,如果想免费使用gpt-4o-mini,可私信沟通。Ps:发布之后,便在X引起了Swarm涉嫌......
  • 深入理解华为鸿蒙的 Context —— 应用上下文解析
    本文旨在深入探讨华为鸿蒙HarmonyOSNext系统(截止目前API12)的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。在华为鸿蒙(HarmonyOS)开发中,Context是......
  • 华为鸿蒙Next:应用启动框架AppStartup的解析与实战应用
    本文旨在深入探讨华为鸿蒙HarmonyOSNext系统(截止目前API12)的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。在华为鸿蒙(HarmonyOS)开发领域,应用的启......
  • 深入解析Apache DolphinScheduler容错机制
    简述ApacheDolphinschedulerMaster和Worker都是支持多节点部署,无中心化的设计。Master主要负责是流程DAG的切分,最终通过RPC将任务分发到Worker节点上以及Worker上任务状态的处理Worker主要负责是真正任务的执行,最后将任务状态汇报给Master,Master进行状态处理那问题来了:M......
  • 【付费】Ambari集成Dolphin实战-001-bigtop.bom的编写——下
    3.实战......