[二叉搜索树(递归练习)]96.95.不同的二叉搜索树 I II (动态规划、递归)

12 篇文章 0 订阅
11 篇文章 0 订阅

95. 不同的二叉搜索树 II(很好的递归练习题)

题目链接:95. 不同的二叉搜索树 II

分类:树、递归

在这里插入图片描述

题目分析

这是一题适合用来训练递归的题目。要重做。

思路:递归法

解析参考:https://leetcode-cn.com/problems/unique-binary-search-trees-ii/solution/cong-gou-jian-dan-ke-shu-dao-gou-jian-suo-you-shu-/

算法设计

1、如何用递归构建一棵二叉搜索树?

例如:按1~n构建一棵二叉搜索树:

public TreeNode generate(int n){
    return helper(1, n);
}
//递归函数
public TreeNode helper(int start, int end){
    if(start > end) return null;

    int mid = (start + end) / 2;//这里选择可选数值上下界的平均值作为root.val,所以得到的是平衡搜索树
    TreeNode root = new TreeNode(mid);
    root.left = helper(start, mid - 1);//使用递归函数获取左子树
    root.right = helper(mid + 1, end);//使用递归函数获取右子树
    return root;
}

如何理解递归函数:不需要去深入递归一层层分析,只需明确递归函数的功能和返回值;设置好递归出口,和递归返回值。

这个递归函数helper()返回的是一棵节点取值范围在[start,end]内的树的根节点,所以调用helper(start, mid - 1)就是在start~mid-1的范围内生成一棵树并返回该树的根节点,可以提供给root的左子树;同理helper(mid + 1, end)生成的是root的右子树。

返回值:返回根节点root。

递归出口:如果剩余的节点可选取值为空:start > end,则说明该节点之下没有子树,返回null。

2、如何递归构建多个二叉树?

要构建多颗二叉树,关键在于要选择不同数值作为根节点构建不同的二叉树,构成二叉树集合。

可以通过for循环遍历给定的取值范围,把遍历到的每个数值都作为一个根节点:

for(int i = start; i <= end; i++){
    TreeNode root = new TreeNode(i);
    ...
}

返回值:因为要用递归函数构建多个二叉树,所以返回值类型要修改为二叉树列表,来存放多个二叉树:List < TreeNode >,每一层递归都独立拥有一个集合列表。

递归出口

  • 当start > end 时,剩余可选数值为空,所以将null加入list中返回,相当于将一个空二叉树加入列表集合。
  • 当start== end 时,剩余可选数值只有一个,可以直接将该数值生成一个节点加入列表,相当于将一个只有根节点的二叉树加入列表集合。
  • 这里需要注意,可选数值为空时也要构造一个null加入到list中,这样list才不为空列表,在for对列表做高级遍历时才不会出错。

递归主体
现在问题变成了如何构建root的左右子树,我们抛开复杂的递归函数,只关心递归的返回值,每次选择根结点root,然后:

  • 递归构建左子树,并拿到左子树所有可能的根结点列表left
  • 递归构建右子树,并拿到右子树所有可能的根结点列表right
    List<TreeNode> left = helper(start, i-1);  
    List<TreeNode> right = helper(i+1, end); 

这个时候我们有了左右子树列表,我们的左右子树都是各不相同的,因为根结点不同,我们如何通过左右子树列表构建出所有的以root为根的树呢?

我们固定一个左孩子,遍历右子树列表,选取右子树的每一个右孩子,每一对左右孩子都与当前的root组合,然后将root加入到当前层对应的二叉树集合中:(相当于从左右子树集合中做两两组合)

    // 固定左孩子,遍历右孩子
    for(TreeNode l : left){
        for(TreeNode r : right){
            TreeNode root = new TreeNode(i);
            root.left = l;
            root.right = r;
            list.add(root);
        }
    }

实现代码

class Solution {
    
