450. 删除二叉搜索树中的节点
题目描述:
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
提示:
- 节点数的范围 [0, 104].
- -105 <= Node.val <= 105
- 节点值唯一
- root 是合法的二叉搜索树
- -105 <= key <= 105
解题思路:
拿到题目我的第一想法是:
- 找到要删除的节点;
- 将要删除节点的所有子节点重新构成一棵二叉搜索树,然后将该树的根节点替代要删除的节点。
要实现第二点,首先我要得到“要删除节点的所有子节点”,这里我考虑使用一个前序遍历来加入子节点。重构的树要满足二叉搜索树的性质,所以我需要对前序遍历按照小到大排序,以中点作为根节点,划分二叉搜索树的左右子树。
重构完二叉搜索树之后,我们需要知道删除的节点是位于父节点的左子树还是右子树,因此通过比较重构二叉树的根节点与删除节点的父节点两者的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 代表的是中序遍历序列的前一个节点。即比当前节点小的最大节点,简称前驱节点。先取当前节点的左节点,然后取该节点的右节点,直到右节点为空,则最后指向的节点为前驱节点。
删除某节点后,必然要重构二叉搜索树,而最简单的方法就是将删除节点替换为前驱节点或者后继节点。因为作为前驱节点,它是要删除节点左子树中最大的节点,替换到根节点必然满足“左子树节点都比它小”;而作为后继节点,它是要删除节点右子树最小的节点,替换到根节点必然也满足“右子树节点都比它大”。替换完之后,再使用递归,在左子树删除前驱节点,或在右子树删除后继节点即可。
因此可以根据要删除节点的结构分为以下情况:
- 要删除节点是叶子节点,直接删除;
- 要删除节点没有左子树,只能找后继节点来替换;
- 要删除节点没有右子树,只能找前驱节点来替换;
- 要删除节点左右子树都有,前驱节点和后继节点任选一个;(可以合并到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; };
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 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)