1. 问题背景与理解
这道题在LeetCode Hot100中编号543,题目名为"二叉树的直径",难度标记为Easy。题目要求我们计算一棵二叉树的直径长度——这里的直径定义为任意两个结点间最长路径的长度。这个路径可能穿过根节点,也可能不穿过。
举个例子,给定这样一棵二叉树:
code复制 1
/ \
2 3
/ \
4 5
这棵树的直径是3,因为最长路径是[4,2,1,3]或者[5,2,1,3],长度为3(路径上的边数)。
注意:题目中的直径是指路径上的边数,而不是节点数。这是很多初学者容易混淆的地方。
2. 解题思路分析
2.1 暴力解法的问题
最直观的想法可能是:对于每个节点,计算左子树的最深路径和右子树的最深路径,然后相加。这样确实可以得到通过该节点的最长路径。然后我们遍历整棵树,对所有节点都这样计算,取最大值。
但是这种方法的时间复杂度是O(n²),因为对于每个节点都要计算其子树的高度。对于平衡二叉树还好,但对于极端情况(如链表状的树),性能会很差。
2.2 递归思路的优化
更聪明的做法是在计算树高的过程中,同时记录直径。这样我们只需要一次后序遍历(DFS)就能解决问题,时间复杂度降为O(n)。
具体思路是:
- 对于每个节点,计算其左子树和右子树的高度
- 当前节点的直径就是左子树高度 + 右子树高度
- 维护一个全局变量记录最大直径
- 返回当前节点的高度(左右子树高度的较大值 + 1)
这样,我们在计算高度的过程中,顺便计算了直径,避免了重复计算。
3. 递归解法实现
3.1 Python代码实现
python复制class Solution:
def diameterOfBinaryTree(self, root: TreeNode) -> int:
self.diameter = 0
def depth(node):
if not node:
return 0
left_depth = depth(node.left)
right_depth = depth(node.right)
self.diameter = max(self.diameter, left_depth + right_depth)
return max(left_depth, right_depth) + 1
depth(root)
return self.diameter
3.2 代码解析
- 我们定义了一个类变量
diameter来记录最大直径 depth函数是一个递归函数,计算以当前节点为根的子树的高度- 在计算高度的同时,我们更新
diameter:left_depth + right_depth就是通过当前节点的直径 - 最后返回的是当前子树的高度(左右子树高度的较大值 + 1)
- 初始调用
depth(root)后,diameter中就存储了整棵树的最大直径
3.3 复杂度分析
- 时间复杂度:O(n),每个节点只被访问一次
- 空间复杂度:O(h),h是树的高度,递归调用栈的深度
4. 递归过程详解
让我们用前面的例子来详细走一遍递归过程:
code复制 1
/ \
2 3
/ \
4 5
递归调用栈:
- 调用depth(1)
- 调用depth(2)
- 调用depth(4)
- depth(4)的左右子树都为0,diameter=max(0,0+0)=0,返回1
- 调用depth(5)
- depth(5)的左右子树都为0,diameter=max(0,0+0)=0,返回1
- depth(2): left=1, right=1, diameter=max(0,1+1)=2,返回2
- 调用depth(4)
- 调用depth(3)
- depth(3)的左右子树都为0,diameter=max(2,0+0)=2,返回1
- depth(1): left=2, right=1, diameter=max(2,2+1)=3,返回3
- 调用depth(2)
最终diameter=3,与预期一致。
5. 边界条件与注意事项
5.1 空树的情况
如果树为空(root=None),直径应该是0。我们的代码能正确处理这种情况,因为depth(None)返回0,diameter初始为0,不会被更新。
5.2 单节点树
只有一个根节点的树,直径是0(因为没有边)。我们的代码也能正确处理:
- depth(root)的左右子树都为0
- diameter=max(0,0+0)=0
- 返回高度1
5.3 最大直径不经过根节点的情况
考虑这棵树:
code复制 1
/
2
/ \
4 5
/ \
6 7
/ \
8 9
最大直径是6(路径8-6-4-2-5-7-9),不经过根节点1。我们的算法能正确计算这种情况,因为它在遍历每个节点时都会计算通过该节点的直径。
5.4 递归深度问题
对于极度不平衡的树(如链表状的树),递归深度可能很大,可能导致栈溢出。但在LeetCode的测试用例中,这通常不是问题。如果确实需要考虑,可以改用迭代方式的后序遍历。
6. 相关题目与扩展
6.1 类似题目
- 104. 二叉树的最大深度:计算树的最大深度,是本题的基础
- 110. 平衡二叉树:判断二叉树是否平衡,也需要计算子树高度
- 124. 二叉树中的最大路径和:类似思路,但计算的是路径和而非长度
6.2 非递归解法
虽然递归解法简洁,但了解迭代解法也有价值。可以使用后序遍历的迭代实现:
python复制def diameterOfBinaryTree(root):
if not root:
return 0
max_diameter = 0
stack = []
node = root
last_visited = None
depth = {None: 0}
while stack or node:
if node:
stack.append(node)
node = node.left
else:
peek_node = stack[-1]
if peek_node.right and last_visited != peek_node.right:
node = peek_node.right
else:
left_depth = depth[peek_node.left]
right_depth = depth[peek_node.right]
max_diameter = max(max_diameter, left_depth + right_depth)
depth[peek_node] = max(left_depth, right_depth) + 1
last_visited = stack.pop()
return max_diameter
6.3 其他树的相关问题
- 二叉树的遍历:前序、中序、后序、层序
- 二叉搜索树:验证、搜索、插入、删除等操作
- 树的序列化与反序列化
- **最近公共祖先(LCA)**问题
7. 实际应用场景
虽然这道题看起来是纯算法题,但它的思想在实际中有广泛应用:
- 网络拓扑中的最长路径:计算网络设备间的最长通信路径
- 文件系统目录结构分析:找出最深嵌套的文件夹路径
- 组织结构图分析:计算公司汇报链的最大长度
- 游戏AI路径规划:类似蜜蜂路径规划问题,寻找最优路径
理解这种递归计算高度并在过程中维护额外信息(直径)的模式,对解决许多树相关问题都很有帮助。
8. 常见错误与调试技巧
8.1 常见错误
- 混淆直径定义:把节点数当成直径(实际是边数)
- 忽略全局变量:尝试直接返回直径而不是高度
- 重复计算:单独计算高度和直径导致O(n²)复杂度
- 空树处理:忘记处理root为None的情况
8.2 调试技巧
- 小例子验证:先用手工计算小树的直径,再与程序输出对比
- 打印递归过程:在递归函数中加入打印语句,观察调用顺序
- 可视化工具:使用二叉树可视化工具(如LeetCode的树可视化)辅助理解
- 单元测试:编写多个测试用例,包括边界情况
9. 性能优化与变种
9.1 不使用全局变量
有些编程规范不鼓励使用全局变量。我们可以通过返回多个值来实现:
python复制def diameterOfBinaryTree(root):
def dfs(node):
if not node:
return 0, 0
left_diameter, left_depth = dfs(node.left)
right_diameter, right_depth = dfs(node.right)
current_diameter = left_depth + right_depth
max_diameter = max(left_diameter, right_diameter, current_diameter)
current_depth = max(left_depth, right_depth) + 1
return max_diameter, current_depth
max_d, _ = dfs(root)
return max_d
9.2 多叉树的直径
如果问题扩展到多叉树(每个节点可以有多个子节点),思路类似:
python复制class Node:
def __init__(self, val=None, children=None):
self.val = val
self.children = children if children is not None else []
def diameter(root):
max_diameter = 0
def depth(node):
nonlocal max_diameter
if not node:
return 0
max_depth1 = max_depth2 = 0
for child in node.children:
d = depth(child)
if d > max_depth1:
max_depth2, max_depth1 = max_depth1, d
elif d > max_depth2:
max_depth2 = d
max_diameter = max(max_diameter, max_depth1 + max_depth2)
return max_depth1 + 1
depth(root)
return max_diameter
10. 总结与个人心得
这道"二叉树的直径"虽然是Easy难度,但它很好地展示了递归在树问题中的应用。关键在于:
- 后序遍历:先处理子节点再处理当前节点的思路
- 高度与直径的关系:在计算高度的同时维护直径信息
- 全局变量:合理使用全局变量避免重复计算
在实际面试中,这类题目考察的是对递归的理解和将问题分解的能力。我个人的经验是:
- 先明确递归函数的定义(这里depth(node)返回的是高度)
- 想清楚当前节点需要子节点提供什么信息(左右子树的高度)
- 在当前节点能计算出什么额外信息(直径)
- 处理好边界条件(空节点)
这种模式可以推广到许多其他树问题上,比如计算最大路径和、判断平衡二叉树等。掌握这种"在递归过程中维护额外信息"的技巧,对解决树相关问题非常有帮助。