    public List<TreeNode> generateTrees(int n) {
        List<TreeNode> res = new ArrayList<>();
        if(n == 0) return res;
        res = helper(1, n);
        return res;
    }
    //递归实现
    public List<TreeNode> helper(int start, int end){
        List<TreeNode> res = new ArrayList<>();

        if(start > end){
            res.add(null);//空节点也视为一个二叉树
            return res;
        }
        if(start == end){
            res.add(new TreeNode(start));
            return res;
        }
        for(int i = start; i <= end; i++){
            List<TreeNode> leftTrees = helper(start, i - 1);//获取所有可能的左子树集合
            List<TreeNode> rightTrees = helper(i + 1, end); //获取所有可能的右子树集合
            //从左子树集合和右子树集合分别取出一个子树构成当前root的左右子树
            for(TreeNode left : leftTrees){
                for(TreeNode right : rightTrees){
                    TreeNode root = new TreeNode(i);
                    root.left = left;
                    root.right = right;
                    res.add(root);
                }
            }
        }
        return res;
    }
}

96. 不同的二叉搜索树

题目链接: 96. 不同的二叉搜索树

分类:树、动态规划、数学规律

在这里插入图片描述

题目分析

虽然本题和95题类似,都是计算二叉搜索树相关的题目,但本题计算的是 1 … n 为节点组成的二叉搜索树有多少种,不需要构造出每一棵二叉树。

解题思路实质上都分治思想卡特兰公式有关,只是在于实现方式是递归、动态规划还是记忆化递归。这里我使用的是动态规划。

思路1:动态规划 + 分治法

参考题解:https://leetcode-cn.com/problems/unique-binary-search-trees/solution/er-cha-sou-suo-shu-fu-xi-li-zi-jie-shi-si-lu-by-xi/(分治思想讲的很详细)

1、状态设置 + 状态转移方程

设G(n)表示n个节点可以组成的二叉搜索树个数,如果n=1,G(n)=1;n=2,G(n)=2,这些初始值是可以立刻得到的。
设以i为根节点的二叉搜索树个数为f(i),G(n)=f(1) + f(2) + … + f(n)

当n=5时,[1,2,3,4,5],
则G(5) = f(1) + f(2) + f(3) + f(4) + f(5),其中:

  • 以1为根节点的二叉搜索树f(1) = 左子树=[]能组成的二叉搜索树个数G(0) * 右子树=[2,3,4,5]能组成的二叉搜索树个数G(4);
  • 以2为根节点的二叉搜索树f(2) = 左子树=[1]能组成的二叉搜索树个数G(1) * 右子树=[3,4,5]能组成的二叉搜索树个数G(3)。
    以此类推,所以G(5) = G(0)*G(4)+G(1)*G(3)+G(2)*G(2)+G(3)*G(1)+G(4)*G(0),由此得到状态转移方程:
G(n) = G(0)*G(n-1) + ... + G(n-1)*G(0),对比可以发现这个方程就是卡特兰公式。
2、如何体现分治思想?

以求2为根节点的二叉树f(2)为例,问题分解为求[1]能组成的二叉搜索树个数 和 [3,4,5]能组成的二叉搜索树个数:

[1]能组成的二叉树个数=1;

[3,4,5]能组成的二叉树个数又可以继续分解为 以3为根、以4为根、以5为根所能组成的二叉搜索树个数。

以3为根,则f(3)=[]能组成的二叉搜索树个数 * [4,5]能组成的二叉搜索树个数;问题又分解为求[4,5]能组成的二叉树个数 = 以4为根,以5为根的二叉树个数。

以4为根,则f(4)=[]能组成的二叉搜索树个数 * [5]能组成的二叉搜索树个数。此时到达边界,单个节点或节点为null能构成的二叉搜索树个数=1。

以此类推,问题可以被不断分解,每个子问题的解又可以组成大问题的解。

3、dp数组

开辟dp[n+1]数组来表示G(n),所以初始值dp[1]=1,dp[2]=2。
最终返回结果:dp[n]存放的就是G(n)的值。

实现时遇到的问题

为什么dp[0]要设置为1?

因为dp[0]即G(0)表示0个节点组成的二叉搜索树个数,因为空二叉树也可以认为是一种二叉树,所以dp[0]=1.

实现代码

class Solution {
    public int numTrees(int n) {
        if(n <= 0) return 0;
        int[] dp = new int[n + 1];
        dp[0] = 1;//dp[0]为什么要设置为1?见“实现遇到的问题”
        dp[1] = 1;
        for(int i = 2; i <= n; i++){//依次求出dp[2]~dp[n]
            for(int j = 0; j <= i - 1; j++){//计算G(i)
                dp[i] += dp[j] * dp[i - j - 1];
            }
        }
        return dp[n];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值