递归法(当代程序员必备技能(算法)之:递归详
前言
递归算法,如同一道贯穿编程世界的智慧之桥,无论你是前端还是后端开发者,掌握它都是必备的技能。从统计文件夹大小到复杂的XML文件,递归的应用无处不在。它的基础性和重要性,使得在面试时,往往成为考察的重点。将带领大家深入递归的世界,一起领略它的魅力。
一、什么是递归?
递归,简单来说,就是函数通过自身调用自身来解决问题的方法。我们可以将其比喻为查词典的过程。当我们遇到一个不懂的词,我们会去查词典,而在词典中,为了解释一个词,可能会使用到其他词,于是我们再查那个词,如此反复,直到查到一个我们完全理解的词为止。这个过程就是递归的过程。
二、递归的特点
递归有两个显著的特点:自身调用和终止条件。
自身调用意味着原问题可以分解为子问题,子问题和原问题的求解方法是一致的。而终止条件则是递归的出口,没有终止条件的递归会导致无限循环。以计算阶乘的递归函数为例,当n=1时,返回1;否则,返回n与sum(n-1)的乘积。这就是终止条件和自身调用的体现。
三、递归与栈的关系
实际上,递归的过程可以看作是栈的出入过程。每次函数调用,都会形成一个栈帧,保存当前的上下文信息。递归的过程就是不断地压栈和弹栈的过程。以计算sum(n)的递归函数为例,每个sum(i)都会形成一个栈帧,保存i的值,然后调用sum(i-1),形成新的栈帧。直到达到终止条件,开始弹栈,返回结果。
四、递归应用场景
递归在编程中的应用场景非常广泛。例如阶乘问题、二叉树、汉诺塔问题、斐波那契数列、快速排序、归并排序等都可以使用递归来解决。在遍历文件、XML文件等场景中,也会用到递归算法。
五、递归解题思路
解决递归问题一般分为三个步骤:定义函数功能、寻找递归终止条件和递推函数的等价关系式。以阶乘问题为例,首先定义函数功能是计算n的阶乘;然后寻找终止条件,当n=1时,返回1;最后递推函数的等价关系式,即n的阶乘等于n乘以(n-1)的阶乘。这样,一个阶乘的递归函数就定义完成了。
六、案例分析:阶乘递归的实现
下面是一个阶乘递归的示例代码:
```java
public int factorial(int n) {
if (n == 1) { // 终止条件
return 1;
阶乘问题的递归
阶乘计算,看似简单,其背后却隐藏着递归的奥妙。当我们求解n的阶乘时,可以将其拆分为求解(n-1)的阶乘,再乘以n。这种拆分方式,正是递归的核心所在。
假设我们有一个函数`factorial(n)`,用于计算n的阶乘。在递归的语境下,我们可以这样描述:
1. 定义函数功能:计算n的阶乘。
2. 寻找递归终止条件:当n等于1时,返回1。因为1的阶乘是1,这是递归的终点。
3. 递推函数的等价关系式:f(n) = n f(n-1)。这就是阶乘的递推关系,表示n的阶乘等于n乘以(n-1)的阶乘。
用递归的方式表示,代码可以写成这样:
```java
public int factorial(int n) {
if (n == 1) { // 终止条件
return 1;
} else {
return n factorial(n - 1); // 递推关系式
}
}
```
这种递归方式简洁明了,体现了递归在解决数学问题中的优势。但需要注意的是,递归虽然简洁,但在实际编程中可能会带来性能问题,因为递归涉及到多次函数调用和堆栈操作。在实际应用中需要根据具体情况权衡使用递归还是迭代的方式。
二叉树翻转问题的递归
翻转二叉树是一个经典的递归问题。首先明确题目要求:输入一棵二叉树,输出这棵二叉树翻转后的结果。这里以题目给出的示例来解释递归的过程。原题链接为 [题目链接](
解决这个问题的思路依然是按照递归的三步骤进行:定义函数功能、寻找递归终止条件、确定递推函数的等价关系式。接下来是具体的:
首先定义函数功能:翻转一棵二叉树。接着找到终止条件:当节点为空或已经是叶子节点时,无需翻转,直接返回该节点即可。最后确定递推关系式:翻转一棵二叉树等同于翻转它的左子树和右子树。我们可以写出如下的代码框架:
```java
public TreeNode invertTree(TreeNode root) {
if (root == null || (root.left == null && root.right == null)) { //终止条件检查节点是否为空或叶子节点
return root; // 直接返回当前节点无需翻转操作的情况
} else { // 执行翻转操作的情况处理左子树和右子树的翻转逻辑}
TreeNode temp = root.left; // 保存左子树的引用以便于后续交换位置使用临时变量交换左右子树的位置root.left = invertTree(root.right); // 对右子树进行翻转root.right = invertTree(temp); // 对左子树进行翻转并交换位置后赋值给当前节点的左子树return root; // 返回翻转后的根节点完成一次递归调用并更新根节点的子节点状态以进行下一轮的递归调用}最终得到的是整个二叉树的翻转结果这是一个经典的递归应用实例展示了如何通过递归解决复杂问题在理解了这个问题后对于其他类似的递归问题也能有更深入的理解和理解起来就相对容易了希望这个解答对你有所帮助!递归法解决青蛙跳阶问题也是非常好的一个入手题目,当然也可以使用循环进行求解,不过对于递归法来说,由于存在重复计算的问题,导致效率低下。对于该问题来说,推荐使用循环法求解。我们还是先来介绍一下使用递归法如何解决青蛙跳阶问题。对于递归的思路是这样的:
1、对于一级台阶只有一种跳法:青蛙直接跳上一级台阶。
2、对于二级台阶有两种跳法:青蛙直接跳上两级台阶或者先跳上一级台阶再跳上一级台阶。
对于n级台阶的跳法数量等于跳上一级台阶的方式数量与跳上两级台阶的方式数量之和。
基于上述思路,我们可以写出如下的递归代码:
public class FrogJump {
public static int frogJump(int n) {
if (n == 1) { // 基础情况
return 1;
} else if (n == 2) { // 第二级台阶需要单独判断一下
return 2;
} else { // 计算n级台阶的跳法数量等于跳上一级台阶的方式数量与跳上两级台阶的方式数量之和
return frogJump(n - 1) + frogJump(n - 2);
}
}
}
虽然递归代码简单明了,但是其时间复杂度是O(2^n),随着n的增大,重复计算的数量会变得非常多,导致效率低下。
为了解决这个问题,我们可以使用循环或者动态规划的方式来解决该问题。这两种方式都需要将已经计算过的结果保存下来,避免重复计算。
以动态规划为例,我们可以将计算过的结果保存在一个数组中,如果再次计算同样的结果时直接从数组中取值即可。
代码示例:
public class FrogJumpDP {
public static int frogJump(int n) {
int[] dp = new int[n + 1]; // dp数组用于保存结果值
dp[0] = 0; // 对于没有台阶的情况返回结果为0
dp[1] = 1; // 对于一级台阶的情况返回结果为1种跳法
dp[2] = 2; // 对于二级台阶的情况返回结果为两种跳法
for (int i = 3; i <= n; i++) { // 从***台阶开始计算每级台阶的跳法数量
dp[i] = dp[i - 1] + dp[i - 2]; // 当前级台阶的跳法数量等于上一级和上上级的和
}
对于这个问题,我们先来看看递归树是如何构建的。要计算原问题 f(10),就需要先计算出子问题 f(9) 和 f(8)。而要计算 f(9),又要先算出子问题 f(8) 和 f(7),以此类推,一直到 f(2) 和 f(1),递归树才终止。我们可以观察到,随着问题的规模增大,递归树的节点数量会迅速增长。这就是递归解法的低效之处。那么,如何解决这个问题呢?我们可以使用带备忘录的递归解法。备忘录的作用是存储已经计算过的结果,避免重复计算。这样,递归树中的重复计算部分就可以被避免掉,从而大大提高算法的效率。接下来我们来详细看看带备忘录的递归解法是如何实现的。
假设我们使用一个数组或者哈希表来作为备忘录。当我们计算 f(n) 时,首先检查备忘录中是否已经存在 f(n) 的计算结果。如果存在,就直接从备忘录中取出结果;如果不存在,就按照递归公式计算 f(n),并将结果存入备忘录中。这样,当再次计算 f(n) 时,就可以直接取出备忘录中的结果,避免了重复计算。这个过程可以形象地表示为在递归树上剪掉重复的节点。通过这种方法,原本爆炸增长的指数级别时间复杂度就被降低到线性级别 O(n)。
带备忘录的递归算法是一种高效的解决递归问题的方法。它通过避免重复计算来降低时间复杂度,使得原本指数级别的算法能够在多项式时间内解决问题。这种方法的优点是简单易实现,而且可以有效地解决具有大量重复计算的问题。这种方法也有一些缺点,比如需要额外的空间来存储备忘录。在解决大规模问题时,这种空间开销是值得的。希望这篇文章能够帮助大家理解带备忘录的递归算法的原理和实现方法。接下来,我们来一起深入并使用带“备忘录”的递归算法,以处理青蛙跳阶问题的超时问题。让我们开始编写代码之旅!
在解决此类问题时,我们可以使用一种被称为动态规划的技术,结合哈希表(在这里我们称之为“备忘录”)来存储已经计算过的结果,避免重复计算,从而提高效率。下面是实现的代码:
```java
public class Solution {
// 使用哈希表作为备忘录
private Map
public int numWays(int n) {
// 如果n为0,只有一种方式
if (n == 0) {
return 1;
}
// 如果已经计算过当前n的结果,直接返回
if (memo.containsKey(n)) {
return memo.get(n);
}
// 对于其他情况,进行递归计算并保存结果到备忘录中
int result = (numWays(n - 1) + numWays(n - 2)) % ; // 对结果进行取余操作,避免溢出
memo.put(n, result); // 保存结果到备忘录中
return result; // 返回结果
}
}
```
这段代码的核心在于使用备忘录(哈希表)来存储已经计算过的结果。当我们再次需要计算相同的结果时,可以直接从备忘录中取出,避免了重复计算。通过这种方式,我们可以大大提高算法的效率,解决超时问题。我们在递归计算的过程中对结果进行了取余操作,以防止结果溢出。我们可以将这段代码提交到leetcode平台上进行测试。
提交后的结果应该会让你感到满意。通过使用带备忘录的递归算法,我们能够高效地解决青蛙跳阶问题,避免了超时的情况。这种使用动态规划和备忘录的方法在许多其他类似的问题中也非常有用。希望这次的学习经历能够帮助你更好地理解和掌握这种算法思想。加油!
f1赛车
- 递归法(当代程序员必备技能(算法)之:递归详
- 如何免费观看nba球赛
- 1994年今年多大什么命(82岁爷爷每天沉浸工作9小时
- 1994年中国男篮队员名单(飞人胡卫东,关键先生郑
- 二年级下册语文找春天课文_二年级下册语文全课
- 西班牙对瑞典女足比赛
- 篮球滚球让球
- 98世界杯atvtvb(细数90年代12部亚视经典电视剧,我
- 中国什么时候再办一次奥运会(1993年我国首次申奥
- 福州羽毛球私教(陈宏近况:与高崚分手娶娇妻,
- 仙剑三电视剧结局_仙剑奇侠传三第38集真正的大
- 我会认真考虑好再做决定(军旅歌手耿为华:再婚
- 广州到岳阳的火车_广州东站至岳阳火车站时刻表
- 对电竞的看法和理解(取经中国、投资教练——北
- 广州好玩的地方有哪些_三亚好玩的地方有哪些地
- 印第安纳步行者球衣