1. 算法训练营第十八天核心题目解析
今天要啃下三道二叉树相关的LeetCode题目:530.二叉搜索树的最小绝对差、501.二叉搜索树中的众数、236.二叉树的最近公共祖先。这三道题覆盖了BST特性应用、树遍历统计和经典LCA问题,都是面试中的高频考点。我把自己在训练营中的解题思路和踩坑经验整理出来,特别适合正在刷二叉树专题的朋友参考。
二叉搜索树(BST)的特性是左子树所有节点值小于根节点,右子树所有节点值大于根节点。这个性质让BST相关问题往往有比普通二叉树更优的解法。而普通二叉树的解法通常需要更通用的遍历技巧。下面我会分别从BST特性和通用解法两个角度拆解这三道题目。
2. LeetCode530. 二叉搜索树的最小绝对差
2.1 BST特性与双指针法
这道题要求找到BST中任意两节点差的最小值。利用BST中序遍历有序的特性,可以在O(1)空间复杂度完成。核心思路是用中序遍历模拟有序数组的双指针遍历:
python复制class Solution:
def getMinimumDifference(self, root: TreeNode) -> int:
self.pre = None
self.min_diff = float('inf')
def inorder(node):
if not node:
return
inorder(node.left)
if self.pre is not None:
self.min_diff = min(self.min_diff, node.val - self.pre)
self.pre = node.val
inorder(node.right)
inorder(root)
return self.min_diff
关键点:使用类成员变量保存前驱节点和最小差值,避免全局变量污染。中序遍历时先处理左子树,然后比较当前节点与前驱节点的差值,最后处理右子树。
2.2 显式转换为有序数组的解法
虽然空间复杂度更高(O(n)),但这种方法更直观,适合BST新手理解:
python复制def getMinimumDifference(root):
vals = []
def inorder(node):
if node:
inorder(node.left)
vals.append(node.val)
inorder(node.right)
inorder(root)
return min(vals[i+1]-vals[i] for i in range(len(vals)-1))
实测对比:在LeetCode提交中,双指针法运行时间约60ms,转换数组法约70ms。虽然时间复杂度都是O(n),但双指针法避免了数组存储开销。
3. LeetCode501. 二叉搜索树中的众数
3.1 中序遍历统计频率
BST的众数问题同样可以利用中序遍历有序的特性,统计连续相同值的出现次数:
python复制class Solution:
def findMode(self, root: TreeNode) -> List[int]:
self.current_val = None
self.current_count = 0
self.max_count = 0
self.modes = []
def inorder(node):
if not node:
return
inorder(node.left)
self.handle_value(node.val)
inorder(node.right)
def handle_value(val):
if val != self.current_val:
self.current_val = val
self.current_count = 0
self.current_count += 1
if self.current_count > self.max_count:
self.max_count = self.current_count
self.modes = [val]
elif self.current_count == self.max_count:
self.modes.append(val)
inorder(root)
return self.modes
3.2 普通二叉树的通用解法
如果不利用BST特性,可以用哈希表统计所有节点值频率:
python复制from collections import defaultdict
def findMode(root):
freq = defaultdict(int)
def traverse(node):
if node:
freq[node.val] += 1
traverse(node.left)
traverse(node.right)
traverse(root)
max_freq = max(freq.values()) if freq else 0
return [k for k,v in freq.items() if v == max_freq]
性能对比:BST解法时间O(n)空间O(1),通用解法时间O(n)空间O(n)。当树非常大时,BST解法优势明显。
4. LeetCode236. 二叉树的最近公共祖先
4.1 递归解法核心思路
这道题是经典的LCA问题,递归解法非常优雅:
python复制class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if not root or root == p or root == q:
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if left and right:
return root
return left if left else right
递归终止条件:
- 当前节点为空
- 当前节点是p或q
递归过程:
- 在左子树中查找p/q
- 在右子树中查找p/q
- 如果左右都找到,当前节点就是LCA
- 否则返回非空的那一侧
4.2 迭代解法与父指针法
对于不喜欢递归的开发者,可以用父指针法:
python复制def lowestCommonAncestor(root, p, q):
stack = [root]
parent = {root: None}
while p not in parent or q not in parent:
node = stack.pop()
if node.left:
parent[node.left] = node
stack.append(node.left)
if node.right:
parent[node.right] = node
stack.append(node.right)
ancestors = set()
while p:
ancestors.add(p)
p = parent[p]
while q not in ancestors:
q = parent[q]
return q
实测建议:递归解法代码更简洁,但在极端情况下(树很深)可能栈溢出。迭代解法更安全但代码稍复杂。
5. 二叉树问题通用解题框架
5.1 遍历模板与选择策略
二叉树问题通常有四种遍历方式:
- 前序遍历:根-左-右
- 中序遍历:左-根-右(BST常用)
- 后序遍历:左-右-根(LCA常用)
- 层序遍历:按层次遍历
选择策略:
- 需要先处理根节点信息 → 前序
- 需要有序处理BST节点 → 中序
- 需要先知道子树结果 → 后序
- 需要层次相关信息 → 层序
5.2 递归与迭代的转换技巧
递归转迭代的通用方法:
- 使用显式栈模拟调用栈
- 将递归参数存入栈中
- 用循环代替递归调用
- 用标记法处理访问顺序(颜色标记法)
示例:中序遍历的迭代写法
python复制def inorderTraversal(root):
res = []
stack = []
curr = root
while curr or stack:
while curr:
stack.append(curr)
curr = curr.left
curr = stack.pop()
res.append(curr.val)
curr = curr.right
return res
6. 常见错误与调试技巧
6.1 指针丢失问题
在树遍历中常见的错误是修改了指针导致后续访问出错。例如:
python复制# 错误写法:修改了curr指针导致右子树丢失
while curr:
stack.append(curr)
curr = curr.left
正确做法是使用临时变量或保持指针不变。
6.2 递归终止条件不完整
特别是在LCA问题中,容易漏掉root == p or root == q的终止条件,导致结果错误。
6.3 BST验证陷阱
虽然今天的题目都是合法的BST,但在实际面试中,经常需要先验证BST的合法性:
python复制def isValidBST(root):
def helper(node, lower=float('-inf'), upper=float('inf')):
if not node:
return True
val = node.val
if val <= lower or val >= upper:
return False
return helper(node.left, lower, val) and helper(node.right, val, upper)
return helper(root)
7. 性能优化与进阶思考
7.1 莫里斯遍历(Morris Traversal)
对于BST问题,可以使用莫里斯遍历实现O(n)时间O(1)空间的解法:
python复制def getMinimumDifference(root):
cur = root
pre = None
min_diff = float('inf')
while cur:
if not cur.left:
if pre:
min_diff = min(min_diff, cur.val - pre.val)
pre = cur
cur = cur.right
else:
# 找前驱节点
predecessor = cur.left
while predecessor.right and predecessor.right != cur:
predecessor = predecessor.right
if not predecessor.right:
predecessor.right = cur
cur = cur.left
else:
predecessor.right = None
if pre:
min_diff = min(min_diff, cur.val - pre.val)
pre = cur
cur = cur.right
return min_diff
7.2 非比较统计众数
对于501题,如果节点值范围已知且不大,可以用计数排序的思路:
python复制def findMode(root):
count = [0] * (MAX_VAL - MIN_VAL + 1)
# 遍历统计频率
# 找出最大值
# 返回所有等于最大值的数
这种方法时间复杂度O(n),空间复杂度O(k),k是值域范围。
8. 面试实战建议
- 先明确问题性质:是BST还是普通二叉树?
- 根据问题特点选择遍历方式
- 先给出暴力解法,再优化
- 注意边界条件:空树、单节点、所有节点值相同等
- 解释时间/空间复杂度时考虑最坏情况
对于今天的三个题目,在面试中常出现的变种:
- 530:求最大绝对差
- 501:找出频率前k高的值
- 236:求多个节点的LCA
我在训练营中最大的收获是:对于树问题,先画出具体例子,手动模拟算法流程,再写代码。这比直接写代码能避免很多错误。比如在LCA问题中,通过画图能清晰看到递归的返回过程。
