普通递归与尾递归

目录

1.递归的定义

2.调用栈

3.尾递归

4.对比


1.递归的定义

        递归(recursion)是一种常用于编程和算法中的方法,它允许函数调用自身来解决问题。递归主要涉及两个阶段:「递」和「归」。

  1. :这个阶段涉及函数不断地调用自身,每次都传入更小或更简化的参数。这个过程一直持续,直到达到某个特定的条件,称为“终止条件”。终止条件是递归的关键,因为它定义了递归何时结束。

  2. :一旦触发了终止条件,递归过程开始逆转。此时,程序开始从最深层的递归调用回溯,逐层返回,同时汇聚并处理每一层的结果。

在实现递归的过程中,通常包含以下三个核心要素:

  • 终止条件:这是递归过程中至关重要的部分,用于决定何时停止递归过程,避免无限循环。

  • 递归调用:这是实现「递」阶段的关键,其中函数调用自身,通常会将问题分解为更小或更简单的子问题。

  • 返回结果:在「归」阶段,每一层的递归函数将其结果返回给上一层,最终形成整个问题的解决方案。

        递归是一种强大且优雅的解决问题的方法,但也需要小心处理,以避免诸如栈溢出等问题。正确设置终止条件和管理递归深度是编写有效递归代码的关键。

2.调用栈

        调用栈(Call Stack):在递归中,每当递归函数调用自身时,计算机系统会在一个称为“调用栈”的内存区域分配新的空间。这个空间用于存储函数的局部变量、返回地址、和其他必要的信息。这种机制有两个主要的影响:

  • 内存消耗:每个函数的调用都需要在调用栈上创建一个新的“栈帧”(stack frame),这是一块用来存储函数上下文(即局部变量、调用地址等)的内存空间。由于在递归过程中,每一次函数调用都会创建一个新的栈帧,直到达到终止条件,因此递归通常比迭代(循环)占用更多的内存空间。当递归深度很大时,可能会导致栈空间耗尽,引发“栈溢出”错误。

  • 时间效率:每次递归调用都涉及在调用栈上创建和管理新的栈帧,这需要时间和处理资源。因此,相比于简单的循环(迭代),递归调用通常会产生更多的开销,从而导致较低的时间效率。尤其在递归函数进行大量重复计算时,这种效率低下会更加明显。

示例,用普通递归演示前n个数之和。

 public static int recur(int n){
        if (n == 1){
            return 1;
        }
        int res = recur(n - 1);
        return n + res;
    }

图片来源于hello-algo.com

3.尾递归

        尾递归(tail recursion)是一种特殊类型的递归,其中递归调用是函数中的最后一个操作。它提供了一种方式,使得递归在空间效率上可以与迭代相当。这种优化通常由编译器或解释器来实现。

 尾递归演示上述例子。

 public static int tailRecur(int n, int res){
        if (n == 0){
            return res;
        }

        return tailRecur(n - 1,res + n);
    }

4.对比

普通递归尾递归 的区别主要体现在对函数调用栈的处理上:

  1. 普通递归:在这种情况下,函数在每次递归调用后还有其他操作或代码需要执行。因此,每次递归调用时,系统都必须在调用栈上保存当前函数的上下文(包括局部变量、返回地址等),以便在递归调用返回后继续执行剩余的操作。这导致调用栈上有多个栈帧,每个栈帧对应一个递归层级。

  2. 尾递归:在尾递归中,递归调用是函数中的最后一个操作,没有更多的代码需要在递归调用后执行。这意味着一旦递归调用返回,函数也随即结束。因为没有必要继续执行更多的操作,所以系统无需保存当前函数调用的上下文。这样,编译器或解释器可以优化递归过程,使得每次递归调用不再需要创建新的栈帧,而是复用当前的栈帧。

        尾递归优化的关键在于,由于递归调用是函数的最后操作,当前函数的上下文在递归调用之后不再需要,因此可以被下一次函数调用重用。这种优化大幅减少了内存的使用,尤其是在深度递归的情况下,从而使尾递归函数的空间效率与循环相当,甚至可以避免栈溢出的问题。然而,值得注意的是,并不是所有的编程语言或编译器都支持尾递归优化。

5.总结

        递归是一种强大的编程技巧,适用于许多场景,尤其在问题本身就具有递归性质时。以下是递归常见的使用场景:

  1. 树和图的遍历:树(如二叉树)和图(如家族树、网络图)的数据结构天然具有递归特性。递归能够简洁地实现对这些结构的遍历和操作,如二叉树的前序、中序、后序遍历,图的深度优先搜索(DFS)等。

  2. 分治算法:分治是一种算法设计策略,它将问题分解成较小的、相似的子问题,独立解决这些子问题,然后合并结果。许多分治算法(如快速排序、归并排序)自然适合递归实现。

  3. 动态规划的备忘录方法:动态规划通常用于解决优化问题。递归结合备忘录(memoization)技术,可以用来避免重复计算,优化动态规划问题的解决方案。

  4. 生成排列和组合:在处理排列、组合等组合数学问题时,递归提供了一种简单直观的方法来生成所有可能的情况。

  5. 解析嵌套结构:解析具有嵌套结构的数据(如JSON、XML)时,递归能够简化解析过程,尤其是在结构深度可变时。

  6. 实现回溯算法:在求解约束满足问题(如八皇后问题)、组合优化问题(如旅行商问题)时,递归结合回溯策略非常有效。

  7. 数学问题:递归经常用于解决数学问题,如计算阶乘、斐波那契数列、汉诺塔问题等。

        在选择使用递归时,需要考虑其对内存和性能的影响。对于深度很大的递归调用,可能会导致栈溢出问题。此外,对于有大量重复计算的问题,应考虑使用迭代或递归配合备忘录技术,以提高效率。递归的优雅与简洁往往以牺牲空间和时间效率为代价,因此在实际应用中,应根据具体问题和环境来权衡其使用。

  • 12
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值