1. 题目来源
链接:不同的二叉搜索树 II
来源:LeetCode
2. 题目说明
给定一个整数 n,生成所有由 1 … n 为节点所组成的二叉搜索树。
示例:
输入: 3
输出:
[
[1,null,3,2],
[3,2,null,1],
[3,1,null,null,2],
[2,1,3],
[1,null,2,null,3]
]
解释:
以上的输出对应以下 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
3. 题目解析
方法一:BST性质、递归解法
这道题是之前的 [每日一题] 131. 不同的二叉搜索树(BST树、数学、动态规划、卡特兰数)的延伸,之前那个只要求算出所有不同的二叉搜索树的个数,这道题让把那些二叉树都建立出来。
这种建树问题一般来说都是用递归来解,这道题也不例外,划分左右子树,递归构造。
其实是考的是 分治法 ,类似的题目还有LeetCode 241. 为运算表达式设计优先级用的方法一样,用 递归 来解,划分左右两个子数组,递归构造。
- 刚开始时,将区间 [1, n] 当作一个整体,需要将其中的每个数字都当作根结点,其划分开了左右两个子区间
- 然后分别调用递归函数,会得到两个结点数组
- 接下来要做的就是从这两个数组中每次各取一个结点当作当前根结点的左右子结点,然后将根结点加入结果 res 数组中即可
通俗来讲就是利用一下查找二叉树的性质。左子树的所有值小于根节点,右子树的所有值大于根节点,所以如果求 1…n 的所有可能:
-
只需要把 1 作为根节点,[ ] 空作为左子树,[ 2 … n ] 的所有可能作为右子树
-
2 作为根节点,[ 1 ] 作为左子树,[ 3…n ] 的所有可能作为右子树
-
3 作为根节点,[ 1 2 ] 的所有可能作为左子树,[ 4 … n ] 的所有可能作为右子树,然后左子树和右子树两两组合
-
4 作为根节点,[ 1 2 3 ] 的所有可能作为左子树,[ 5 … n ] 的所有可能作为右子树,然后左子树和右子树两两组合
-
…
-
n 作为根节点,[ 1… n ] 的所有可能作为左子树,[ ] 作为右子树
至于,[ 2 … n ] 的所有可能以及 [ 4 … n ] 以及其他情况的所有可能,可以利用上边的方法,把每个数字作为根节点,然后把所有可能的左子树和右子树组合起来即可。如果只有一个数字,那么所有可能就是一种情况,把该数字作为一棵树。而如果是 [ ],那就返回 null。
对代码的解释如下:
对于连续整数序列 [left, right] 中的一点 i,若要生成以 i 为根节点的 BST,则有如下规律:
i左边的序列可以作为左子树结点,且左儿子可能有多个,所以有vector<TreeNode *> left = help(left, i - 1);
i右边的序列可以作为右子树结点,同上所以有vector<TreeNode *> right = help(i + 1, right);
产生的以当前i为根结点的 BST(子)树有left.size() * right.size()
个,遍历每种情况,即可生成以 i 为根节点的 BST 序列;
然后以 for 循环使得 [left, right] 中每个结点都能生成子树序列。
参见代码如下:
// 执行用时 :16 ms, 在所有 C++ 提交中击败了89.20%的用户
// 内存消耗 :17.6 MB, 在所有 C++ 提交中击败了8.51%的用户
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<TreeNode*> generateTrees(int n) {
if (n == 0)
return {};
return help(1, n);
}
// 构建(start,end)的二叉树(得到的是所有可能存在的形式的集合)
vector<TreeNode*> help(int start, int end) {
if (start > end)
return {nullptr};
vector<TreeNode*> res;
// 根节点为 i时,生成二叉搜索树
for (int i = start; i <= end; ++i) {
auto left = help(start, i - 1); // 构建左子树(得到的是左子树所有可能存在的形式的集合)递归
auto right = help(i + 1, end); // 构建右子树(得到的是右子树所有可能存在的形式的集合)递归
// 对所有左子树、右子树进行组合
for (auto& a : left) {
for (auto& b : right) {
TreeNode* node = new TreeNode(i);
// 将左、右子树合并到根节点下
node->left = a;
node->right = b;
res.push_back(node);
}
}
}
/ 这里返回的是根节点的指针的vector
// 这个返回值代表的意思是,这个ans里面存了所有当前树的根节点指针,
// 就是,如果l=2,r=4,里面就保存了根节点为:2,3,4时候,所有子树的情况,
// 而且,当根节点为3的时候,左右子树的根可以分别为[1,2]和[4],那么,需要组合所有情况,
// 就是,当根节点为3时,左右子树的根节点分别为[1,4]或者[2,4]
// 通过这个根节点指针,可以找到相应的左子树和右子树,
return res;
}
};
方法二:记忆数组、动态规划法
可以使用记忆数组来优化,保存计算过的中间结果,从而避免重复计算。注意这道题的标签有一个是动态规划其实带记忆数组的递归形式就是 DP 的一种,memo[ i ][ j ] 表示在区间 [i, j] 范围内可以生成的所有 BST 的根结点,所以 memo 必须是一个三维数组,这样在递归函数中,就可以去 memo 中查找当前的区间是否已经计算过了,是的话,直接返回 memo 中的数组,否则就按之前的方法去计算,最后计算好了之后要更新 memo 数组,参见代码如下:
参见代码如下:
// 执行用时 :20 ms, 在所有 C++ 提交中击败了75.17%的用户
// 内存消耗 :12.8 MB, 在所有 C++ 提交中击败了94.01%的用户
class Solution {
public:
vector<TreeNode*> generateTrees(int n) {
if (n == 0)
return {};
vector<vector<vector<TreeNode*>>> memo(n, vector<vector<TreeNode*>>(n));
return helper(1, n, memo);
}
vector<TreeNode*> helper(int start, int end, vector<vector<vector<TreeNode*>>>& memo) {
if (start > end)
return {nullptr};
if (!memo[start - 1][end - 1].empty())
return memo[start - 1][end - 1];
vector<TreeNode*> res;
for (int i = start; i <= end; ++i) {
auto left = helper(start, i - 1, memo), right = helper(i + 1, end, memo);
for (auto a : left) {
for (auto b : right) {
TreeNode *node = new TreeNode(i);
node->left = a;
node->right = b;
res.push_back(node);
}
}
}
return memo[start - 1][end - 1] = res;
}
};