一文学会递归题

于我而言,递归实在是一种非常美的算法,用它写出来的代码既简介又易懂,而且最重要的是,可结解释度又非常高。它的精妙之处就在于,你可以“自顶向下”地分析问题,而不必纠结于细枝末节。所以递归是我比较喜欢的一种算法。

递归解题精髓在于以下几点:

1.定义一个函数,并明确它代表的含义,也就是说要明确这个函数的功能是什么,由于
递归的特点是问题和子问题都会调用函数自身,所以这个函数的功能一旦确定了, 
之后只要找寻问题与子问题的递归关系即可。

2.接下来寻找问题与子问题间的关系(即递推公式),这样由于问题与子问题具有相同
解决思路,只要子问题调用步骤 1 定义好的函数,问题即可解决。“自顶向下”的分析问题,
先分析立成功最近的那一步该怎么做,假设之前的条件都已经满足,我这一步应该怎样
做才能达到最后的结果。

3.将第二步得到的递推公式用代码表示出来,并将其补充到步骤1的函数中。

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

对此我举几个经典的例子:

1.青蛙跳台阶问题:

一只青蛙可以一次跳 1 级台阶或一次跳 2 级台阶,例如:跳上第 1 级台阶只有一种跳法:
直接跳 1 级即可。跳上第 2 级台阶有两种跳法:每次跳 1 级,跳两次;或者一次跳 2 级。
问要跳上第 n 级台阶有多少种跳法?

我们继续来按四步曲来看怎么套路

1.定义一个函数,这个函数代表了跳上 n 级台阶的跳法

/**
 * 跳 n 极台阶的跳法
 */
public int f(int n) {
}

2.寻找问题与子问题之前的关系 这两者之前的关系初看确实看不出什么头绪,但仔细看题目,一只青蛙只能跳一步或两步台阶,自上而下地思考,也就是说如果要跳到 n 级台阶只能从 从 n-1 或 n-2 级跳, 所以问题就转化为跳上 n-1 和 n-2 级台阶的跳法了,如果 f(n) 代表跳到 n 级台阶的跳法,那么从以上分析可得 f(n) = f(n-1) + f(n-2),显然这就是我们要找的问题与子问题的关系,而显然当 n = 1, n = 2, 即跳一二级台阶是问题的最终解。

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

/**
 * 跳 n 极台阶的跳法
 */
public int f(int n) {
    if (n == 1) return1;
    if (n == 2) return2;
    return f(n-1) + f(n-2)
}

4.计算时间复杂度 由以上的分析可知 f(n) 满足以下公式
在这里插入图片描述
可以看到有大量的重复计算, f(3) 计算了 3 次, 随着 n 的增大,f(n) 的时间复杂度自然呈指数上升了

5.优化
在之前的动态规划文章里已经讲过利用备忘录进行优化的方法
既然有这么多的重复计算,我们可以想到把这些中间计算过的结果保存起来,如果之后的计算中碰到同样需要计算的中间态,直接在这个保存的结果里查询即可,这就是典型的 以空间换时间。

6.继续观察我们发现可以进一步降低空间复杂度:

public int f(int n) {
    if (n == 1) return1;
    if (n == 2) return2;

    int result = 0;
    int pre = 1;
    int next = 2;
    
    for (int i = 3; i < n + 1; i ++) {
        result = pre + next;
        pre = next;
        next = result;
        //这里pre=next必须在next=result之前,因为要保证更新的是之前的变量,
        //若先把next=result,那么之后赋给pre的值就是本次result的值(也就是next更新后的值),而不是next更新前的值。
    }
    return result;
}

2.接下来我们来看下一道经典的题目: 反转二叉树 将左边的二叉树反转成右边的二叉树

1.大一部,定义函数,这个函数代表了翻转以 root 为根节点的二叉树

publicstaticclass TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x) { val = x; }
}

public TreeNode invertTree(TreeNode root) {
}


2.查找问题与子问题的关系,得出递推公式 我们之前说了,解题要采用自上而下的思考方式,那我们取前面的1, 2,3 结点来看,对于根节点 1 来说,假设 2, 3 结点下的节点都已经翻转,那么只要翻转 2, 3 节点即满足需求.
不过在找问题的过程中 切忌把子问题层层展开,到汉诺塔这个问题上切忌再分析 n-3,n-4 怎么移,这样会把你绕晕,只要找到一层问题与子问题的关系得出可以用递归表示即可。

