[二叉搜索树(递归练习)]96.95.不同的二叉搜索树 I II (动态规划、递归)
95. 不同的二叉搜索树 II(很好的递归练习题)
分类:树、递归
题目分析
这是一题适合用来训练递归的题目。要重做。
思路:递归法
算法设计
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. 不同的二叉搜索树
分类:树、动态规划、数学规律
题目分析
虽然本题和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];
}
}