LeetCode 450:删除二叉搜索树节点的递归与迭代详解

发布时间:2026/9/14 10:38:41
LeetCode 450:删除二叉搜索树节点的递归与迭代详解 1. 题目拆解与前置知识1.1 第450题究竟在考什么LeetCode第450题“删除二叉搜索树中的节点”表面看是一道“删除节点”的题目但实际考察的是一个非常核心的算法能力在动态维护数据结构时如何保证原有性质不被破坏。这和工程中的“删除缓存后如何保证一致性”“数据库删记录后如何维护索引”是同一个逻辑模型理解了这道题很多生产场景里的树形结构问题都能触类旁通。题目要求给定一个二叉搜索树BST的根节点和一个目标值 key删除树中键值等于 key 的节点并保证删除后的树依然是二叉搜索树。返回值可以是根节点也就是说可以在原树上就地修改也可以重新组织节点结构。大多数人在第一次接触这道题时的反应是找到节点很简单麻烦的是删完以后怎么办。这就是这道题真正的分水岭。删除一个叶子节点直接置空即可删除只有一个子树的节点把子树提上来接住就行但删除一个同时拥有左右子树的节点时必须找到替代者否则整棵树的搜索结构就会乱掉。1.2 先吃透二叉搜索树的三个铁律在动手写代码之前有必要把二叉搜索树的性质再夯实一遍。很多题解写得晦涩难懂根子上是因为没有把这三个性质内化对于任意节点它的左子树中所有节点的值都小于该节点的值。对于任意节点它的右子树中所有节点的值都大于该节点的值。左右子树本身也必须各是一棵二叉搜索树。有了这三个铁律删除后的“补位”就有思路了不管用什么策略只要保证替换节点值的那个位置仍然满足左小右大的关系即可。第450题的标准解法里最经典的替代者选择有两个方向——左子树中的最大值节点或右子树中的最小值节点。二者都能维持BST性质只是代码实现的细节略有不同。1.3 为什么说第450题是“必刷题”如果你在刷LeetCode热门100题会发现450题并不是传统意义上的高频热门题但它在面试中的出现频率并不低。原因很简单它能高效检验一个人是否真正理解递归、BST性质以及树状结构的状态维护。很多公司面试算法题时会从热门100题里抽取原题也会在二面三面中改用这道题做变体比如“删除后返回新的根节点并且输出中序遍历序列”或者“不递归用迭代方式实现同样的功能”。从知识地图的角度看450题处在“二叉树”和“高级搜索结构”的交界地带。做完这道题你会自然引申出对红黑树、AVL树、跳表平衡逻辑的兴趣也会对BST的插入、查找、删除整套操作有体系化的认识。如果你目前正处在刷题迷茫期我建议把这道题的三种写法递归、迭代、带父节点指针全部熟悉一遍收益很大。2. 递归解法的完整推导与代码实现2.1 递归第一步找到目标节点并分析三种情况递归法删除BST节点核心逻辑分三步先找、再删、最后返回新的根节点。“先找”这一步充分利用BST的有序性——目标值比当前节点小就去左子树找比当前节点大就去右子树找等于当前节点时执行删除逻辑。删除逻辑要处理的情况可以拆成三类情况A目标节点是叶子节点。直接返回 null 给上层相当于把父节点原本指向它的引用断开。情况B目标节点只有左子树或只有右子树。返回它的左孩子或右孩子让父节点直接跨过它指向下一个有效节点。情况C目标节点同时有左右子树。这是最核心也最容易出错的情况标准做法是找到左子树里的最大值节点或右子树里的最小值节点用这个节点的值覆盖目标节点的值然后递归地删除那个用来覆盖的节点。这里面最关键的理解是情况C的本质是把“删除当前节点”转化为“删除另一个更容易删除的节点”。那个更容易删除的节点要么是左子树的最右节点要么是右子树的最左节点它最多只有一个孩子所以删除它只会落到情况A或情况B复杂度大大降低。2.2 递归代码实现Java版先给出一个最常用、面试中最好写的版本语言选Java因为企业面试用Java的比例较高。核心思路取右子树最小节点作为继承者。class Solution { public TreeNode deleteNode(TreeNode root, int key) { if (root null) { return null; } // 先去左右子树找目标节点 if (key root.val) { root.left deleteNode(root.left, key); } else if (key root.val) { root.right deleteNode(root.right, key); } else { // 找到了目标节点执行删除逻辑 // 情况A叶子节点直接返回null if (root.left null root.right null) { return null; } // 情况B只有右子树 if (root.left null) { return root.right; } // 情况B只有左子树 if (root.right null) { return root.left; } // 情况C左右子树都在找右子树的最小节点 TreeNode minNode findMin(root.right); root.val minNode.val; // 关键步骤递归删除右子树里的那个最小节点 root.right deleteNode(root.right, minNode.val); } return root; } private TreeNode findMin(TreeNode node) { while (node.left ! null) { node node.left; } return node; } }这段代码的逻辑顺序很有讲究。每次递归返回的都是“当前子树在删除操作之后的新根节点”这样父节点在回溯时只需要把返回值接住即可。不会出现“删完节点后不知道如何连接”的窘境这也是递归解BST题目的通用套路。从时空复杂度来看最坏情况是链路型BST递归深度为O(n)平均情况是O(log n)。空间复杂度主要由递归栈深度决定同样是最坏O(n)、平均O(log n)。2.3 Python版本对照有的读者习惯用Python刷题尤其是对LeetCode周赛430这类需要在短时间内快速写出的场景Python的代码通常更短但要注意递归深度限制。class Solution: def deleteNode(self, root: TreeNode, key: int) - TreeNode: if not root: return None if key root.val: root.left self.deleteNode(root.left, key) elif key root.val: root.right self.deleteNode(root.right, key) else: if not root.left and not root.right: return None if not root.left: return root.right if not root.right: return root.left # 找右子树的最小节点 min_node self.findMin(root.right) root.val min_node.val root.right self.deleteNode(root.right, min_node.val) return root def findMin(self, node: TreeNode) - TreeNode: while node.left: node node.left return node这段代码和Java版完全同构核心逻辑没有任何转换成本。反复训练这个版本的价值在于你把“递归删除”的编程范式内化后再去看二叉树的插入、搜索、验证合法性会觉得所有操作都是同一个套路代码的稳定性会显著提升。3. 迭代法挑战进阶与工程考量3.1 为什么工程师要刻意练迭代法递归实现简洁优美但有两个实际问题一是当BST高度较大时递归调用栈有爆栈风险二是很多面试官会现场抛出一个追加问题“能不能用迭代实现”如果只能写递归很容易被看穿只是背了模板而没有真正理解树的结构变化。迭代法的本质是模拟递归过程同时自己维护一个指针来遍历树。删除操作最麻烦的地方在于你需要同时记录“当前节点”和“当前节点的父节点”否则删除节点后无法修改父节点指向。我在实际写迭代版时踩过不少坑核心就是父节点引用处理不当导致树断裂或者丢失整棵子树。3.2 迭代法完整代码实现下面给出一个经过多次验证的迭代实现覆盖了删除节点时可能出现的全部情况。为了保证代码可读性我把“找右子树最小节点”做成了独立函数。class Solution { public TreeNode deleteNode(TreeNode root, int key) { if (root null) { return null; } TreeNode cur root; TreeNode parent null; // 第一阶段找到目标节点及其父节点 while (cur ! null cur.val ! key) { parent cur; if (key cur.val) { cur cur.left; } else { cur cur.right; } } // 没找到目标节点直接返回原树 if (cur null) { return root; } // 第二阶段分类处理 // 情况A和B目标节点最多只有一个孩子 if (cur.left null || cur.right null) { TreeNode child (cur.left ! null) ? cur.left : cur.right; // 如果删除的是根节点 if (parent null) { return child; } if (cur parent.left) { parent.left child; } else { parent.right child; } } else { // 情况C左右子树都在 // 找到右子树最小节点及其父节点 TreeNode minParent cur; TreeNode minNode cur.right; while (minNode.left ! null) { minParent minNode; minNode minNode.left; } // 把值覆盖过来 cur.val minNode.val; // 删除这个最小节点相当于把最小节点的右子树接给它的父节点 if (minParent cur) { // 特殊情况最小节点的父节点就是目标节点 cur.right minNode.right; } else { minParent.left minNode.right; } } return root; } }这段代码里最容易出问题的地方是“最小节点的父节点就是目标节点”的情况。比如一棵树只有根节点和右子树根节点的值为5右子树只有一个节点6那么右子树的最小节点就是6它的父节点就是目标节点5。此时不能再用“把最小节点从父节点摘除”的惯性思维否则会把整个右子树置空。我在初学时曾在这里卡了很久如果你在LeetCode上反复报错大概率也是这个分支的问题。3.3 迭代法与递归法的对比从生产选型的角度迭代法和递归法各有优劣不是说会一种就万事大吉。对比维度如下对比项目递归法迭代法代码可读性高逻辑清晰中需维护parent指针空间复杂度递归栈O(h)O(1)额外空间爆栈风险树高较大时存在无面试友好度高容易表达思路高能体现底层理解容易出错点返回值拼接父节点引用与边界分支如果只是应付LeetCode题解和笔试建议优先掌握递归法因为代码量少不容易出错。但如果是为了大厂面试和实际工程里的树结构操作迭代法必须会写。有经验的面试官很容易从你的代码里看出你有没有写过真正的底层数据结构。4. 细节复盘复杂度、边界条件与隐藏陷阱4.1 复杂度分析不是可有可无的结论每次刷完一道题如果直接下一题那刷10题和刷100题的区别不大。第450题的两个解法复杂度分析都要能脱口而出。递归法和迭代法的时间复杂度都是O(h)其中h是树的高度。在最坏情况下比如退化成链表的BSTh等于n在平衡的BST里h等于log n。这也就解释了为什么工业界要引入AVL树、红黑树做自平衡——平衡后的删除操作才能稳定在对数级别。空间复杂度上递归法使用的是系统调用栈最坏O(n)平均O(log n)迭代法只用了有限几个变量所以额外空间是O(1)。但需要注意迭代法的O(1)只指“额外空间”如果题目要求返回的新树另算存储那肯定还是要O(n)去存节点。4.2 边界条件清单建议背诵我发现很多人在LeetCode上提交450题时第一次容易挂在这些用例上树为空root null应该返回null而不是报错。目标值不存在于树中应该返回原树根节点。删除的节点是根节点且它没有左子树只有右子树。此时新的根节点应该是原右子树的根。删除的节点是根节点且左右子树都存在。此时需要找到合适的替代者并且要处理好替代者本身的位置。树中所有节点的值都不重复但可能存在值为负数的情况。比较时用直接相等判断即可。目标节点在极端位置比如树的最左下角或最右下角。这些边界条件如果你在写代码前有意识地列出来调试时间能减少一半。我自己的习惯是写完代码后立刻在草稿纸上按照这6条过一遍用例手动推演根节点的变化。4.3 那些年我踩过的几个坑第一个坑在情况C中只覆盖值但忘了删除替代节点。这样会导致树中出现重复节点值破坏BST性质。这个问题在LeetCode上不会直接报错但如果你用中序遍历去验证会发现输出里多了一个重复节点树形结构也不再合法。第二个坑在递归法中将根节点的引用直接赋值给另一个变量然后修改这个变量的左右指针导致原根的引用丢失。很多初学者会误以为TreeNode对象传引用后就可以随意修改但如果不把递归返回值接回来上层连接就断了。这是递归题的通病不只是450题。第三个坑在迭代法中没有区分“要删除的节点是父节点的左孩子还是右孩子”。有些人直接用parent.left child强行接上在一半用例里能过另一半就会挂。写这类代码时必须用if-else处理两种方位。第四个坑Python解法不设置递归深度。在LeetCode平台测试时递归深度通常足够但如果你在本机跑极端用例比如一条链5000层Python默认递归深度只有1000会直接RecursionError。建议在本地调试时设置sys.setrecursionlimit(100000)或者直接用迭代版本验证。4.4 如何用中序遍历验证删除是否成功这是个很实用的小技巧。二叉搜索树的中序遍历结果一定是升序序列。删除一个节点后再用中序遍历输出整棵树的节点值就可以立刻检测BST性质是否被破坏。我的习惯流程是用数组或列表保存中序遍历结果。检查结果是否严格递增。在结果里确认节点值key已经不存在。检查是否失去了某个本不该丢失的子树节点数量。这个验证方法虽然多花几分钟但对初学者理解树结构非常有帮助。在LeetCode上提交前先用本地测试函数跑一遍能避免很多低级错误。5. 从第450题出发高频变体与刷题路线5.1 基于450题延伸的四道高频变体把450题吃透后有几道关联度极高的题目一定要连在一起刷它们共享同一套知识体系LeetCode 700BST中的搜索这是二叉搜索树操作的基础和删除的第一步“定位节点”完全相同属于热身题。LeetCode 701BST中的插入插入操作的核心也是找位置只不过比删除简单没有“替代者”这层逻辑。先插后删能形成完整的操作闭环。LeetCode 98验证二叉搜索树这道题要求在树上做中序遍历然后判断是否严格递增。450题删除后的合法性验证就依赖这个思路。LeetCode 230BST中第K小的元素考察对BST中序遍历深入理解本质上是用树的排名特性解决顺序统计量问题。把这四道题和450题连起来刷相当于把BST的查、改、增、验证全部过了一遍。这样的组合刷法远比按题号顺序依次刷效率高。5.2 关于“LeetCode旅行商”与周赛430的启示最近在LeetCode相关热词里看到一个很有意思的词LeetCode旅行商。这其实不是指旅行商问题TSP本身而是社区里对一种刷题状态的调侃——像旅行商一样在各个知识点之间来回奔波却始终没有形成体系。这恰恰是很多刷题者的真实困境题刷了不少但知识点是散的没有串成线。如果你去看LeetCode周赛430的题目设置会发现它的题目分布依然遵循“简单到困难递进”的原则。做过几次周赛的人应该能感觉到周赛前三题通常考察的是基础数据结构和经典算法最后一题才可能上动态规划或较复杂的组合逻辑。BST相关的问题出现概率并不低尤其是删除、插入这类“对结构做动态修改”的题目是周赛和面试都很喜欢出的题型。所以我的建议是与其漫无目的地刷不如以450题为锚点建立自己的知识小地图。每做完一道题就问自己三个问题这道题用了哪个核心数据结构能拆出哪些复用性强的操作能不能改写成其他语言或另一种实现方式如果三个问题都能回答清楚这道题才真正属于你了。5.3 值得一试的“旅行商”式刷题清单这里分享一个我自己用过的刷题清单适合在一个周期内集中攻破“二叉树增删改查”专项。按顺序刷知识点递进顺利不会出现跨度太大导致看不懂的情况LeetCode 144二叉树前序遍历LeetCode 94二叉树中序遍历LeetCode 145二叉树后序遍历LeetCode 700BST搜索LeetCode 701BST插入LeetCode 450BST删除LeetCode 98验证BSTLeetCode 230BST中第K小元素LeetCode 99恢复二叉搜索树这套清单里前3题是热身让你熟悉树遍历的递归和迭代两种写法中间3题是BST的增删查核心操作最后3题是用BST性质解决复杂问题。全套做下来你对树的理解会有一个质的飞跃尤其是遇到“树的递归和回溯”类题目时反应速度会明显变快。6. 实操笔记三天吃透BST删除问题的经验总结6.1 第一天动手推演而非直接看题解拿到450题第一件要做的事情不是打开题解而是拿出纸笔画一棵包含各种形状的BST树。我习惯画出一个根节点为10的树左右子树分别挂上各种情况的节点比如一个叶子节点、一个只有左孩子的节点、一个只有右孩子的节点、一个同时有左右孩子的节点。然后在树上手动模拟删除不同的目标值特别关注删除根节点和删除中间节点时树的形态变化。这种“离线推演”看起来慢实际上是在给大脑建立空间模型。做完两三棵树的推演之后再看代码你会发现每一行都有了画面感。很多人看题解能看懂但自己写就卡壳就是因为没有经历从“图形直觉”到“代码表达”的转化过程。第一天的目标就是补齐这部分。6.2 第二天一题三写递归、迭代、手动模拟第二天就进入代码训练阶段。建议同一道题写三遍第一遍写递归版本不限时允许查资料目标是保证AC。第二遍关掉资料纯手写递归版本目标是10分钟内写完并通过所有测试用例。第三遍写迭代版本允许看自己的第一遍代码思路但要独立把父节点指针的逻辑理清。这个“一题三写”的方法残酷但有效。尤其是迭代版本写一遍往往不够如果时间充裕建议隔一天再复盘一次重点看minParent cur这个分支是否还记得怎么处理。如果这个分支能凭记忆写完说明这道题已经进入长期记忆了。6.3 第三天把删除操作模块化迁移到新题目第三天最有价值的一件事是跨题应用。把450题的删除操作封装成一个独立函数然后试着在另一道题里调用它。比如LeetCode 669修剪二叉搜索树就很适合修剪的操作和删除在思路上高度同源都是“根据条件调整树结构”。做完这道题你会发现450题的删除逻辑并不是孤立的而是一类“结构修剪”问题的解题模板。第三天还可以做一个进阶练习尝试把递归版改写成尾递归优化版本。Java本身不保证支持尾递归优化但这个练习能逼你思考递归调用的真正结构对理解JVM的栈帧会有帮助。我个人在实际操作中的体会是450题是最适合用来做“树结构入门到进阶”的桥梁题。它的难度卡在“会遍历”和“会设计算法”之间如果你能一次性AC它的递归版和迭代版说明你对二叉树的掌握已经到了一个可靠的层次。很多人在LeetCode刷题时会陷入“数量焦虑”但把一道经典题彻底吃透效果远胜于浅尝辄止地刷十道题。最后再分享一个小技巧你可以在LeetCode问题页面上方的“相关讨论”区找一些热门题解看别人对情况C的处理方式你会发现每个作者的风格差异很大但对BST性质的理解必须一致这正是这道题最微妙、也最能暴露水平的地方。