删除二叉搜索树中的节点
题目要求:给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:1. 首先找到需要删除的节点;2. 如果找到了,删除它。
解决这道题的关键是要分情况讨论:
- 要删除的结点无孩子结点 (直接删),这是毫无疑问的,删除我后,父亲节点直接指向空。
- 要删除的结点只有左孩子结点(直接删),我的孩子只有一个,删除我后,右为空,将父亲指向我的左。
- 要删除的结点只有右孩子结点 (直接删),删除我后,左为空,将父亲指向我的右。 总的来说父亲带的孩子数不变,原来带了我,但是我删除了,最后又带了我的孩子。
- 要删除的结点左、右孩子结点均存在 (间接删),我有两个孩子,不能全都托付给父亲,孩子太多了!-> 使用替换删除法,在它的右子树中寻找中序下的第一个结点(节点值最小),或者是在它的左子树中寻找中序下的最后一个节点(节点值最大),将该节点的值和被删除节点的值进行交换,最终待删除的节点转移到了最底端,转化成了第1 、2、3种情况的删除问题。
下面借助画图来说明:
- 删除叶子结点(属于第1种情况):只需要知道要删除节点的父节点,将父节点置为NULL,删除子节点即可。
- 删除只有一个孩子节点(属于第2、3种情况):需要知道要删除节点的父节点,关键问题是要用父节点的左还是右来指向? 这个要好好思考,下图中可能是左吗?不可能,想想二叉搜索树的性质!
还有个要注意的是,下面这种情况就找不到父节点,此时的根节点就是要删除的节点,这个好办,将根节点转移,删除旧的根节点即可。
- 删除节点有两个孩子节点(第4种情况):
方法一:找左子树的最大节点(7) --> 将最大节点(7)和要删除的节点进行交换 --> 删除的位置转移到了蓝色圆圈 --> 让其父节点连接它的左即可 (它的右一定是空,左不一定为空)
方法二:找右子树的最小节点(10) --> 将最小节点(10)和要删除的节点进行交换 --> 删除的位置转移到了蓝色圆圈 --> 让其父节点连接它的右即可 (它的左一定是空,右不一定为空)
递归法
方法一:使用上一层栈帧的节点来接受递归返回的子树根节点。
TreeNode* deleteNode(TreeNode* root, int key)
{
if(root == nullptr) return nullptr;
// 注意递归返回值一定是当前子树的根节点, 这个根节点由上一层接受
// 递归往左走就由上一层的left接受, 递归往右走就由上一层的right接受
if(root->val > key) // 当前值比目标值要大, 继续递归左子树寻找目标节点
root->left = deleteNode(root->left, key);
else if(root->val < key) // 当前值比目标值要小, 继续递归右子树寻找目标节点
root->right = deleteNode(root->right, key);
else // 当前值 = 目标值, 删除目标节点
{
// 包括叶子结点 + 左为空, 右不为空(两种情况合并)
if(root->left == nullptr)
return root->right;
// 左不为空, 右为空
else if(root->right == nullptr)
return root->left;
// 左右均不为空
else
{
// 先找到右子树的最小节点
TreeNode* MinRight = root->right;
while(MinRight->left) MinRight = MinRight->left;
// 将MinRight的值和root的值进行替换
swap(root->val, MinRight->val);
// 不要直接返回MinRight->right,会出大问题!因为此时要删除的节点跑到右子树最左边去了
// , root离MinRight可能还隔着很多个节点, 要继续递归右子树, 找到下一次删除的位置
root->right = deleteNode(root->right, key);
}
}
return root;
}
方法二:形参传别名,别名就是上一层递归的左或者右,给别名的赋值就是间接的改变连接。
void traversal(TreeNode*& root, int key)
{
if(root == nullptr) return;
// 当前值比目标值要大, 继续递归左子树寻找目标节点
if(root->val > key) traversal(root->left, key);
// 当前值比目标值要小, 继续递归右子树寻找目标节点
else if(root->val < key) traversal(root->right, key);
// 当前值 = 目标值, 删除目标节点
else
{
// 包括叶子结点 + 左为空, 右不为空(两种情况合并)
// 赋值运算符左边的root是上一层中root->right的别名, 也就是将当前层root的right链接到上一层中的root->right
if(root->left == nullptr)
root = root->right;
// 左不为空, 右为空
else if(root->right == nullptr)
root = root->left;
// 左右均不为空
else
{
// 先找到右子树的最小节点
TreeNode* MinRight = root->right;
while(MinRight->left) MinRight = MinRight->left;
// 将MinRight的值和root的值进行替换
swap(root->val, MinRight->val);
traversal(root->right, key);
}
return;
}
}
TreeNode* deleteNode(TreeNode* root, int key) {
traversal(root, key);
return root;
}
时间复杂度: O(N)。因为是二叉搜索树的搜索,比较好的情况是logN,最坏的情况是单链表,为O(N)。
空间复杂度: O(N)。最坏的情况要建立N层栈帧,递归深度为N。
迭代法
迭代法考虑的问题稍微多一点,总的思路都一样,先找,后删。
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
TreeNode* cur = root, *prev = nullptr;
while(cur)
{
if(cur->val > key)
{
prev = cur;
cur = cur->left;
}
else if(cur->val < key)
{
prev = cur;
cur = cur->right;
}
else
{
// 包括叶子结点 + 左为空, 右不为空(两种情况合并)
if(cur->left == nullptr)
{
// 考虑要删除的 cur为根节点的情况
if(cur == root)
{
root = cur->right; // 根节点转移
}
else
{
if(cur == prev->left) prev->left = cur->right;
else if(cur == prev->right) prev->right = cur->right;
}
}
// 左不为空, 右为空
else if(cur->right == nullptr)
{
if(cur == root)
{
root = cur->left;
}
else
{
if(cur == prev->left) prev->left = cur->left;
else if(cur == prev->right) prev->right = cur->left;
}
}
// 左右均不为空
else
{
// 找右子树的最小节点
TreeNode* MinRTree = cur->right; // 右子树的最小节点
TreeNode* PMinRTree = cur; // 右子树的最小节点的父节点
while(MinRTree->left)
{
PMinRTree = MinRTree;
MinRTree = MinRTree->left;
}// 循环出来,MinRTree就是最小节点
// 将最小节点和cur的值进行替换, 并将PMinRTree指向MinRTree的右孩子节点
// 值得注意: MinRTree不一定是叶子结点啊!
cur->val = MinRTree->val;
// 删除右子树的最左结点
if(MinRTree == PMinRTree->left) PMinRTree->left = MinRTree->right;
if(MinRTree == PMinRTree->right) PMinRTree->right = MinRTree->right;
}
return root;
}
}
return root;
}
};
思考:为什么前两种情况要考虑删除节点为根节点的情况,而第三种不需要考虑?
因为前两种情况根节点删除之后就不存在了,需要新的节点去担任根节点;而第三种情况,原根节点还存在,只不过删除的节点转移到了右子树的最左节点!
时间复杂度: O(N)。因为是二叉搜索树的搜索,比较好的情况是logN,最坏的情况是单链表,为O(N)。
空间复杂度: O(1)。因为迭代只建立了有限个变量,开辟空间为常数。