1. 二叉树直径问题解析
这道题目来自力扣热门题目排行榜(Hot 100)中的经典二叉树问题。题目看似简单,但蕴含着对二叉树遍历和递归思想的深刻理解。作为刷过300+力扣题的老手,我发现这是检验二叉树掌握程度的绝佳题目。
二叉树的直径定义为树中任意两个节点间最长路径的长度。这个路径可能穿过根节点,也可能完全位于某个子树中。例如,对于一棵高度为3的完全二叉树,其直径为5(从最左叶子到最右叶子的路径)。
关键提示:直径长度等于某节点左右子树高度之和的最大值。这个定义是解题的核心突破口。
2. 解题思路与算法选择
2.1 暴力解法分析
新手最容易想到的方法是:计算每个节点的左右子树高度,相加后取最大值。这种方法确实可行,但时间复杂度高达O(n²),对于大型树结构效率极低。
python复制def diameterOfBinaryTree(root):
if not root:
return 0
# 计算当前节点直径
left_height = getHeight(root.left)
right_height = getHeight(root.right)
current_diameter = left_height + right_height
# 递归计算左右子树的直径
left_diameter = diameterOfBinaryTree(root.left)
right_diameter = diameterOfBinaryTree(root.right)
return max(current_diameter, left_diameter, right_diameter)
def getHeight(node):
if not node:
return 0
return 1 + max(getHeight(node.left), getHeight(node.right))
2.2 优化思路
观察到在暴力解法中,我们重复计算了多次节点高度。更聪明的做法是在计算高度的同时记录直径,这样可以将时间复杂度优化到O(n)。
3. 最优解法实现
3.1 深度优先搜索(DFS)方案
采用后序遍历的思路,在计算每个节点高度的同时,更新全局最大直径:
python复制class Solution:
def diameterOfBinaryTree(self, root: TreeNode) -> int:
self.diameter = 0
self.dfs(root)
return self.diameter
def dfs(self, node):
if not node:
return 0
left = self.dfs(node.left)
right = self.dfs(node.right)
# 更新最大直径
self.diameter = max(self.diameter, left + right)
# 返回当前节点高度
return 1 + max(left, right)
3.2 关键步骤解析
- 递归终止条件:当节点为空时,返回高度0
- 递归计算左右子树高度:这是标准的后序遍历顺序
- 更新直径:当前节点的直径等于左右子树高度之和
- 返回当前子树高度:用于父节点的直径计算
4. 复杂度分析与对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力解法 | O(n²) | O(h) | 不推荐使用 |
| DFS优化 | O(n) | O(h) | 推荐方案 |
其中h为二叉树的高度,最坏情况下(树退化为链表)h=n。
5. 常见问题与调试技巧
5.1 边界条件处理
- 空树情况:直接返回0
- 只有根节点:直径为0
- 单边树:直径等于节点数-1
5.2 调试技巧
在递归函数中添加打印语句,观察遍历顺序和高度计算:
python复制def dfs(self, node, depth=0):
if not node:
print(' '*depth + 'None')
return 0
print(' '*depth + str(node.val))
left = self.dfs(node.left, depth+1)
right = self.dfs(node.right, depth+1)
self.diameter = max(self.diameter, left + right)
return 1 + max(left, right)
5.3 易错点警示
- 直径定义误解:不是简单的根节点左右高度和,而是整棵树中的最大值
- 返回值混淆:dfs函数返回的是高度,不是直径
- 全局变量使用:注意在类中正确声明和使用self.diameter
6. 变种与扩展题目
掌握这道题后,可以尝试以下相关题目巩固知识:
- 二叉树的最大路径和(力扣124题)
- 最长同值路径(力扣687题)
- 二叉树的坡度(力扣563题)
- 平衡二叉树判断(力扣110题)
这些题目都采用了类似的递归遍历思想,只是在计算和更新值时有所不同。
7. 实际应用场景
二叉树直径问题虽然抽象,但在网络路由、社交网络关系分析等领域有实际应用:
- 网络拓扑分析:计算网络中最远两个节点的距离
- 社交网络:找出关系链最长的两个用户
- 文件系统:确定目录树中最深的两个文件间的路径长度
8. 性能优化进阶
对于特别大的树结构,可以考虑以下优化:
- 迭代法实现:用栈模拟递归,避免递归栈溢出
- 并行计算:对左右子树分别启动线程计算高度
- 记忆化存储:如果树结构不变但多次查询,可以缓存节点高度
迭代法示例:
python复制def diameterOfBinaryTree(root):
if not root:
return 0
diameter = 0
stack = [(root, False)]
height = {}
while stack:
node, visited = stack.pop()
if visited:
left_height = height.get(node.left, 0)
right_height = height.get(node.right, 0)
diameter = max(diameter, left_height + right_height)
height[node] = 1 + max(left_height, right_height)
else:
stack.append((node, True))
if node.right:
stack.append((node.right, False))
if node.left:
stack.append((node.left, False))
return diameter
9. 不同语言实现对比
虽然算法思想相同,但不同语言的实现各有特点:
9.1 Java实现
java复制class Solution {
int diameter = 0;
public int diameterOfBinaryTree(TreeNode root) {
dfs(root);
return diameter;
}
private int dfs(TreeNode node) {
if (node == null) return 0;
int left = dfs(node.left);
int right = dfs(node.right);
diameter = Math.max(diameter, left + right);
return 1 + Math.max(left, right);
}
}
9.2 C++实现
cpp复制class Solution {
public:
int diameterOfBinaryTree(TreeNode* root) {
int diameter = 0;
dfs(root, diameter);
return diameter;
}
int dfs(TreeNode* node, int& diameter) {
if (!node) return 0;
int left = dfs(node->left, diameter);
int right = dfs(node->right, diameter);
diameter = max(diameter, left + right);
return 1 + max(left, right);
}
};
10. 测试用例设计
全面的测试用例应该包括:
- 空树:输入null,预期输出0
- 单节点树:只有根节点,预期输出0
- 完全二叉树:如[1,2,3,4,5],预期输出3
- 退化链表树:如[1,2,null,3,null,4],预期输出3
- 不平衡树:如[1,2,3,4,null,null,5,6,null,7],预期输出5
11. 刷题心得
在力扣刷题过程中,我发现这类二叉树题目有几个共同特点:
- 递归思想:大部分解法都基于递归遍历
- 后序遍历:通常需要先处理子节点再处理父节点
- 全局变量:用于在递归过程中记录最大值
- 高度与深度:要清楚区分这两个概念
实际刷题时,建议先画出树结构,手动计算预期结果,再编写代码验证。对于递归不熟悉的同学,可以从简单的二叉树遍历开始练习。
