一、题目描述
给你一棵二叉树的根节点 root,返回该树的 直径。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root。
两节点之间路径的 长度 由它们之间 边数 表示。
示例
| 示例 | 输入 | 输出 | 路径说明 |
|---|---|---|---|
| 示例1 | root = [1,2,3,4,5] |
3 |
路径 [4,2,1,3] 或 [5,2,1,3] |
| 示例2 | root = [1,2] |
1 |
路径 [1,2] |
示例1的树结构:
1
/ \
2 3
/ \
4 5
最长路径:4 → 2 → 1 → 3,边数为3
注意:不是经过根节点的路径才最长!
提示
- 树中节点数目在范围
[1, 10^4]内 -100 <= Node.val <= 100
二、解题思路总览
| 方法 | 核心思想 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 递归 | 后序遍历,计算每个节点的"左右深度和" | O(n) | O(h),h为树高 |
关键 insight:
- 经过某个节点的最长路径 = 左子树深度 + 右子树深度 + 1(节点数)
- 直径 = 所有节点中最大的"左右深度和 + 1"减1(因为直径是边数不是节点数)
核心公式:
经过节点x的最长路径 = 左子树深度 + 右子树深度 + 1
直径 = max(所有节点的左子树深度 + 右子树深度) + 1 - 1
三、完整代码
cpp
class Solution {
public:
int ans = 1; // 记录最大直径(节点数)
int traversal(TreeNode* root) {
if (!root) return 0; // 1. 空节点,深度为0
int l = traversal(root->left); // 2. 左子树深度
int r = traversal(root->right); // 3. 右子树深度
// 4. 更新最大直径(节点数)
// l + r + 1:左子树深度 + 右子树深度 + 当前节点
ans = max(ans, l + r + 1);
// 5. 返回当前节点的深度(用于父节点计算)
// 深度 = max(左子树深度, 右子树深度) + 1
return max(l, r) + 1;
}
int diameterOfBinaryTree(TreeNode* root) {
traversal(root); // 6. 递归遍历所有节点
return ans - 1; // 7. 直径 = 节点数 - 1 = 边数
}
};
四、算法流程图(ASCII)
示例1的树结构
1
/ \
2 3
/ \
4 5
计算过程:
traversal(1)
│
├── traversal(2)
│ │
│ ├── traversal(4) → return 1 (叶子节点)
│ │
│ ├── traversal(5) → return 1 (叶子节点)
│ │
│ ├── ans = max(1, 1+1+1) = max(1, 3) = 3
│ └── return max(1,1) + 1 = 2
│
├── traversal(3)
│ │
│ ├── traversal(NULL) → return 0
│ ├── traversal(NULL) → return 0
│ ├── ans = max(3, 0+0+1) = 3
│ └── return max(0,0) + 1 = 1
│
├── ans = max(3, 2+1+1) = max(3, 4) = 4 ← 这里其实不会变
└── return max(2, 1) + 1 = 3
最终:ans = 4(节点数),直径 = ans - 1 = 3
直径与深度的关系
直径路径可能不经过根节点:
1
/ \
2 3
/ \
4 5
路径 [4,2,5] 不经过根节点:
- 经过节点2的路径 = 深度4(1) + 深度5(1) + 1(节点2) = 3个节点
- 直径 = 3 - 1 = 2条边
路径 [4,2,1,3] 经过根节点:
- 经过节点1的路径 = 深度2(2) + 深度3(1) + 1(节点1) = 4个节点
- 但这不是最长的!
五、逐行解析
cpp
class Solution {
public:
// ─────────────────────────────────────────
// 全局变量ans:记录遍历过程中遇到的最大直径(节点数)
// 初始化为1:至少有一个节点时,直径至少是1个节点
// ─────────────────────────────────────────
int ans = 1;
// ─────────────────────────────────────────
// 后序遍历:返回以root为根的子树的深度
// 同时更新全局变量ans
// ─────────────────────────────────────────
int traversal(TreeNode* root) {
// ─────────────────────────────────────────
// 第1步:递归终止条件
// 空节点深度为0
// ─────────────────────────────────────────
if (!root) return 0;
// ─────────────────────────────────────────
// 第2步:递归计算左子树深度
// 后序遍历:先处理左子树
// ─────────────────────────────────────────
int l = traversal(root->left);
// ─────────────────────────────────────────
// 第3步:递归计算右子树深度
// 后序遍历:再处理右子树
// ─────────────────────────────────────────
int r = traversal(root->right);
// ─────────────────────────────────────────
// 第4步:更新最大直径(节点数)
//
// 经过当前root的最长路径 = 左深度 + 右深度 + 1
// l:左子树深度(边数)
// r:右子树深度(边数)
// +1:当前root节点
//
// ans记录所有节点中最大的那个值
// ─────────────────────────────────────────
ans = max(ans, l + r + 1);
// ─────────────────────────────────────────
// 第5步:返回当前节点的深度
//
// 当前节点的深度 = max(左子树深度, 右子树深度) + 1
// +1:从子节点到当前节点的边
//
// 这个返回值会给父节点使用
// 父节点计算路径时需要知道子树的深度
// ─────────────────────────────────────────
return max(l, r) + 1;
}
// ─────────────────────────────────────────
// 入口函数:返回直径(边数)
// ─────────────────────────────────────────
int diameterOfBinaryTree(TreeNode* root) {
traversal(root); // 遍历所有节点,更新ans
return ans - 1; // ans是节点数,直径是边数,所以-1
}
};
六、复杂度分析
时间复杂度
| 分析 | 复杂度 |
|---|---|
| 每个节点访问一次 | O(n) |
推导:递归遍历每个节点一次,每个节点做常数次操作。
空间复杂度
| 分析 | 复杂度 |
|---|---|
| 函数调用栈,最大深度为树高h | O(h) |
推导:
- 最坏情况(链表形状):h = n,复杂度 O(n)
- 平衡树情况:h = log n,复杂度 O(log n)
七、面试追问 FAQ
| 问题 | 回答 |
|---|---|
为什么返回 ans - 1? |
ans 记录的是最长路径的节点数 ,直径是边数,边数=节点数-1 |
| 直径一定经过根节点吗? | 不一定!如示例1,直径是 [4,2,5] 或 [4,2,1,3],前者不经过根 |
如何理解 l + r + 1? |
左子树深度(边)+ 右子树深度(边)+ 当前节点 = 经过当前节点的最长路径的节点数 |
| 为什么不叫"深度"而叫"直径"? | 树的直径是图论概念,指任意两点间的最长路径 |
| 和二叉树最大深度有什么区别? | 最大深度是从根到叶子的最长路径,直径是任意两节点之间 |
八、相关题目
| 题目 | 难度 | 关键点 |
|---|---|---|
| 543. 二叉树的直径 | 简单 | 本题 |
| 104. 二叉树的最大深度 | 简单 | 递归求深度 |
| 110. 平衡二叉树 | 简单 | 计算高度 |
| 124. 二叉树中的最大路径和 | 困难 | 扩展本题 |
九、总结
| 对比项 | 说明 |
|---|---|
| 代码行数 | 核心7行 |
| 时间复杂度 | O(n) |
| 空间复杂度 | O(h) |
| 递归顺序 | 后序遍历(左-右-根) |
| 核心变量 | ans:最大直径(节点数),l/r:子树深度 |
核心理解:
直径 = max(左子树深度 + 右子树深度) + 1 - 1
= max(所有节点的左深度 + 右深度)
返回值 = max(左子树深度, 右子树深度) + 1
易错点:
- 直径是边数,不是节点数
- 直径可能不经过根节点
ans初始化为1(而不是0)因为单个节点也算一条"路径"