450. 删除二叉搜索树中的节点

题目描述:

  给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

提示:

  • 节点数的范围 [0, 104].
  • -105 <= Node.val <= 105
  • 节点值唯一
  • root 是合法的二叉搜索树
  • -105 <= key <= 105

 

解题思路:

  拿到题目我的第一想法是:

  1.   找到要删除的节点;
  2.   将要删除节点的所有子节点重新构成一棵二叉搜索树,然后将该树的根节点替代要删除的节点。

  要实现第二点,首先我要得到“要删除节点的所有子节点”,这里我考虑使用一个前序遍历来加入子节点。重构的树要满足二叉搜索树的性质,所以我需要对前序遍历按照小到大排序,以中点作为根节点,划分二叉搜索树的左右子树。

  重构完二叉搜索树之后,我们需要知道删除的节点是位于父节点的左子树还是右子树,因此通过比较重构二叉树的根节点与删除节点的父节点两者的val,来决定把重构二叉搜索树放在父节点的左边还是右边。

  考虑两种特殊情况:

  •   要删除节点是根节点,那么直接返回重构二叉树的根节点即可;
  •   要删除节点是叶子节点,那么直接删除该节点;

 

复制代码
var deleteNode = function(root, key) {
    let cur = root;
    let pre = null; //记录删除节点的父节点
    while(cur){
        if(cur.val == key){
            let arr = []; //使用数组来存储删除节点的后代节点
            search(cur.left,arr);
            search(cur.right,arr);
            arr.sort(function(a,b){return a-b;});//!!!尤其注意,不能直接写arr.sort()
            let t = myBuild(arr,0,arr.length-1);
            if(pre){
                if(t==null){   //表示要删除节点是叶子节点
                    cur.val<pre.val?pre.left=null:pre.right=null;//这里我们不能直接用cur=null来删除,因为cur是一个引用,这样做只是表示将它变为一个空指针
                }else{
                    t.val<pre.val?pre.left=t:pre.right=t;
                }
                return root;
            }else{ //若pre==null,表示要删除节点是根节点
                return t;
            }
        }else if(cur.val>key){
            pre = cur;
            cur = cur.left;
        }else{
            pre = cur;
            cur = cur.right;
        }
    }
    return root;
};

function search(root,arr){
    if(!root){
        return;
    }
    arr.push(root.val);
    search(root.left,arr);
    search(root.right,arr);
    return ;
};

function myBuild(arr,start,end){ //用于重构二叉搜索树
    if(start>end){
        return null;
    }
    let mid = Math.floor((end-start)/2)+start;
    let root = new TreeNode(arr[mid]);
    root.left = myBuild(arr,start,mid-1);
    root.right = myBuild(arr,mid+1,end); 
    return root;
}
复制代码

  尤其注意高亮的排序代码,js的Array.sort()是默认按字母表顺序排序的,比如:[3,22,111],排序后会变成[111,22,3]。因此一定要把比较函数添加进去,本人就是在这上面卡了十几分钟,百思不得其解!!!!!!!!!!!最后跑出来的发现时间复杂度和空间复杂度都不太理想,毕竟是完全按照最初思路来的,没有任何技巧。后面有官方题解的思路。

 

官方题解:

  

  • 二叉搜索树的中序遍历的序列是递增排序的序列。中序遍历的遍历次序:Left -> Node -> Right

 

 

 

  • Successor 代表的是中序遍历序列的下一个节点。即比当前节点大的最小节点,简称后继节点先取当前节点的右节点,然后一直取该节点的左节点,直到左节点为空,则最后指向的节点为后继节点。
  • Predecessor 代表的是中序遍历序列的前一个节点。即比当前节点小的最大节点,简称前驱节点先取当前节点的左节点,然后取该节点的右节点,直到右节点为空,则最后指向的节点为前驱节点。

  

 

 

   删除某节点后,必然要重构二叉搜索树,而最简单的方法就是将删除节点替换为前驱节点或者后继节点。因为作为前驱节点,它是要删除节点左子树中最大的节点,替换到根节点必然满足“左子树节点都比它小”;而作为后继节点,它是要删除节点右子树最小的节点,替换到根节点必然也满足“右子树节点都比它大”。替换完之后,再使用递归,在左子树删除前驱节点,或在右子树删除后继节点即可。

  因此可以根据要删除节点的结构分为以下情况:

  1.   要删除节点是叶子节点,直接删除;
  2.   要删除节点没有左子树,只能找后继节点来替换;
  3.   要删除节点没有右子树,只能找前驱节点来替换;
  4.   要删除节点左右子树都有,前驱节点和后继节点任选一个;(可以合并到2或3)

  

复制代码
var deleteNode = function(root, key) {
    if(!root){
        return root;
    }
    if(key<root.val){ 
        root.left =  deleteNode(root.left,key); //这样就把问题划分成子问题,在左子树里找,返回的是重构后左子树根节点
    }else if(key>root.val){
        root.right =  deleteNode(root.right,key);
    }else{
        if(root.left==null&&root.right==null){//叶子节点直接删除,这里的root已经是子树里的根了
            root = null; //这里可以直接用root=null来删除,因为最后root是作为返回值返回到某节点的left或right指针里的
        }else if(root.left == null){
            root.val = successor(root);
            root.right = deleteNode(root.right,root.val);//替换后,再去右子树里删除后继节点
        }else{
            root.val = predecessor(root);
            root.left = deleteNode(root.left,root.val);
        }
    }
    return root;
};

function successor(root){ //寻找root的后继节点
    root = root.right; //后继节点肯定在右子树里找
    while(root.left){
        root = root.left;
    }
    return root.val; //返回的是后继节点的值
};
function predecessor(root){//寻找root的前驱节点
    root = root.left; //前驱节点肯定在左子树里找
    while(root.right){
        root = root.right;
    }
    return root.val;
}
复制代码

 

参考某大佬的一种更通俗易懂的方法:

  在二叉搜索树中,一个节点的右子树所有节点必然比左子树所有节点要大。在删除节点之前,该节点的左子树和右子树已经满足二叉搜索树的性质了,删除节点后,只要把左子树的根节点并入到要删除节点的后继节点的Left上,那么重构的这棵二叉搜索树仍然满足它要求的性质。因此分为以下情况:

  •   要删除节点是叶子节点,直接删除;
  •   要删除节点只有左子树或右子树,将它并入到删除节点的父节点的Left或Right上;
  •   要删除节点左子树右子树都有,先将左子树并入到右子树中,再将合并后的树并入到删除节点的父节点的Left或Right上;

 

复制代码
var deleteNode = function(root, key) {
    if(!root){
        return root;
    }
    if(key<root.val){
        root.left = deleteNode(root.left,key);
    }else if(key>root.val){
        root.right = deleteNode(root.right,key);
    }else{
        if(root.left==null&&root.right==null){
            root = null;
        }else if(root.left == null){
            root = root.right;
        }else if(root.right==null){
            root = root.left;
        }else{
            let s = successor(root);
            s.left = root.left;
            root = root.right;
        }
        return root;
    }
    return root;
};

function successor(root){
    root = root.right; 
    while(root.left){
        root = root.left;
    }
    return root; 
};
复制代码

 

posted @   ˙鲨鱼辣椒ゝ  阅读(51)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
点击右上角即可分享
微信分享提示