一. 因为当时没有理清二叉搜索树的含义,所以直接看答案了.........所以基础的数据结构一定要好好掌握.
二. 参考大神
作者:windliang
链接:https://leetcode-cn.com/problems/unique-binary-search-trees-ii/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-2-7/
解法一 回溯法
1. 常规的回溯思想,就是普通的一个 for 循环,尝试插入 1, 2 ... n,然后进入递归,在原来的基础上继续尝试插入 1, 2... n。直到树包含了所有的数字。就是差不多下边这样的框架。
作者的java代码,不过很遗憾重复了.........是的,因为每次循环都尝试了所有数字,所以造成了重复。
public List<TreeNode> generateTrees(int n) {
List<TreeNode> ans = new ArrayList<TreeNode>();
if (n == 0) {
return ans;
}
TreeNode root = new TreeNode(0); //作为一个哨兵节点
getAns(n, ans, root, 0);
return ans;
}
private void getAns(int n, List<TreeNode> ans, TreeNode root, int count) {
if (count == n) {
//复制当前树并且加到结果中
TreeNode newRoot = treeCopy(root);
ans.add(newRoot.right);
return;
}
TreeNode root_copy = root;
//尝试插入每个数
for (int i = 1; i <= n; i++) {
root = root_copy;
//寻找要插入的位置
while (root != null) {
//在左子树中插入
if (i < root.val) {
//到了最左边
if (root.left == null) {
//插入当前数字
root.left = new TreeNode(i);
//进入递归
getAns(n, ans, root_copy, count + 1);
//还原为 null,尝试插入下个数字
root.left = null;
break;
}
root = root.left;
//在右子树中插入
}
else if (i > root.val) {
//到了最右边
if (root.right == null) {
//插入当前数字
root.right = new TreeNode(i);
//进入递归
getAns(n, ans, root_copy, count + 1);
//还原为 null,尝试插入下个数字
root.right = null;
break;
}
root = root.right;
//如果找到了相等的数字,就结束,尝试下一个数字
}
else {
break;
}
}
}
}
解法二 递归
1. 解法一完全没有用到查找二叉树的性质,暴力尝试了所有可能从而造成了重复。我们可以利用一下查找二叉树的性质。左子树的所有值小于根节点,右子树的所有值大于根节点。
2. 所以如果求 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。
3. 递归的关键在于明确递归函数的定义.(坑有点多,要仔细分析一下代码).
//递归
class Solution {
public:
//假设递归函数getAns的含义为给定生成二叉搜索树的范围[start,end],
//返回最终能生成的所有不同二叉搜索树.
vector<TreeNode*> getAns(int start, int end) {
//定义中间数组ans,保存当前递归下的结果.
vector<TreeNode*> ans;
//如果s>e,则保存NULL,因为生成不了.
if (start > end) {
ans.push_back(NULL);
//别忘了返回.
return ans;
}
//如果s==e,则保存.
if (start == end) {
TreeNode* tmp = new TreeNode(start);
ans.push_back(tmp);
//别忘了返回.
return ans;
}
//遍历所有值的范围,因为都可以当做一个根节点.
for (int i = start; i <= end; i++) {
//因为在i左边的都比i小.
vector<TreeNode*> leftTree = getAns(start, i - 1);
//在i右边的都比i大.
vector<TreeNode*> rightTree = getAns(i + 1, end);
//遍历所有生成的左子树以及右子树,并组合.
for (auto left : leftTree) {
for (auto right : rightTree) {
//生成根节点,
//必须将生成根节点放在里面,
//这样会将上一个结果清空.
TreeNode* root = new TreeNode(i);
root->left = left;
root->right = right;
//保存当前结果.
ans.push_back(root);
}
}
}
return ans;
}
vector<TreeNode*> generateTrees(int n) {
//初始情况,如果n==0,直接返回ans,为空的
vector<TreeNode*> ans;
if (n <= 0)
return ans;
return getAns(1, n);
}
};
解法三 动态规划
1. 大多数递归都可以用动态规划的思想重写,这道也不例外。从底部往上走.(可以看下原作者的解析).
2. 然后利用上边的思路基本上可以写代码了,就是求出长度为 1 的所有可能,长度为 2 的所有可能 ... 直到 n。但是我们注意到,求长度为 2 的所有可能的时候,我们需要求 [ 1 2 ] 的所有可能,[ 2 3 ] 的所有可能,这只是 n = 3 的情况。如果 n 等于 100,我们需要求的更多了 [ 1 2 ] , [ 2 3 ] , [ 3 4 ] ... [ 99 100 ] 太多了。能不能优化呢? 仔细观察,我们可以发现长度是为 2 的所有可能其实只有两种结构。
3. 通过上边的所有分析,代码可以写了,总体思想就是求长度为 2 的所有情况,求长度为 3 的所有情况直到 n。而求长度为 len 的所有情况,我们只需要求出一个代表 [ 1 2 ... len ] 的所有情况,其他的话加上一个偏差,加上当前根节点即可。
4. 直接看作者的java代码,(太妙了,需要多多理解).
public List<TreeNode> generateTrees(int n) {
ArrayList<TreeNode>[] dp = new ArrayList[n + 1];
dp[0] = new ArrayList<TreeNode>();
if (n == 0) {
return dp[0];
}
dp[0].add(null);
//长度为 1 到 n
for (int len = 1; len <= n; len++) {
dp[len] = new ArrayList<TreeNode>();
//将不同的数字作为根节点,只需要考虑到 len
for (int root = 1; root <= len; root++) {
int left = root - 1; //左子树的长度
int right = len - root; //右子树的长度
for (TreeNode leftTree : dp[left]) {
for (TreeNode rightTree : dp[right]) {
TreeNode treeRoot = new TreeNode(root);
treeRoot.left = leftTree;
//克隆右子树并且加上偏差
treeRoot.right = clone(rightTree, root);
dp[len].add(treeRoot);
}
}
}
}
return dp[n];
}
private TreeNode clone(TreeNode n, int offset) {
if (n == null) {
return null;
}
TreeNode node = new TreeNode(n.val + offset);
node.left = clone(n.left, offset);
node.right = clone(n.right, offset);
return node;
}
作者:windliang
链接:https://leetcode-cn.com/problems/unique-binary-search-trees-ii/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-2-7/
解法四 动态规划 2
1. 解法三的动态规划完全是模仿了解法二递归的思想,这里再介绍另一种思想,会更好理解一些。
2. 仔细分析,可以发现一个规律。首先我们每次新增加的数字大于之前的所有数字,所以新增加的数字出现的位置只可能是根节点或者是根节点的右孩子,右孩子的右孩子,右孩子的右孩子的右孩子等等,总之一定是右边。其次,新数字所在位置原来的子树,改为当前插入数字的左孩子即可,因为插入数字是最大的。
3. 以上就是根据 [ 1 2 ] 推出 [ 1 2 3 ] 的所有过程,可以写代码了。由于求当前的所有解只需要上一次的解,所有我们只需要两个 list,pre 保存上一次的所有解, cur 计算当前的所有解。
4. 作者java代码, 看代码可以发现,动态规划真的很妙........
public List<TreeNode> generateTrees(int n) {
List<TreeNode> pre = new ArrayList<TreeNode>();
if (n == 0) {
return pre;
}
pre.add(null);
//每次增加一个数字
for (int i = 1; i <= n; i++) {
List<TreeNode> cur = new ArrayList<TreeNode>();
//遍历之前的所有解
for (TreeNode root : pre) {
//插入到根节点
TreeNode insert = new TreeNode(i);
insert.left = root;
cur.add(insert);
//插入到右孩子,右孩子的右孩子...最多找 n 次孩子
for (int j = 0; j <= n; j++) {
TreeNode root_copy = treeCopy(root); //复制当前的树
TreeNode right = root_copy; //找到要插入右孩子的位置
int k = 0;
//遍历 j 次找右孩子
for (; k < j; k++) {
if (right == null)
break;
right = right.right;
}
//到达 null 提前结束
if (right == null)
break;
//保存当前右孩子的位置的子树作为插入节点的左孩子
TreeNode rightTree = right.right;
insert = new TreeNode(i);
right.right = insert; //右孩子是插入的节点
insert.left = rightTree; //插入节点的左孩子更新为插入位置之前的子树
//加入结果中
cur.add(root_copy);
}
}
pre = cur;
}
return pre;
}
private TreeNode treeCopy(TreeNode root) {
if (root == null) {
return root;
}
TreeNode newRoot = new TreeNode(root.val);
newRoot.left = treeCopy(root.left);
newRoot.right = treeCopy(root.right);
return newRoot;
}
作者:windliang
链接:https://leetcode-cn.com/problems/unique-binary-search-trees-ii/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-2-7/