二叉搜索树修剪(Trim a Binary Search Tree):leetcode1 仓库中的 DFS 与迭代解法全解析
【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode
本篇指南以 LeetCode 669(Trim a Binary Search Tree,二叉搜索树修剪)为核心,结合当前仓库 leetcode1/leetcode 中python、go、java、javascript、kotlin、typescript等多语言的 0669-trim-a-binary-search-tree 实现,系统讲解如何利用 BST 的有序性,将树中所有节点裁剪到[low, high]区间内。读完你将掌握递归 DFS、显式栈迭代、双线性扫描三种解法,并理解它们的正确性依据、复杂度差异与常见陷阱。
前置知识
开始前,需要熟悉以下基础:
- 二叉搜索树(BST)性质:左子树所有节点值小于根节点,右子树所有节点值大于根节点。这一有序性决定了"修剪时往哪个方向走"的决策依据——当根节点超界时,可以一次性丢弃整棵子树而无需逐个检查。
- 递归与 DFS:能够以递归方式遍历树,并在遍历过程中重建树结构(先裁剪子树、再挂接结果)。
- 树节点的指针操作:通过重新赋值
left/right指针修改父子关系,例如把越界的左孩子替换为其右孩子。
仓库中多语言实现统一使用 LeetCode 标准TreeNode定义,例如 python/0669-trim-a-binary-search-tree.py:
# Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right1. 深度优先搜索(递归 DFS)
核心直觉
BST 性质给出了一个非常强力的剪枝策略:
- 若当前节点值大于
high,那么该节点及其整棵右子树都过大,应当整体丢弃,只保留并返回修剪后的左子树; - 若当前节点值小于
low,那么该节点及其整棵左子树都过小,只保留并返回修剪后的右子树; - 若节点值落在
[low, high]内,则递归修剪左右孩子,把结果重新挂回当前节点后返回。
算法步骤
- 当前节点为
null,返回null(递归基)。 - 节点值大于
high:返回对左子树递归修剪的结果(丢弃当前节点与右子树)。 - 节点值小于
low:返回对右子树递归修剪的结果(丢弃当前节点与左子树)。 - 否则节点在区间内:递归修剪左右孩子,重新挂接后返回该节点。
多语言实现
Python(与仓库实现一致):
class Solution: def trimBST(self, root: Optional[TreeNode], low: int, high: int) -> Optional[TreeNode]: if not root: return None if root.val > high: return self.trimBST(root.left, low, high) if root.val < low: return self.trimBST(root.right, low, high) root.left = self.trimBST(root.left, low, high) root.right = self.trimBST(root.right, low, high) return rootGo(见 go/0669-trim-a-binary-search-tree.go):
func trimBST(root *TreeNode, low int, high int) *TreeNode { if root == nil { return nil } if root.Val > high { return trimBST(root.Left, low, high) } if root.Val < low { return trimBST(root.Right, low, high) } root.Left = trimBST(root.Left, low, high) root.Right = trimBST(root.Right, low, high) return root }JavaScript(见 javascript/0669-trim-a-binary-search-tree.js):
var trimBST = function (root, low, high) { if (!root) { return null; } if (root.val < low) { return trimBST(root.right, low, high); } if (root.val > high) { return trimBST(root.left, low, high); } root.left = trimBST(root.left, low, high); root.right = trimBST(root.right, low, high); return root; };Java(见 java/0669-trim-a-binary-search-tree.java):
public class Solution { public TreeNode trimBST(TreeNode root, int low, int high) { if (root == null) { return null; } if (root.val > high) { return trimBST(root.left, low, high); } if (root.val < low) { return trimBST(root.right, low, high); } root.left = trimBST(root.left, low, high); root.right = trimBST(root.right, low, high); return root; } }C++:
class Solution { public: TreeNode* trimBST(TreeNode* root, int low, int high) { if (!root) return nullptr; if (root->val > high) { return trimBST(root->left, low, high); } if (root->val < low) { return trimBST(root->right, low, high); } root->left = trimBST(root->left, low, high); root->right = trimBST(root->right, low, high); return root; } };Kotlin、Swift、Rust 的实现与上述逻辑完全同构(注意 Rust 版需通过Rc<RefCell<TreeNode>>借用检查,先取left/right的克隆再写回),见仓库对应的 kotlin/0669-trim-a-binary-search-tree.kt 与 typescript/0669-trim-a-binary-search-tree.ts。
复杂度
- 时间复杂度:$O(n)$,每个节点最多被访问一次。
- 空间复杂度:$O(n)$,最坏情况下(如链状树)递归栈深度为 $n$。
2. 迭代 DFS(显式栈)
核心直觉
递归可以借助显式栈改写为迭代。整体思路不变:越界节点需要被其合法的孩子替换。与递归"自顶向下一次性返回结果"不同,迭代法先找到合法的根节点,再借助栈逐层修复越界孩子——通过把越界孩子替换为其合适的孙节点来"跳级"接续。
算法步骤
- 寻找合法根:若当前根过小则走向右孩子,过大则走向左孩子,直到根落在
[low, high]内。 - 用该合法根初始化栈。
- 当栈非空时循环:
- 弹出节点
node; - 若左孩子存在且值小于
low,将其替换为左孩子的右孩子; - 若右孩子存在且值大于
high,将其替换为右孩子的左孩子; - 若发生了替换,把
node重新压栈(因为替换上来的孙节点可能仍越界,需要再次检查); - 否则把左右孩子(若存在)压栈继续处理。
- 弹出节点
- 返回合法根。
多语言实现
Python:
class Solution: def trimBST(self, root, low, high): while root and (root.val < low or root.val > high): if root.val < low: root = root.right else: root = root.left stack = [root] while stack: node = stack.pop() if not node: continue left_out = node.left and node.left.val < low right_out = node.right and node.right.val > high if left_out: node.left = node.left.right if right_out: node.right = node.right.left if left_out or right_out: stack.append(node) else: if node.left: stack.append(node.left) if node.right: stack.append(node.right) return rootJava:
public class Solution { public TreeNode trimBST(TreeNode root, int low, int high) { while (root != null && (root.val < low || root.val > high)) { root = (root.val < low) ? root.right : root.left; } Stack<TreeNode> stack = new Stack<>(); stack.push(root); while (!stack.isEmpty()) { TreeNode node = stack.pop(); if (node == null) continue; boolean leftOut = (node.left != null && node.left.val < low); boolean rightOut = (node.right != null && node.right.val > high); if (leftOut) node.left = node.left.right; if (rightOut) node.right = node.right.left; if (leftOut || rightOut) { stack.push(node); } else { if (node.left != null) stack.push(node.left); if (node.right != null) stack.push(node.right); } } return root; } }C++:
class Solution { public: TreeNode* trimBST(TreeNode* root, int low, int high) { while (root && (root->val < low || root->val > high)) { root = (root->val < low) ? root->right : root->left; } stack<TreeNode*> stack; stack.push(root); while (!stack.empty()) { TreeNode* node = stack.top(); stack.pop(); if (!node) continue; bool leftOut = (node->left && node->left->val < low); bool rightOut = (node->right && node->right->val > high); if (leftOut) node->left = node->left->right; if (rightOut) node->right = node->right->left; if (leftOut || rightOut) { stack.push(node); } else { if (node->left) stack.push(node->left); if (node->right) stack.push(node->right); } } return root; } };JavaScript、Go、Kotlin、Swift、Rust 版本结构一致,其中 Rust 版用Vec<Option<Rc<RefCell<TreeNode>>>>作为栈,并在替换越界孩子时先clone()出孙节点引用再写回,避免借用冲突。
复杂度
- 时间复杂度:$O(n)$。
- 空间复杂度:$O(n)$(栈空间)。
3. 迭代 DFS(最优版:双线性扫描)
核心直觉
显式栈仍然需要 $O(n)$ 的辅助空间。利用 BST 的偏序特性,可以进一步去掉栈:找到合法根之后,沿左脊线向下修复所有小于low的节点,再沿右脊线向下修复所有大于high的节点。原因在于 BST 中一旦在某一侧修复了一个节点,只需要继续沿同一方向检查即可——左子树中不可能存在"需要修右边界"的节点(所有左子树节点都更小),反之亦然。
算法步骤
- 跳过越界节点,找到合法的根。
- 用
tmpRoot保存该合法根。 - 左脊线:只要左孩子存在且值小于
low,就把它替换为左孩子的右孩子;随后下移到新的左孩子,重复直到左孩子合法或为空。 - 回到
tmpRoot,右脊线:只要右孩子存在且值大于high,就把它替换为右孩子的左孩子;随后下移到新的右孩子,重复。 - 返回
tmpRoot。
多语言实现
Python:
class Solution: def trimBST(self, root: Optional[TreeNode], low: int, high: int) -> Optional[TreeNode]: while root and (root.val < low or root.val > high): root = root.right if root.val < low else root.left tmpRoot = root while root: while root.left and root.left.val < low: root.left = root.left.right root = root.left root = tmpRoot while root: while root.right and root.right.val > high: root.right = root.right.left root = root.right return tmpRootJava:
public class Solution { public TreeNode trimBST(TreeNode root, int low, int high) { while (root != null && (root.val < low || root.val > high)) { root = (root.val < low) ? root.right : root.left; } TreeNode tmpRoot = root; while (root != null) { while (root.left != null && root.left.val < low) { root.left = root.left.right; } root = root.left; } root = tmpRoot; while (root != null) { while (root.right != null && root.right.val > high) { root.right = root.right.left; } root = root.right; } return tmpRoot; } }C++:
class Solution { public: TreeNode* trimBST(TreeNode* root, int low, int high) { while (root && (root->val < low || root->val > high)) { root = (root->val < low) ? root->right : root->left; } TreeNode* tmpRoot = root; while (root) { while (root->left && root->left->val < low) { root->left = root->left->right; } root = root->left; } root = tmpRoot; while (root) { while (root->right && root->right->val > high) { root->right = root->right->left; } root = root->right; } return tmpRoot; } };JavaScript、Go、Kotlin、Swift、Rust 版本逻辑相同;Rust 版通过loop { ... match ... _ => break }模拟内层 while 循环,并配合borrow()/borrow_mut()交替读写节点字段。
复杂度
- 时间复杂度:$O(n)$。
- 空间复杂度:$O(1)$ 额外空间(不使用递归栈或显式栈),是三种写法中最省内存的。
常见陷阱
陷阱一:跳过越界节点后忘记继续递归修剪
当某节点值越界、跳到其合法方向的孩子时,必须继续对该孩子子树做修剪。常见错误是直接返回该孩子而不递归处理——这个孩子本身或其子孙仍可能越界。例如递归版中return self.trimBST(root.left, low, high)必须带上前缀递归调用,而非return root.left。
陷阱二:混淆越界时应保留哪一侧子树
- 节点值大于
high:应返回修剪后的左子树(不是右子树); - 节点值小于
low:应返回修剪后的右子树(不是左子树)。
把这两者搞反会违反 BST 性质,得到错误结果。判断依据很简单:low是下界,小于low的节点连同其左子树(全部更小)都应被舍弃,唯一可能合法的是右子树;high是上界,同理。
陷阱三:未处理根节点本身越界的情况
根节点可能不在[low, high]区间内,此时整个原始根都会被丢弃,需要先在子树中找到新的合法根。常见疏漏是假设根一定合法。无论递归还是迭代解法,都必须先处理"根越界":递归版通过root.val > high/root.val < low两个分支自然下沉;迭代版则在主循环之前用 while 循环跳到合法根(见第 2、3 节的步骤 1)。
总结
| 解法 | 思路 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|---|
| 递归 DFS | 后序重建,越界即丢整侧子树 | $O(n)$ | $O(n)$(递归栈) | 最直观、面试首选 |
| 迭代 DFS(栈) | 先找合法根,再逐层替换越界孩子 | $O(n)$ | $O(n)$(显式栈) | 避免递归深度限制 |
| 迭代 DFS(双脊线) | 左脊修下界、右脊修上界 | $O(n)$ | $O(1)$ | 追求常数辅助空间 |
三种解法的正确性都建立在 BST"左小右大"的性质上:越界时整棵子树可以直接丢弃,使得修剪只需沿少数路径下沉,而非遍历所有节点。仓库中的 0669-trim-a-binary-search-tree 系列实现覆盖 Python、Go、Java、JavaScript、Kotlin、TypeScript 六种语言,逻辑与本文讲解的递归 DFS 完全一致,可作为复习或对照调试的参考。递归版最适合快速 AC,最优迭代版则适合在空间受限场景或需要展示工程化能力时使用。
【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考