算法专题:递归

什么是递归?

函数直接或间接调用自身
 

递归的特点:

  • 自身调用:原问题可以分解为子问题,子问题和原问题的求解方法是一致的,即都是调用自身的同一个函数。
  • 终止条件:递归必须有一个终止的条件,不能无限循环地调用本身。
public int sum(int n) {
    if (n <= 1) {   // 终止条件
        return 1;
    } 
    return sum(n - 1) + n;   // 自身调用
}

 
递归与栈的关系:

其实,递归的过程,可以理解为出入栈的过程

函数sum(n=5)的递归执行过程,如下:

img
 

递归的应用场景

  • 阶乘问题
  • 二叉树深度
  • 汉诺塔问题
  • 斐波那契数列
  • 快速排序、归并排序(分治算法体现递归)
  • 遍历文件,解析xml文件
     

递归解题思路

  1. 定义函数功能
  2. 寻找终止条件
  3. 递推关系式
int fibonaci(int n)   // 1.定义函数功能
{
    if(n==0)   // 2.递归终止条件
        return 0;
    if(n==1)
        return 1;
    return fibonaci(n-1)+fibonaci(n-2);   // 3.递推关系式
}

 

leetcode 案例分析

226. 翻转二叉树

示例:

输入:

      4
    /   \
  2      7
 / \    / \
1   3  6   9

输出:

      4
    /   \
  7      2
 / \    / \
9   6  3   1
  1. 定义函数功能

    给出一棵树,然后翻转它,题目已给出

    //翻转一颗二叉树
    public TreeNode invertTree(TreeNode root) {
    
    }
    /**
     * Definition for a binary tree node.
     * struct TreeNode {
     *     int val;
     *     struct TreeNode *left;
     *     struct TreeNode *right;
     * };
     */
    struct TreeNode* invertTree(struct TreeNode* root){
        struct TreeNode *left, *right;
    }
    
  2. 寻找终止条件

    public TreeNode invertTree(TreeNode root) {
        if(root==null || (root.left ==null && root.right ==null)){
           return root;
        }
    }
    
  3. 递推关系式

    翻转一棵树,就是递归地交换左右子树

    递推关系式:invertTree(root)= invertTree(root.left) + invertTree(root.right);

    //翻转一颗二叉树
    public TreeNode invertTree(TreeNode root) {
        if(root==null || (root.left ==null && root.right ==null){
           return root;
        }
        //翻转左子树
        TreeNode left = invertTree(root.left);
        //翻转右子树
        TreeNode right= invertTree(root.right);
    }
    
  4. 完整版

    /* C语言 */
    struct TreeNode* invertTree(struct TreeNode* root){
        if(root==NULL || (root->left ==NULL && root->right ==NULL)){
            return root;
        }
        struct TreeNode* left = invertTree(root->left);  //翻转左子树
        struct TreeNode* right= invertTree(root->right);  //翻转右子树
        //左右子树交换位置
        root->left = right;
        root->right = left;
        return root;
    }
    
    /* Java */
    class Solution {
        public TreeNode invertTree(TreeNode root) {
            if(root==null || (root.left==null)&&(root.right==null)){
                return root;
            }
            TreeNode left = invertTree(root.left);
            TreeNode right = invertTree(root.right);
            root.left = right;
            root.right = left;
            return root;
        }
    }
    

 

递归存在的问题

  1. 递归调用层级太多,导致栈溢出问题

    • 每一次函数调用在内存栈中分配空间,而每个进程的栈容量是有限的。
    • 当递归调用的层级太多时(比如50000次),就会超出栈的容量,从而导致调用栈溢出。
  2. 递归重复计算,导致效率低下

    看一道经典的青蛙跳阶问题:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

    直接使用递归会超时

    class Solution {
        public int numWays(int n) {
        if (n == 0){
           return 1;
         }
        if(n <= 2){
            return n;
        }
        return numWays(n-1) + numWays(n-2);
        }
    }
    

    画出递归树分析

    • 要计算原问题 f(10),就需要先计算出子问题 f(9) 和 f(8)
    • 然后要计算 f(9),又要先算出子问题 f(8) 和 f(7),以此类推。
    • 一直到 f(2) 和 f(1),递归树才终止。

    「递归时间复杂度 = 解决一个子问题时间*子问题个数」

    • 一个子问题时间 = f(n-1)+f(n-2),也就是一个加法的操作,所以复杂度是 「O(1)」
    • 问题个数 = 递归树节点的总数,递归树的总结点 = 2n-1,所以是复杂度**「O(2n)」**。

    因此,青蛙跳阶,递归解法的时间复杂度 = O(1) * O(2^n) = O(2^n),就是指数级别的,爆炸增长的,「如果n比较大的话,超时很正常的了」

    问题:这棵递归树存在**「大量重复计算」**,比如f(8)被计算了两次,f(7)被重复计算了3次…

    解决:先把计算好的答案存下来,即造一个备忘录,等到下次需要的话,先去**「备忘录」查一下,如果有,就直接取就好了,备忘录没有才再计算!这就是「带备忘录的解法」**

    一般使用一个数组或者一个哈希map充当这个**「备忘录」**。

    假设f(10)求解加上**「备忘录」**:

    • f(10)= f(9) + f(8),f(9) 和f(8)都需要计算出来,然后再加到备忘录中
    • f(9) = f(8)+ f(7),f(8)= f(7)+ f(6), 因为 f(8) 已经在备忘录中啦,所以可以省掉,f(7),f(6)都需要计算出来,加到备忘录中
    • f(8) = f(7)+ f(6),发现f(8),f(7),f(6)全部都在备忘录上了,所以都可以不用计算。

    带「备忘录」的递归算法,子问题个数 = 树节点数 = n,解决一个子问题还是O(1),所以**「带「备忘录」的递归算法的时间复杂度是O(n)」**。代码如下:

    public class Solution {
        //使用哈希map,充当备忘录的作用
        Map<Integer, Integer> tempMap = new HashMap();
        public int numWays(int n) {
            // n = 0 也算1种
            if (n == 0) {
                return 1;
            }
            if (n <= 2) {
                return n;
            }
            //先判断有没计算过,即看看备忘录有没有
            if (tempMap.containsKey(n)) {
                //备忘录有,即计算过,直接返回
                return tempMap.get(n);
            } else {
                // 备忘录没有,即没有计算过,执行递归计算,并且把结果保存到备忘录map中,对1000000007取余(这个是leetcode题目规定的)
                tempMap.put(n, (numWays(n - 1) + numWays(n - 2)) % 1000000007);
                return tempMap.get(n);
            }
        }
    }
    

参考:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值