目录
二叉搜索树的最近公共祖先
题干
题目:给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
说明:
-
所有节点的值都是唯一的。
-
p、q 为不同节点且均存在于给定的二叉搜索树中。
思路和代码
之前做了 “二叉树的最近公共祖先” 问题,是用后序遍历层层向上返回子树是否有 p、q 结点。但这道题是要找 二叉搜索树 的最近公共祖先,可以利用二叉树结点有序的特性,来寻找 p、q。同时也不需要回溯,因为二叉搜索树自带方向性,只要自顶向下遍历找到 p、q 所在的区间即可。
递归法
-
递归参数和返回值:参数是传入的当前结点,要查询的 p、q 结点;无返回值。在遍历过程中不断更新公共祖先 ancestor。
-
递归结束的条件:当找到公共祖先时,立即返回,不需要再往下遍历了。
-
递归顺序:先比较 p、q 和当前结点 root 的大小,再根据大小情况遍历左右子树。
-
如果 p、q 都比当前结点大,则遍历右子树。
-
如果 p、q 都比当前结点小,则遍历左子树。
-
其余情况,当前结点即为公共祖先:
-
如果 p < 当前结点 < q 或 q < 当前结点 < p,则说明当前结点就是最近公共祖先,直接返回。
-
如果 p = 当前结点 或 q = 当前结点,由于二叉搜索树有序,则另一个结点肯定在子树中,当前结点即为最近公共祖先,直接返回。
-
-
class Solution {
public:
TreeNode* ancestor = nullptr; // 记录公共祖先结点
void findAncestor(TreeNode* root, TreeNode* p, TreeNode* q){
if (root == nullptr) return;
int minValue = min(p->val,q->val);
int maxValue = max(p->val,q->val);
if (maxValue < root->val){ // 遍历左子树
findAncestor(root->left,p,q);
if (ancestor != nullptr) return;
} else if (minValue > root->val){ // 遍历右子树
findAncestor(root->right,p,q);
if (ancestor != nullptr) return;
} else { // 找到公共祖先
ancestor = root;
return;
}
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
findAncestor(root,p,q);
return ancestor;
}
};
迭代法
思路和递归法相同,只不过将递归转为迭代而已。
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
TreeNode* ancestor = nullptr; // 记录公共祖先
TreeNode* cur = root; // 遍历二叉树
while (ancestor == nullptr){
if (p->val > cur->val && q->val > cur->val){
// 如果 p、q 都比当前结点要大,遍历右子树
cur = cur->right;
} else if (p->val < cur->val && q->val < cur->val){
// 如果 p、q 都比当前结点要小,遍历左子树
cur = cur->left;
} else{
ancestor = cur;
}
}
return ancestor;
}
};
二叉搜索树的插入操作
题干
题目:给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。
说明:输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。
思路和代码
比较容易的方法是找到一个插入位置刚好是末尾结点处,这样就可以直接插入,并不需要修改树的结构。
基本思路就是从上往下遍历,比较 val 和当前结点的大小,若大于则往右子树遍历,小于则往左子树遍历,直到找到末尾节点插入。
递归法
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
// 找到插入位置为末尾结点时,返回要新插入的结点
if (root == nullptr){
TreeNode* newNode = new TreeNode(val);
return newNode;
}
if (val > root->val){
TreeNode* newNode = insertIntoBST(root->right,val);
// 若新节点不为空,说明已经插入,将新插入的结点返回给上一层父节点
if (newNode){
root->right = newNode;
}
} else if (val < root->val){
TreeNode* newNode = insertIntoBST(root->left,val);
if (newNode){
root->left = newNode;
}
}
return root;
}
};
迭代法
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
// 若初始为空结点,直接返回新建结点
if(root == nullptr) return new TreeNode(val);
TreeNode* cur = root;
// 从根节点开始往后遍历
while (cur){
if (val < root->val){
if (cur->left == nullptr){
cur->left = new TreeNode(val);
break;
} else{
cur = cur->left;
}
} else if (val > root->val){
if (cur->right == nullptr){
cur->right = new TreeNode(val);
break;
} else{
cur = cur->right;
}
}
}
return root;
}
};
删除二叉搜索树的结点
题干
题目:给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
-
首先找到需要删除的节点;
-
如果找到了,删除它。
说明:二叉树的节点值唯一。
思路
首先要找到待删除的结点,若没有找到,说明结点不在树中,直接返回原根节点 root;若找到待删除的结点,需要调整二叉树。
如何调整?只需要让待删除结点的左右子树合并为一颗二叉搜索树,再将合并后的二叉搜索树的根节点替代掉删除的位置。
最关键的问题就是如何合并两棵子树?
-
待删除结点是叶子结点,没有左右子树,则合并后的子树为空。
-
待删除结点只有一棵子树,直接让子树的根节点替代掉删除结点。
-
待删除结点有两棵子树,此时有两种方法。
-
方法一:将左子树合并到右子树中,找到右子树的最左边结点(即右子树的最小值),让左子树插入到此结点的左孩子处。
-
方法二:从左右子树中选择一个结点作为新的根结点,为了保持二叉搜索树的特性,这个根节点可以是左子树的最右结点(即左子树的最大值)或右子树的最左结点(即右子树的最小值)。
-
代码
方法一:递归法
将问题拆解为在左右子树中删除结点,并返回删除后子树的新结点给上层。
-
递归参数和返回值:参数是传入的根节点和要删除的节点值。返回值是删除结点后经过调整的子树的根。
-
递归结束的条件:当结点为空,说明没找到,返回空结点;当找到删除结点,执行删除操作,返回。
-
递归顺序:采用前序遍历,先比较当前结点是否为被删除结点,如果当前结点 < 删除结点,遍历右子树;如果当前结点 > 删除结点,遍历左子树。
class Solution {
public:
TreeNode* pre = nullptr; // 记录上一个结点
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) return nullptr; // 遍历到最后,没有找到被删除的结点
if (root->val == key){
// 找到了被删除的结点
// 1)叶子节点
if (root->left == nullptr && root->right == nullptr){
return nullptr; // 返回空结点
} else if (root->left != nullptr && root->right == nullptr){
// 2)左子树非空,右子树空
return root->left;
} else if (root->left == nullptr && root->right != nullptr){
// 3)左子树空,右子树非空
return root->right;
} else{
// 4)左右子树非空,需要返回新结点,先找到右子树的最左边结点,让最左边结点的左孩子为删除节点的左子树
TreeNode* cur = root->right;
while (cur->left != nullptr){
cur = cur->left;
}
cur->left = root->left;
return root->right;
}
}
pre = root; // 记录遍历过的上一层结点
if (root->val < key) root->right = deleteNode(root->right,key); // 遍历右子树
if (root->val > key) root->left = deleteNode(root->left,key); // 遍历左子树
return root;
}
};
方法二:拆解删除步骤
删除的步骤有三步:
-
找到待删除的结点,定义为一个方法 findKey()
-
合并待删除结点的左右子树,定义为一个方法 mergeTwoTree()
-
将合并后的树的根节点替换掉待删除的结点,在方法 deleteNode() 中执行。
三个步骤方法合起来的时间复杂度是 O(h)。
class Solution {
public:
TreeNode* pre = nullptr; // 记录待删除结点的父结点
// 查找待删除结点
TreeNode* findKey(TreeNode* root, int key){
if (root == nullptr) return root;
TreeNode* keyNode = nullptr;
if (key < root->val){
pre = root;
// pre 一定要在 keyNode之前
keyNode = findKey(root->left,key);
} else if (key > root->val){
pre = root;
keyNode = findKey(root->right,key);
} else{
keyNode = root;
}
return keyNode;
}
// 合并左右子树
TreeNode* mergeTwoTree(TreeNode* leftTree, TreeNode* rightTree){
if (leftTree == nullptr && rightTree == nullptr){ // 1)左右子树为空,合并后的子树也为空结点
return nullptr;
} else if (leftTree == nullptr && rightTree != nullptr){ // 2)左子树空,右子树非空,合并后的子树即右子树
return rightTree;
} else if (leftTree != nullptr && rightTree == nullptr){ // 3)左子树空,右子树非空,合并后的子树即右子树
return leftTree;
} else{ // 4)左右子树非空
TreeNode* cur = leftTree; // 找到左子树的最右结点,作为左右子树的根节点
TreeNode* father = nullptr; // 找到 cur 的父节点
while (cur->right != nullptr){
father = cur;
cur = cur->right;
}
if (father == nullptr){
cur->right = rightTree;
} else{
father->right = cur->left;
cur->left = leftTree;
cur->right = rightTree;
}
return cur;
}
}
// 删除结点
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) return root;
// 找到待删除的结点
TreeNode* keyNode = findKey(root,key);
if (keyNode == nullptr) return root;
// 将删除节点的左右子树进行合并
TreeNode* newNode = mergeTwoTree(keyNode->left,keyNode->right);
if (pre == nullptr){
// 如果 pre 为空,说明要删除的结点是根结点
root = newNode;
} else{
if (pre->right == keyNode){
pre->right = newNode;
} else{
pre->left = newNode;
}
}
return root;
}
};