算法实现方式--递归和迭代(不断更新)

本文深入探讨了递归和迭代的概念,强调递归在解决问题时的分治思想,但指出其可能导致的空间浪费和栈溢出问题。递归的特点包括函数自我调用和明确的递归出口。递归优化主要涉及尾递归,它可以减少堆栈使用,提高效率。文章还介绍了非递归转换,以避免递归带来的问题。总结了将递归转换为尾递归或非递归方法的策略,并讨论了递归在空间和时间复杂度上的考量。
摘要由CSDN通过智能技术生成

两者比较

两者之间的关系:

1) 能用迭代的不用递归,递归调用函数,计算有重复,浪费空间,并且递归太深容易造成堆栈的溢出.

2)递归其实是专门用来实现分治思想的一种手段,说它是一种专用技能也不为过

3)递归和迭代都是循环中的一种。简单地说,递归是重复调用函数自身实现循环。迭代是函数内某段代码实现循环,而迭代与普通循环的区别是:循环代码中参与运算的变量同时是保存结果的变量,当前保存的结果作为下一次循环计算的初始值

4)优劣对比:

  • 迭代是逐渐逼近,用新值覆盖旧值,直到满足条件后结束,不保存中间值,空间利用率高。
  • 递归是将一个问题分解为若干相对小一点的问题,遇到递归出口再原路返回,因此必须保存相关的中间值,这些中间值压入栈保存,同时还要较多push pop操作,问题规模较大时会占用大量内存
  • 上面是从空间利用率角度说的
  • 下面从代码简洁性和可读性来说:
  • 递归代码相比较来说,更加容易阅读,简单明了

递归详解

详解

套路

我们在上一节仔细剖析了什么是递归,可以发现递归有以下两个特点

  1. 一个问题可以分解成具有相同解决思路的子问题,子子问题,换句话说这些问题都能调用同一个函数

  2. 经过层层分解的子问题最后一定是有一个不能再分解的固定值的(即终止条件),如果没有的话,就无穷无尽地分解子问题了,问题显然是无解的。
    所以解递归题的关键在于我们首先需要根据以上递归的两个特点判断题目是否可以用递归来解。
    经过判断可以用递归后,接下来我们就来看看用递归解题的基本套路(四步曲):

  3. 先定义一个函数,明确这个函数的功能,由于递归的特点是问题和子问题都会调用函数自身,所以这个函数的功能一旦确定了, 之后只要找寻问题与子问题的递归关系即可

  4. 接下来寻找问题与子问题间的关系(即递推公式),这样由于问题与子问题具有相同解决思路,只要子问题调用步骤 1 定义好的函数,问题即可解决。所谓的关系最好能用一个公式表示出来,比如 f(n) = n * f(n-) 这样,如果暂时无法得出明确的公式,用伪代码表示也是可以的, 发现递推关系后,要寻找最终不可再分解的子问题的解,即(临界条件),确保子问题不会无限分解下去。由于第一步我们已经定义了这个函数的功能,所以当问题拆分成子问题时,子问题可以调用步骤 1 定义的函数,符合递归的条件(函数里调用自身)

  5. 将第二步的递推公式用代码表示出来补充到步骤 1 定义的函数中

  6. 最后也是很关键的一步,根据问题与子问题的关系,推导出时间复杂度,如果发现递归时间复杂度不可接受,则需转换思路对其进行改造,看下是否有更靠谱的解法

【特点】:

1)递归就是在过程或函数里面调用自身;
2)在使用递归时,必须有一个明确的递归结束条件,称为递归出口.

递归优化

  1. 递归–>尾递归(Continuation Style Program)

    1. 先说一下什么是尾调用

      1. 函数A里面调用了函数B。
      2. 函数B执行后,函数A马上返回。
      3. 也就是说调用函数B(并返回执行结果)是函数A所做的最后一件事。
      4. 相当于执行完函数B后,函数A也就执行完。

      因此在执行函数B时,函数A的栈帧其实是已经大部分没用了,可以被修改或覆盖。编译器可以利用这一点进行优化,函数B执行后直接返回到函数A的调用者。

      这里有一点需要注意:它是来自于编译器的优化。 这一点点的优化对于普通的尾调用来说可能意义不大,但是对于尾递归来说就很重要了。

    2. 若一个函数在尾位置调用本身(或是一个尾调用本身的其他函数等),则称这种情况为尾递归,是递归的一种特殊情形。而形式上只要是最后一个return语句返回的是一个完整函数,它就是尾递归。

    3. 尾递归实例

      int fac(int n) {
          if (n == 1) {
              return 1;
          }
          return fac(n-1) * n;
      }
      
      //  上面的这种递归计算最终的return操作是乘法操作。所以不是尾递归。  
      //  尾递归 最后就是自身一个函数,不要在加其他操作
      
      int tailfac(int n,int sum) {
          if (n == 1) {
              return sum;
          }
          return fac(n-1, n * sum);
       //上面这个就是尾递归  不用看这俩函数具体意义
      
    4. 尾递归为何会效率高

      1. 尾递归在堆栈层面基本可以不存任何东西啊,他就是返回下一层递归函数的结果吗,又不需要这一层的任何东西。所以,对于递归函数常常面对的堆栈溢出问题,就基本解决了很多
  2. 下面所总结的转为非递归

递归注意点

  1. 类似于二分查找递归思路,参数可以传入一个起始和一个终止值,这样每次用的也依旧是那一个旧的列表,不用每次递归新生成一个,节省空间
  2. 上一层递归中的变量是不可以被下一层递归使用的,即使名字相同,也会是两个相互独立的变量

递归转非递归

  1. 递归两种优化方式,第一种是上面所总结的转为尾递归;第二种就是转为非递归的形式;

  2. 第一种方法:

    1. 因为递归本身就是通过堆栈实现的,我们只要把递归函数调用的局部变量和相应的状态放入到一个栈结构中,在函数调用和返回时做好push和pop操作,就可以了
    2. 其实递归转非递归,主要考虑两个点:一是如何把该层次的信息传递到下一层;二是,如果需要返回到上一层,如何把下一层的信息接回到上一层
    3. 这种基本就是通用的方法
  3. 第二种方法

    1. 借助堆栈的循环结构算法。这种方法常常适用于某些局部变量有依赖关系,且需要重复执行的场景,例如二叉树的遍历算法,就采用的这种方法。
    2. 这种暂时还没有搞懂,后面可以继续看看
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值