看这篇直接拿下递归

2 篇文章 0 订阅
2 篇文章 0 订阅
本文详细介绍了递归的概念、与循环迭代的比较、递归的优点、分类(直接递归与间接递归)、递归在数据结构中的应用以及尾递归优化。同时强调了掌握递归的必要性和使用注意事项,包括递归终止条件的设置和调试方法。
摘要由CSDN通过智能技术生成

递归从学C语言的时候就在看,时至今日仍然在看,这次干脆整理到位。

1.了解递归

1.1 定义:

递归算法应用的场景是要解决的问题和其子问题具有相似性的时候,通过直接或间接的调用自己求出问题解的方法。

它是通过解决一个问题的更小实例来解决一个大的问题的解的算法。

递归算法有两个过程,一是调用过程,二是向上传递结果的过程。

1.2 相对于循环迭代,递归的优点?

  1. 简洁性:递归可以用更少的代码表达复杂的逻辑,使代码更加简洁。

  2. 可读性:递归可以更直观地表达问题的逻辑,使得代码更易于理解和维护。

  3. 数学归纳法:递归是基于数学归纳法的,可以更自然地解决问题,特别是对于处理树形结构或者递归定义的问题。

  4. 分治思想:递归可以很自然地体现出分治思想,将问题分解为子问题,然后合并子问题的解。

  5. 解决一些问题更加直观:有些问题本身就天然适合递归解决,如阶乘、斐波那契数列等。

  6. 减少重复代码:递归可以避免写出重复的代码,提高了代码的重用性。

1.3 递归的分类

递归分为 直接递归 和 间接递归 :

直接递归:函数直接调用自身实现递归,也是最常见的形式。直接递归中有一种可以实现递归优化的,特殊的递归形式——尾递归,我会在举例中提到。

间接递归:两个及以上的函数之间相互调用,最终形成一个循环链。

//两个函数相互调用形成循环链的【间接递归】:
bool is_BBC(int n){
    if(n==1)
        return true;
    else
        return is_CCTV(N-1);
}
​
bool is_CCTv(int n){
    if(n==0)
        return false;
    else
        return is_BBC;
}

1.4 掌握递归的必要性

其实这个疑问在学习了数据结构这门课后,就不应该存在了。除了上面提到的优点外,其必要性其实主要体现在:

  • 在数据结构中,递归直接定义了包括二叉树、广义表在内的数据结构形式!从这点看,递归是学习数据结构及算法必须掌握的内容。

  • 函数定义,如斐波那契函数(计算斐波那契数列的第n项的函数)直接是按递归定义的。

  • 在某些算法问题中,用递归实现可以使代码更加简洁易懂,减少代码复杂度。

当然,以上只是客观为主的观点,递归必要性的体现远不止这三点'。

1.5 递归使用注意事项

递归的本质就是函数不断调用自身。递归函数每次调用自身时,系统都会为其新调用的函数分配内存,用于存储局部变量、调用地址和其它信息。而这个过程是与联系在一起的,即分配的内存区域称为“栈帧空间“,这个空间只有触发递归终止条件后才会随着函数返回而释放。前面说了,递归过程分为“递” 和 “归” 两个过程。所以就是:

递:入栈过程,程序执行”递“的动作时,就是各种数据进行 压栈 的过程。

归:出栈过程,触发递归终止条件后,内存空间随着函数的 return 而不断释放的过程,即 出栈。

因为递归过程是与 栈 打交道,所以自然也就涉及到堆栈溢出 的潜在风险,所以在写递归时要控制其深度。

2. 如何实现递归?

2.1 递归函数的构成:

递归函数包含两部分:

  1. 找出递归终止条件(边界条件):这是一个非递归的情况,它指明了递归(准确的来说应该是”递“何时停止)应该在何时停止。在递归函数中,通常会检查是否满足某些条件,如果满足,就不再进行递归调用,而是返回一个特定的值(开始”归“的过程)或执行其他操作。

  2. 递归情况:这是递归函数调用自身的部分。在这里,根据某些条件递归调用同样的函数,但是针对一个更小的问题规模(参数变化上实现)。

2.2 阶乘举例

以最简单的计算阶乘的函数为例:

2.2.1 构建分析

第一步:找到递归终止条件。计算的是n的阶乘,拆分是一个”由大到小“的过程,所以参数”递“到最小的时候,参数应该为”1“ (因为阶乘是 123...n,从1开始的),所以当参数”递“到 1 的时候,递归的”递”就该停止了。所以当参数n为1时,开始返回(出栈,开始“归”的过程),此时阶乘的运算才是正式展开。

第二步:只看目前的参数和下一个要“递”下去的参数之间的关系,我们求的是阶乘,所以它们俩之间是相乘的关系 所以关系式是:

n * factorial(n-1)

全过程如下图所示:

f52430f8da3f4bfe9487f9966c51e028.png

2.2.2 代码实现

C语言代码如下:

int factorial(int n){
    //递归终止条件
    if(n == 1)
        return 1;
    # 递归函数
    else
        return n*factorial(n-1);
}

2.3 尾递归优化

2.3.1 什么是尾递归?

对于尾递归的阐述也挺多的,但我个人总结了一下,就是 省略了”归“过程的递归

2.3.2 如何实现尾递归?

就是在递归的过程中(也就是递归函数多增加一个参数)增加一个变量res,用于存储并处理每一次递归调用(”递“)前的参数。简单说就是,在”递“的同时完成了”归“的操作过程。

对应求阶乘代码如下:

int tailFactorial(int n,int res){
    //终止条件不变
    if(n==1)
        return res; //但返回时不再是返回”上一次递归“,
    else            //而是将返回结果直接一次性返回。
        return tailFactorial( (n-1) , (res*n) );
    //形如:【return 尾递归函数(<一般递归中的递归参数>,<在递归调用(递)前,操作并存储当前参数>)】
    //对于求阶乘这个问题而言,“操作”就是相邻的递归参数之间相乘,所以res*n记录了一路”递“过来的这些数的 乘积
    //所以”递“完后直接返回的 res 就是这些递归参数的乘积,而不用在返回到上一级递归,运算,再上一级,运算......这种重复.
    //所以说是省略了”归“的过程。
}

01f42f917f644302bd0dfb1285905782.png

2.3.3 尾递归注意事项:

虽然尾递归的确实现了递归优化,但实际使用时因编译器以及语言自身的情况,还是存在不支持尾递归优化的情况,如Python,所以这种情况下即便函数是尾递归的形式,但依然会存在栈溢出的问题。

2.4

2.4 日常练习递归小技巧:

递归这块想要熟练掌握的最好方式是在理解其基础知识后,再进行各种各样的手搓练习,相信假以时日我们都会有明显的进步!一下是日常练习的一些小技巧,希望能帮到你。

  1. 确保基本情况的正确性:基本情况的定义对于递归函数的正确性至关重要,因此你需要确保它是正确的。

  2. 找到递归关系:找到函数调用自身的规律,并且通过参数的变化来确保问题规模在递归调用中不断减小,最终满足基本情况。

  3. 确保递归终止:递归函数应该能够在某个时刻终止,避免出现无限循环最终栈溢出。

  4. 调试和测试:尽可能地通过调试和测试来验证递归函数的正确性,确保它的行为符合预期。

此外,也别忘了递归函数通常对于解决树形结构、分治算法以及动态规划等问题非常有效,因此在这些领域中更容易找到适合递归的算法。

 

  • 30
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值