LEETCODE 543Easy
二叉树的直径
直径的答案藏在每个节点的“左深 + 右深”里,顺着求深度的递归就能顺手捞出来。
问题拆解
直径是任意两节点间最长路径的边数,这条路径可能穿过根,也可能不穿过。如果对每个节点都单独去搜一遍最长路径,复杂度会很高,而且大量深度会被重复计算。
关键洞察是换一个统计口径:任何一条路径,都有一个“最高点”——路径上离根最近的那个节点。以某个节点为最高点的最长路径,恰好是它左子树深度加右子树深度。于是直径就是所有节点这个值的最大者。
与其枚举路径,不如枚举路径的转折点:每个节点贡献一个“左深 + 右深”,取全局最大即可。
求深度时顺带更新
我们本来就要为每个节点算深度,那就在同一趟递归里,把“左深 + 右深”拿去刷新一个全局最大值:
private int best = 0;
public int diameterOfBinaryTree(TreeNode root) {
depth(root);
return best;
}
private int depth(TreeNode node) {
if (node == null) return 0;
int left = depth(node.left);
int right = depth(node.right);
best = Math.max(best, left + right);
return 1 + Math.max(left, right);
}
def diameterOfBinaryTree(root):
best = 0
def depth(node):
nonlocal best
if not node:
return 0
left = depth(node.left)
right = depth(node.right)
best = max(best, left + right)
return 1 + max(left, right)
depth(root)
return best
func diameterOfBinaryTree(root *TreeNode) int {
best := 0
var depth func(node *TreeNode) int
depth = func(node *TreeNode) int {
if node == nil {
return 0
}
left := depth(node.Left)
right := depth(node.Right)
best = max(best, left+right)
return 1 + max(left, right)
}
depth(root)
return best
}
pub fn diameter_of_binary_tree(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
fn depth(node: &Option<Rc<RefCell<TreeNode>>>, best: &mut i32) -> i32 {
match node {
None => 0,
Some(n) => {
let n = n.borrow();
let left = depth(&n.left, best);
let right = depth(&n.right, best);
*best = (*best).max(left + right);
1 + left.max(right)
}
}
}
let mut best = 0;
depth(&root, &mut best);
best
}
depth 的返回值仍然是老老实实的子树深度,供上层使用;而 best 只在递归内部被悄悄更新。返回值和副作用各司其职,是这类题的常见分工。注意直径按边数计,所以是 left + right 而不用再加一。
复杂度
| 指标 | 复杂度 | 原因 |
|---|---|---|
| 时间 | O(n) |
深度只在一趟后序遍历中算完 |
| 空间 | O(h) |
递归栈深度等于树高 |
可以迁移的模式
- 求“路径最值”时,把路径按其最高点归类,往往能把二维搜索压成一维统计;
- 递归函数可以“返回一个量给上层,同时用副作用维护另一个全局量”;
- 遇到“最长路径”“最大和路径”这类树形问题,先想想每个节点作为转折点能贡献什么。
一次遍历同时算两件事,是把暴力枚举优化掉的常见手法。