修剪二叉搜索树
思路:
- 若
root->val < low
,root在区间外。去root的右子树去找有无区间内的,因为root的左子树只会比root->val
更小; - 若
root->val > high
,root在区间外。去root的左子树去找有无区间内的,因为root的右子树只会比root->val
更大;
1.递归法
误区:如下代码是错误解法
之前做过删除二叉搜索树中的节点这题之后,我们很容易能写出一个节点被删除,要将root->right
连接到要删除节点的父节点,这样我们就遗漏了后面要删除的节点,如下图。
TreeNode* trimBST(TreeNode* root, int low, int high) {
if(root == nullptr) return nullptr;
// 如果root的值小于low, 那么root包括root的左子树都不是我们要保留的
// 所以要将root的右子树的根节点递给上一层栈帧中的root->left
if(root->val < low)
return root->right;
if(root->val > high)
return root->left;
root->left = trimBST(root->left, low, high);
root->right = trimBST(root->right, low, high);
return root;
}
所以我们要在子树中继续去找,将整棵树的问题转化为子树的子问题,它们的操作逻辑是一样的。具体递归展开如下图:
从递归最深的节点开始向上回溯,2的左和2的右都接收到返回值之后,就意味着1的删除;然后2将它自己返回作为3的左的接收值,3的右接受空;再然后3将自己返回作为8的左的接收值,这意味着0的删除;右子树的逻辑也如此…
正确解法:
TreeNode* trimBST(TreeNode* root, int low, int high) {
if(root == nullptr) return nullptr;
// 如果root的值小于low, 那么root包括root的左子树都不是我们要保留的
// 所以要递归遍历root的右子树, 返回不会被修剪的子树根节点
if(root->val < low)
{
TreeNode* Right = trimBST(root->right, low, high);
return Right;
}
if(root->val > high)
{
TreeNode* Left = trimBST(root->left, low, high);
return Left;
}
// 上一层递归中的root要接受下一层的返回值
root->left = trimBST(root->left, low, high);
root->right = trimBST(root->right, low, high);
// 节点的左和右接受完毕,返回当前层的节点
return root;
}
时间复杂度:O(N)。最坏的情况是要遍历N个节点的。
空间复杂度:O(N)。最坏的情况递归调用的深度为N。
2.迭代法
如果使用迭代法,头结点一定要先处理,因为如果头结点需要删除的话,就连带着它的一颗子树也处理了,这样子我们再更新头结点,直到头结点不需要删除为止。这样我们后面考虑问题只需要从孩子节点开始考虑即可,运用双指针法,当cur需要删除的时候,将cur->left
或者cur->right
连接到prev
;
双指针如何更新呢?
如下拿左子树为例:
对于有删除的情况,prev
是不会变的,原节点要删除,它等待着接受新的节点,而cur
则要更新 (插一嘴:为什么往right
跑呢?因为left
只会比cur
还要小,而cur
已经小于low
了,往right
跑是因为右边的节点还不确定)。
对于没有删除的情况,prev占据cur之前的位置,cur往left
跑。(为什么这个又是往left
跑?因为此时的cur
没有删,也就是大于low
,我当然要找比cur
要小的节点才能知道是否需要删除啊,而比cur
要小的节点不就是在cur的左子树吗)
下面写出代码实例:
对于节点的删除传统的写法是这样的:如果当初cur
连在prev->left
,那么我的新节点也就要连在prev->left
;如果当初cur
连在prev->right
,那么我的新节点也就要连在prev->right
。这是二叉搜索树的性质决定的。
TreeNode* cur = root->left, *prev = root;
// 此时root已经在区间[low,high]中了, 接下来处理左孩子小于low的情况
while(cur)
{
if(cur->val < low)
{
// 删除操作
if(prev->left == cur) prev->left = cur->right;
if(prev->right == cur) prev->right = cur->right;
cur = cur->right;
}
else
{
prev = cur;
cur = cur->left;
}
}
其实我们不用写两个判断,因为我们确定cur
一定连接在prev->left
,因为我每次都是往左子树去找的。
整体代码如下:
TreeNode* trimBST(TreeNode* root, int low, int high)
{
// 先处理头结点
while(root != nullptr && (root->val < low || root->val > high))
{
if(root->val < low)
root = root->right; // root及其左子树都要删掉
else
root = root->left; // root及其右子树都要删掉
}
if(root == nullptr) return nullptr;
TreeNode* cur = root->left, *prev = root;
// 此时root已经在区间[low,high]中了, 接下来处理左孩子小于low的情况
while(cur)
{
if(cur->val < low)
{
// 删除操作
// 为什么是链接到prev的左, 因为我每次都是往左子树去找的
// 那么自然cur就链接着prev的左了
prev->left = cur->right;
cur = cur->right;
}
else
{
prev = cur;
cur = cur->left;
}
}
// 接下来处理右孩子大于high的情况
cur = root->right, prev = root;
while(cur)
{
if(cur->val > high)
{
// 删除操作
prev->right = cur->left;
cur = cur->left;
}
else
{
prev = cur;
cur = cur->right;
}
}
return root;
}
时间复杂度:O(N)。最坏的情况是要遍历N个节点的。
空间复杂度:O(1)。只创建了常数的变量。