所以明确函数的功能非常重要,按着函数的功能来解释,递归问题其实很好解析,切忌在每一个子问题上层层展开死抠,这样这就陷入了递归的陷阱,计算机都会栈溢出,何况人脑。
在这里插入图片描述
对于2, 3 结点来说,也是翻转其左右节点即可,依此类推,对每一个根节点,依次翻转其左右节点,所以我们可知问题与子问题的关系是 翻转(根节点) = 翻转(根节点的左节点) + 翻转(根节点的右节点) 即

invert(root) = invert(root->left) + invert(root->right)

而显然递归的终止条件是当结点为叶子结点时终止(因为叶子节点没有左右结点)

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

public TreeNode invertTree(TreeNode root) {
    // 叶子结果不能翻转
    if (root == null) {
        returnnull;
    }
    **// 翻转左节点下的左右节点,这里可能会有些疑问,我来解释一下
    //因为我们定义的函数功能是“翻转以root为根节点的二叉树”
    //而之前又提到,一个根节点只有在以左其右孩子为根节点的树都完成翻转后
    //才能执行之后的操作,所以在执行最后一步操作时,要先达到必须的条件,
    //所以要先翻转其左右孩子**
    TreeNode left = invertTree(root.left);
    // 翻转右节点下的左右节点
    TreeNode right = invertTree(root.right);
    // **左右节点下的二叉树翻转好后,翻转根节点的左右节点**
    root.right = left;
    root.left = right;
    return root;
}

4.时间复杂度分析 由于我们会对每一个节点都去做翻转,所以时间复杂度是 O(n),那么空间复杂度呢,这道题的空间复杂度非常有意思,我们一起来看下,由于每次调用 invertTree 函数都相当于一次压栈操作, 那最多压了几次栈呢, 仔细看上面函数的下一段代码
从根节点出发不断对左结果调用翻转函数, 直到叶子节点,每调用一次都会压栈,左节点调用完后,出栈,再对右节点压栈…,下图可知栈的大小为3, 即树的高度,如果是完全二叉树 ,则树的高度为logn, 即空间复杂度为O(logn)。

 细胞分裂 有一个细胞 每一个小时分裂一次,一次分裂一个子细胞,第三个小时后会死亡。那么n个小时候有多少细胞?

1.定义问题的递归函数,明确函数的功能 我们定义以下函数为 n 个小时后的细胞数

2.接下来寻找问题与子问题间的关系(即递推公式) 首先我们看一下一个细胞出生到死亡后经历的所有细胞分裂过程

在这里插入图片描述
图中的 A 代表细胞的初始态, B代表幼年态(细胞分裂一次), C 代表成熟态(细胞分裂两次),C 再经历一小时后细胞死亡 以 f(n) 代表第 n 小时的细胞分解数 fa(n) 代表第 n 小时处于初始态的细胞数, fb(n) 代表第 n 小时处于幼年态的细胞数 fc(n) 代表第 n 小时处于成熟态的细胞数 则显然 f(n) = fa(n) + fb(n) + fc(n) 那么 fa(n) 等于多少呢,以n = 4 (即一个细胞经历完整的生命周期)为例

仔细看上面的图

可以看出 fa(n) = fa(n-1) + fb(n-1) + fc(n-1), 当 n = 1 时,显然 fa(1) = 1

fb(n) 呢,看下图可知 fb(n) = fa(n-1)。当 n = 1 时 fb(n) = 0

fc(n) 呢,看下图可知 fc(n) = fb(n-1)。当 n = 1,2 时 fc(n) = 0
总结
大部分递归题其实还是有迹可寻的, 按照之前总结的解递归的四个步骤可以比较顺利的解开递归题,一些比较复杂的递归题我们需要勤动手,画画图,观察规律,这样能帮助我们快速发现规律,得出递归公式,一旦知道了递归公式,将其转成递归代码就容易多了,很多大厂的递归考题并不能简单地看出递归规律,往往会在递归的基础上多加一些变形,不过万遍不离其宗,我们多采用自顶向下的分析思维,多练习,相信递归不是什么难事。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值