【算法】递归

一、算法理解

严格来说,递归不是一种算法,而是一种解题思路。
递归的核心思想:将大问题分解为小问题来求解,然后再将小问题分解为更小的问题。这样一层一层地分解,直到问题规模被分解得足够小,不用继续分解,可以直接计算结果为止。

二、适用场景

(1)求f(n)
(2)可以做出f(n)与f(n-1)、f(n-2)...的规律

三、使用注意事项

使用递归,需要分析出:

  1. 【求解目标】:f(n)
  2. 【递归公式】:∑f(n)到∑f(n-1)/∑f(n-2)..的规律:对原始问题建模,找到大问题 分解成 小问题 的规律,即递归公式。
  3. 【终止条件】:即把问题分解大足够小,不用继续分解的条件。

【注1】:f(n)与∑f(n)
一些用到递归的题目中,可能f(n)即∑f(n)。
但是,一些题目中,经常不是直接的f(n)=f(n-1)...f(n-2)...关系。而是:∑f(n)=∑f(n-1)/∑f(n-2)... ; f(n)=...∑f(n) 的关系。
详见如下“细胞分裂”样例。

【注2】:递归中缓存的使用
递归中∑f(n)=∑f(n-1)/∑f(n-2)...,扩展后:
∑f(n)=∑f(n-1)/∑f(n-2)...
∑f(n-1)=∑f(n-2)/∑f(n-3)...
...
不同深度的递归,∑f(n-2)、∑f(n-3)....会重复计算。为了提高效率,可以考虑缓存:
(1)计算∑f(x)时,把∑f(x)结果缓存起来。
(2)后续在调用到∑f(x),直接从结果返回

如:如下递归关系f(n) = f(n-1) + f(n-2)

f(6) =         f(5)         +        f(4)
​             /    \                 /   \
​         f(4)     f(3)            f(3)    f(2)
​        /  \      /   \           /   \
​      f(3) f(2) f(2)  f(1)     f(2)  f(1)

四、场景应用思路

1. 爬台阶/青蛙跳

爬台阶:有n级台阶,每次可以爬 1个台阶 或 2 个台阶。求:有多少种不同的方法可以爬到楼顶呢?
青蛙跳:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求:该青蛙跳上一个 n 级的台阶总共有多少种跳法。

【思路】:

由于每次只能爬1阶、或2级阶梯。所以跳上第n个台阶有两种选择:
(1)跳上第n-1台阶后,再跳1个台阶;
(2)跳上第n-2台阶后,再跳2个台阶。
So,
迭代公式:f(n) = f(n-1) + f(n-2)
终止条件:f(1) = 1; f(2) = 2;

2. 斐波那契数列

1,1,2,3,5,8,13,21,34,55,....这个数列从第3项开始,每一项都等于前两项之和。

递归公式:f(n) = f(n-1) + f(n-2)
终止条件:f(1) = 1; f(2) = 1;

3. 细胞分裂

有一个细胞,每一个小时分裂一次;一次分裂从1个变成2个;第三个小时后会死亡;那么n个小时之后有多少细胞?

【思路】
对问题进行建模:

第一小时 第二小时
1 1 1 死亡
1 1 1
1
1 1
1
1 1 1 死亡
1 1
1
1 1 1
1
1 1
1

可以看到细胞总数规律:
第一小时:1
第二小时:2
第三小时:4
第四小时:6
第五小时:10
......
总数上来看,规律不是很明显。

换个思路:既然细胞只能分裂2次,第三个小时死亡。那么:在第n个小时新分裂的细胞,一定是在n-1和n-2小时新分裂出来细胞。因为n-3小时新分裂的细胞,到第n小时就死亡了。
即:第n小时新分裂出的细胞f(n) = f(n-1) + f(n-2)。
因为细胞分裂是1分2,所以第n小时细胞总数就是2*f(n)。
So:
递归公式:第n小时新分裂细胞数 f(n) = f(n-1) + f(n-2)
终止条件:第1小时新分裂细胞数:f(1) = 1; 第2小时新分裂细胞数f(2) = 1;

代码样例:

public int sumCell(int n) {
    if (n <= 0) {
        return 0;
    }

    //第1小时没分裂,直接返回
    if (n == 1) {
       return 1;
    }

    //数组作为缓存,f(x)数据缓存在数组n位置。初始值全是0
    int[] cache = new int[n + 1];
    return 2* newCell(n, cache);
}

//递归第n小时新分类细胞数
public int newCell(int n, int[] cache) {
    if (n == 1) {
        return 1;
    }
    if (n == 2) {
        return 1;
    }

    //f(x)有缓存,直接返回
    if (cache[n] > 0) {
        return cache[n];
    }

    //f(x)计算结果同时放入缓存中
    cache[n] = newCell(n - 1, cache) + newCell(n - 2, cache);
    return cache[n];
}

4. 反转二叉树

反转二叉树的左右节点。

【思路】
递归公式:reverse(root) →
(1)反转子树reverse(root->left) 、 reverse(root->right);
(2)反转root的left、right子节点。
终止条件:反转的节点为null。

5. 路径总和

给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。

【思路】:
递归公式:f(root) = SUM ? → f(root.left) = SUM=root.val || f(root.right) = SUM=root.val
终止条件:到叶子节点Node && 其Node.value == 递减后的值。

注意:

  1. 可能存在Node,只有左子树、或只有右子树。要注意判空。
  2. 节点值可能是负数。
class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if(root == null) {
            return false;
        }

        // 叶子节点、并且其值等于剩余值
        if(root.left == null && root.right == null) {
           if(targetSum == root.val) {
              return true;
           }
           else {
               return false;
           }
        }

        boolean ret = hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
        return ret;
    }
}
posted @ 2021-05-23 10:41  小拙  阅读(195)  评论(0)    收藏  举报