【LeetCode】不同的二叉搜索树(递归——递归+记忆搜索优化——动态规划)

这篇博客详细介绍了如何解决LeetCode上的【不同二叉搜索树】问题,从递归算法开始,逐步优化到记忆化搜索,最后通过动态规划实现0ms的高效解法。博主通过递归逻辑分析,发现了动态规划的递推公式,并给出了动态规划的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

#LeetCode每日一题【二叉树专题】

  • 不同的二叉搜索树
    https://leetcode-cn.com/problems/unique-binary-search-trees/
  • 分析
    二叉搜索树,根节点的值大于左节点小于右节点,且他的每一颗子树都是二叉搜索树。
    所以确定根节点i之后,那左子树的值必然是是[1,i)之间,右子树的值必然是在(i,n]之间。
    即能切分成子问题:左子树的根节点值j在[1,i)中,其左子树的值必然是在[i,j),其右子树的值必然是在(j,i)…以此类推。
    即是一个递归问题,不断的寻找左子树和右子树的数量,最终结果是左子树的数目*右子树的数目
  1. 递归结束条件,区间start大于end;
  2. 递归返回值:左子树有多种可能,右子树也有多种可能,记录可能的数目;
  3. 最小问题,本级递归应该做:左子树的数目*右子树的数目
  • 代码实现
	public int numTrees(int n) {
        return numTrees(1, n);
    }

    private int numTrees(int start, int end) {
        if (start > end) {
            return 1;
        }
        int sum = 0;
        for (int i = start; i <= end; i++) {
            // 左子树的数目
            int left = numTrees(start, i - 1);

            // 右子树的数目
            int right = numTrees(i + 1, end);

            // i作为根节点,拼上左右子树之后,可能的数量
            sum += left * right;
        }
        return sum;
    }

因为这里面的递归存在大量的重复计算,所以当n=18的时候,LeetCode显示超时
在这里插入图片描述
如当n=18计算i=10的时候左子树需要计算1-9区间的值,再计算i=11的时候需要1-10区间的值其中包含了1-9,即有重复计算了一次。那么我们可以使用记忆化搜索的方式将1-9区间的值记录下来,再下次递归的时候进行判断,如果记忆中有则直接取,若没有则放入记忆中。

  • 优化——递归+记忆搜索法
	public int numTrees(int n) {
        int[][] memory = new int[n + 2][n + 2];
        return numTreesMemory(1, n, memory);
    }
    
    // 使用记忆搜索优化
    // 如计算10的左右子树的数量的时候,左子树在1-9之间组合;当计算11的时候,左子树在1-10之间组合,必然包含了1-9等,即存在大量的重复计算。
    // 用一个二维数组存储start、end的计算结果,当准备递归调用之前先判断是否在记忆存储,若有则直接取,若无则放入
    private int numTreesMemory(int start, int end, int[][] memory) {
        if (start > end) {
            // 返回1不是0,表示该子树为null,也是一种可能
            return 1;
        }
        int sum = 0;
        for (int i = start; i <= end; i++) {
            // 左子树的数目
            int left = memory[start][i - 1] > 0 ? memory[start][i - 1] : numTreesMemory(start, i - 1, memory);

            // 右子树的数目
            int right = memory[i + 1][end] > 0 ? memory[i + 1][end] : numTreesMemory(i + 1, end, memory);

            // i作为根节点,拼上左右子树之后,可能的数量
            sum += left * right;
            memory[start][i - 1] = left;
            memory[i + 1][end] = right;
        }
        return sum;
    }

在原来的代码基础上进行的记忆优化,LeetCode耗时:0ms
在这里插入图片描述

  • 动态规划
    看了评论和题解之后,说是一个典型的动态规划类型的题;

    动态规划关键于寻找递推公式:

    	dp(0)=1
    	dp(1)=dp(0)*dp(0)=1
    	dp(2)=dp(0)*dp(1)+dp(1)*dp(0)=2
    	dp(3)=dp(0)*dp(2)+dp(1)*dp(1)+dp(2)*dp(0)=5
    	...
    	dp(n)=dp(0)*dp(n-1)+dp(1)*dp(n-2)+...+dp(n-1)*dp(0)
    	即
    		   n
    	dp(n)= ∑ dp(i-1)*dp(n-i)
    	      i=1
    

    dp(n)表示:从1-n个数,能组成的二叉搜索树的数量
    其实该递推公式寻找的过程也是上面递归算法体现的逻辑,从1-n分别作为根,在[1,i)之间寻找左子树的数量(有i-1个节点),在(i,n]之间寻找右子树的数量(有n-i个节点),最后累计求和
    具体实现:

    // 使用动态规划,递推公式:dp(n)= 求和(i=1~n) dp(i-1)*dp(n-i)
    public int numTreesDP(int n) {
        // 初始化dp
        int[] dp = new int[n + 1];
        dp[0] = 1;
    
        // 根据递推公式
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                // 计算每一步的结果,i =》 1~n
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
    

    LeetCode耗时:0ms
    在这里插入图片描述

    • 总结
      该题通过最开始的递归——递归+记忆搜索优化——动态规划,算法思想层层递进,最重要的是如何分析问题,想到使用递归,在优化递归,再寻找递推公式使用动态规划。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值