目录
Leetcode 235. 二叉搜索树的最近公共祖先
题目链接:Leetcode 235. 二叉搜索树的最近公共祖先
题目描述:给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
思路:和昨天做的这道题236. 二叉树的最近公共祖先很像,只不过本题我们可以利用二叉搜索树的性质,每次比较大小后确定下一次的搜索方向,只要搜索到的节点的val在[p, q]区间中则可以说明该节点就是p 和 q的公共祖先。注意这里是闭区间,也就是说搜索到的节点的值可以等于p和q。为什么搜索到的一定是最近公共祖先呢?有没有可能之后还有公共祖先,只不过没有来得及搜索?
我们可以用反证法来思考一下:假设当前搜索到的节点是root,此时(1)p和q分布在root的两侧(或者与root重合)。如果继续向root的左右子树之一搜索,如果还存在公共祖先,说明(2)p和q分布在root->left或root->right两侧(或者与其重合)。无论哪种情况,(2)都与(1)的结论相悖。因此先搜索到的一定是最近公共祖先。
代码如下:(递归法)
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root->val > p->val && root->val > q->val) {
//如果传入节点比两个节点大,向左寻找
return lowestCommonAncestor(root->left, p, q);
} else if (root->val < p->val && root->val < q->val) {
//如果传入节点比两个节点小,向右寻找
return lowestCommonAncestor(root->right, p, q);
} else //此时要么root为空节点,要么root与p或q相等,直接返回root
return root;
}
};
(迭代法)
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
while (root) {
if (root->val > p->val && root->val > q->val) {
root = root->left;
} else if (root->val < p->val && root->val < q->val) {
root = root->right;
} else
return root;
}
return nullptr;
}
};
Leetcode 701.二叉搜索树中的插入操作
题目描述:给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据保证,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果。
思路:由于题干说插入后保持二叉搜索树即可,因此可以不用改变树的结构,只需二分查找到该插入的位置即可。
代码如下:(递归法)
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if (root == nullptr) {//当遍历到空节点的时候就可以把新节点加入了
TreeNode* node = new TreeNode(val);
return node;
}
if (root->val > val)//如果value比根节点的值大,就向左搜索
root->left = insertIntoBST(root->left, val);
if (root->val < val)//如果value比根节点的值小,就向右搜索
root->right = insertIntoBST(root->right, val);
return root;
}
};
(迭代法)
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if (root == nullptr) {
TreeNode* node = new TreeNode(val);
return node;
}
//由于cur最终会遍历到空节点,因此需要另一个变量保存cur的前一个位置
//否则无法赋值
TreeNode *cur = root, *parent = nullptr;
while (cur != nullptr) {
parent = cur;
if (cur->val > val)
cur = cur->left;
else //由于二叉搜索树不能有重复元素,因此可以这么写
cur = cur->right;
}
TreeNode* node = new TreeNode(val);
if (parent->val > val)
parent->left = node;
else //由于二叉搜索树不能有重复元素,因此可以这么写
parent->right = node;
return root;
}
};
Leetcode 450.删除二叉搜索树中的节点
题目描述:给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
思路:一般来说,删除节点可分为两个步骤:首先找到需要删除的节点; 如果找到了,删除它。删除和上道题的插入不同,可能不可避免地要改变树的结构了。删除一个节点需要分类讨论:
- 第一种情况:没找到删除的节点,遍历到空节点直接返回了
- 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点。
- 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
- 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
- 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
代码如下:(递归法)
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr)
return root; // 1.没找到删除的节点
if (root->val == key) {
// 2.删除的节点是叶子节点
if (root->left == nullptr && root->right == nullptr) {
delete root;
return nullptr;
}
// 3.左孩子为空
else if (root->left == nullptr) {
TreeNode* node = root->right;
delete root;
return node;
}
// 4.右孩子为空
else if (root->right == nullptr) {
TreeNode* node = root->left;
delete root;
return node;
}
// 5.左右孩子都不为空(二选一)
//(1)将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置
//(2)将删除节点的右子树放到删除节点的左子树的最右面节点的右孩子的位置
else {
//以(1)为例
TreeNode* node = root->right;
while (node->left != nullptr) {
node = node->left;
}
node->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置
TreeNode* tmp = root; //保存root,方便后续删除root
root = root->right;
delete tmp;
return root;
}
}
if (root->val > key)
root->left = deleteNode(root->left, key);
if (root->val < key)
root->right = deleteNode(root->right, key);
return root;
}
};
我们发现第五种情况(删除度为2的节点)比较麻烦,而删除叶子节点(删除度为0的节点)最简单,什么都不用管,直接删除就好,那有没有方法可以将第五种情况转化删除叶子节点呢?当然有!我们可以将第五种情况的root左子树最靠右的节点或者root右子树最靠左的节点与其交换数值,然后就转化成了删除叶子节点(转化成第二种情况)。根据二叉搜索树的性质可以知道,此时仍然是一颗二叉搜索树。尽管如此,仍然没有将第三、四种情况与上述操作统一(也就是删除度为1的节点)。让我们思考一下我们是如何判断一个节点属于2~5这四种情况的?
第二种情况:root->left == nullptr&&root->right == nullptr
第三种情况:root->left == nullptr&&root->right != nullptr
第四种情况:root->left != nullptr&&root->right == nullptr
第五种情况:root->left != nullptr&&root->right != nullptr
我们发现都需要对root的左右孩子同时进行判断,才可以确定root属于哪种情况,那如果我们只做其中一个判断条件,是不是可以把上面四种情况抽象成两种情况进行处理?由于删除操作的只需要调整节点之间的父子关系,因此可以把空节点也看成一个实际存在节点进行处理。
举个例子:如果我们只判断root->right是否为空,是不是可以将二和四抽象成一种情况,三和五抽象成另一种情况?同样的,只判断root->left也是一个道理。
代码如下:(优化后递归)
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if(root==nullptr) return root;//第一种情况:若为空节点直接返回
if(root->val==key){
if(root->right==nullptr){//只要右孩子为空,无论左孩子是否为空,都直接返回左孩子
//对于第四种情况,此时return就相当于删除root,root->left接替root的位置
//对于第二种情况,此时return就相当于删除root,root->left(也就是nullptr)接替root的位置
return root->left;
}
TreeNode* node=root->right;
while(node->left){//找到右子树最靠左的节点
node=node->left;
}
//对于第三种情况和第五种情况,会将待删除的节点root转化成叶子节点
//然后在第二次搜索到的时候被删除(也就是在上面那两种情况的地方)
swap(node->val,root->val);//将右子树最靠左的节点放到root所在位置
}
root->left = deleteNode(root->left, key);
root->right = deleteNode(root->right, key);
return root;
}
};
总结:今天最后一道题的优化思路不太容易理解,想了很长时间算是给出了一种比较合理的解释吧。
最后,如果文章有错误,请在评论区或私信指出,让我们共同进步!