1. 问题背景与需求分析
今天想和大家分享一道我在LeetCode上刷到的经典二叉树题目——938. Range Sum of BST(二叉搜索树的范围和)。这道题看似简单,但想要写出高效解法却需要我们对二叉搜索树(BST)的性质有深刻理解。我在多次尝试后终于写出了耗时100%的解法,下面就来详细拆解这道题的解题思路和优化过程。
首先明确题目要求:给定一个二叉搜索树的根节点和两个整数low和high,我们需要返回树中所有节点值在[low, high]范围内的节点值之和。例如,对于BST [10,5,15,3,7,null,18],low=7,high=15,那么符合条件的节点是7、10、15,它们的和是32。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 二叉搜索树的核心性质回顾
2.1 BST的基本特性
二叉搜索树是一种特殊的二叉树,对于树中的每个节点:
- 其左子树所有节点的值都小于该节点的值
- 其右子树所有节点的值都大于该节点的值
- 左右子树也必须是二叉搜索树
这个性质决定了BST的中序遍历结果是一个升序序列,这也是解决本题的关键所在。
2.2 与普通二叉树的区别
相比普通二叉树,BST的这种有序性让我们可以做出更高效的搜索决策:
- 如果当前节点值小于low,那么其左子树的所有节点值都必然小于low,可以直接跳过
- 如果当前节点值大于high,那么其右子树的所有节点值都必然大于high,也可以直接跳过
- 只有在[low, high]范围内的节点才需要同时检查左右子树
3. 递归解法实现与优化
3.1 基础递归实现
最直观的解法是使用深度优先搜索(DFS)遍历整棵树,对每个节点判断其值是否在范围内:
python复制def rangeSumBST(root, low, high):
if not root:
return 0
sum_val = 0
if low <= root.val <= high:
sum_val += root.val
sum_val += rangeSumBST(root.left, low, high)
sum_val += rangeSumBST(root.right, low, high)
return sum_val
这个解法虽然正确,但效率不高,因为它会遍历所有节点,没有利用BST的性质进行剪枝。
3.2 优化后的递归解法
根据BST的性质,我们可以添加剪枝条件:
python复制def rangeSumBST(root, low, high):
if not root:
return 0
if root.val < low:
return rangeSumBST(root.right, low, high)
if root.val > high:
return rangeSumBST(root.left, low, high)
return root.val + rangeSumBST(root.left, low, high) + rangeSumBST(root.right, low, high)
这个版本的时间复杂度从O(n)优化到了最优情况下的O(log n),最坏情况下仍然是O(n)(当所有节点都在范围内时)。
注意:这里的剪枝条件判断顺序很重要,必须先判断小于low的情况,再判断大于high的情况,最后处理在范围内的情况。这个顺序可以最大化剪枝效率。
4. 迭代解法与性能对比
4.1 使用栈的迭代实现
递归解法虽然简洁,但在实际应用中可能会遇到栈溢出的问题。我们可以用显式栈来实现迭代版本:
python复制def rangeSumBST(root, low, high):
stack = [root]
sum_val = 0
while stack:
node = stack.pop()
if node:
if low <= node.val <= high:
sum_val += node.val
if node.val > low:
stack.append(node.left)
if node.val < high:
stack.append(node.right)
return sum_val
4.2 性能对比分析
在实际测试中,递归解法通常比迭代解法稍快,因为函数调用的开销在现代Python解释器中已经得到了很好的优化。但迭代解法的优势在于:
- 不会出现栈溢出问题
- 更容易控制遍历的顺序
- 内存使用更可预测
在我的测试中,优化后的递归解法能够达到100%的运行速度,主要是因为:
- 充分利用了BST的性质进行剪枝
- Python对递归的优化较好
- 减少了不必要的节点访问
5. 边界条件与测试用例设计
5.1 常见边界情况
在实现这类树遍历算法时,必须考虑以下边界条件:
- 空树(root为None)
- 所有节点值都小于low
- 所有节点值都大于high
- low和high相等
- low等于树的最小值或high等于树的最大值
- 树退化为链表的情况
5.2 推荐的测试用例
python复制# 用例1:普通情况
tree1 = [10,5,15,3,7,null,18]
low1, high1 = 7, 15
# 预期输出:32
# 用例2:所有节点都在范围内
tree2 = [10,5,15]
low2, high2 = 5, 15
# 预期输出:30
# 用例3:空树
tree3 = []
low3, high3 = 1, 100
# 预期输出:0
# 用例4:单节点且在范围内
tree4 = [10]
low4, high4 = 10, 10
# 预期输出:10
# 用例5:范围外
tree5 = [10,5,15]
low5, high5 = 20, 30
# 预期输出:0
6. 算法复杂度分析
6.1 时间复杂度
- 最坏情况:O(n),当所有节点都在范围内时,需要访问所有节点
- 最好情况:O(log n),当范围很小且树平衡时,可以跳过大部分子树
- 平均情况:O(k + log n),其中k是范围内的节点数
6.2 空间复杂度
- 递归解法:O(h),h是树的高度,由递归调用栈深度决定
- 迭代解法:O(h),显式栈的最大深度也是树的高度
对于平衡的BST,h=log n;对于退化的BST(如链表),h=n。
7. 实际编码中的优化技巧
7.1 短路评估的应用
在条件判断中利用Python的短路评估特性可以进一步提高效率:
python复制def rangeSumBST(root, low, high):
if not root:
return 0
current = root.val if low <= root.val <= high else 0
left = rangeSumBST(root.left, low, high) if root.val > low else 0
right = rangeSumBST(root.right, low, high) if root.val < high else 0
return current + left + right
这种写法避免了不必要的递归调用,特别是在范围较小时效果明显。
7.2 尾递归优化
虽然Python官方解释器不支持尾递归优化,但了解这个概念有助于写出更清晰的代码:
python复制def rangeSumBST(root, low, high, sum_val=0):
if not root:
return sum_val
if root.val < low:
return rangeSumBST(root.right, low, high, sum_val)
if root.val > high:
return rangeSumBST(root.left, low, high, sum_val)
sum_val += root.val
sum_val = rangeSumBST(root.left, low, high, sum_val)
sum_val = rangeSumBST(root.right, low, high, sum_val)
return sum_val
这种形式虽然不会带来性能提升,但逻辑更加清晰,便于理解。
8. 类似题目与扩展思考
8.1 LeetCode上的相关题目
掌握了这道题后,可以尝试解决以下类似题目:
-
- Search in a Binary Search Tree(BST搜索)
-
- Insert into a Binary Search Tree(BST插入)
-
- Delete Node in a BST(BST删除)
-
- Kth Smallest Element in a BST(BST第k小元素)
8.2 实际应用场景
BST的范围求和在实际中有很多应用,比如:
- 数据库索引的范围查询
- 统计分数在一定范围内的学生人数
- 时间序列数据在特定时间段内的汇总
8.3 进阶思考:如何处理频繁的范围求和查询?
如果我们需要对同一个BST执行多次不同的范围求和查询,有什么优化方法?可以考虑:
- 预处理所有节点的前缀和
- 使用更高级的数据结构如线段树或树状数组
- 对BST进行平衡处理(如转为AVL树或红黑树)
在实际工程中,BST的范围求和通常作为更复杂算法的一部分出现。理解这个基础问题的各种解法,能够帮助我们更好地解决更复杂的问题。我在多次面试中都被问到这道题的变种,掌握它的优化解法确实能给面试官留下深刻印象。
