二叉树的直径

给你一棵二叉树的根节点 root,返回该树的直径。

二叉树的直径,是指树中任意两个节点之间最长路径的长度。

这条路径可能经过根节点 root,也可能不经过根节点。

两节点之间路径的长度由它们之间的边数表示。

示例 1:

输入:root = [1,2,3,4,5]
输出:3
解释:取路径 [4,2,1,3] 或 [5,2,1,3],长度为 3。

示例 2:

输入:root = [1,2]
输出:1

提示:

  • 树中节点数目在范围 [1, 10^4]
  • -100 <= Node.val <= 100

后序 DFS:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int diameterOfBinaryTree(TreeNode* root) {
        ans = 0;
        depth(root);
        return ans;
    }

private:
    int ans;

    int depth(TreeNode* node) {
        if (node == nullptr)
        {
            return 0;
        }

        int leftDepth = depth(node->left);
        int rightDepth = depth(node->right);

        ans = max(ans, leftDepth + rightDepth);

        return max(leftDepth, rightDepth) + 1;
    }
};

核心思想

这题容易和“二叉树最大深度”联系起来。

对于任意一个节点,如果最长路径经过这个节点,那么这条路径一定是:

左子树中最深的节点 -> 当前节点 -> 右子树中最深的节点

所以经过当前节点的最长路径长度是:

左子树最大深度 + 右子树最大深度

这里要注意:

  • 深度函数返回的是从当前节点向下的最长路径上的节点数
  • 直径要求的是两个节点之间的边数

如果左子树深度是 leftDepth,右子树深度是 rightDepth,那么从左侧最深节点到右侧最深节点的边数正好是:

leftDepth + rightDepth

这题最关键的观察是:

全局直径一定会经过某个节点,并且等于这个节点左右子树最大深度之和。

因此,在递归求每个节点深度的同时,用 leftDepth + rightDepth 更新全局答案即可。

深度函数的含义

定义函数:

int depth(TreeNode* node)

它表示:

以 node 为根的子树,从 node 向下走到最远叶子节点的节点数

如果 node == nullptr,空树深度为 0

return 0;

如果 node 不为空,先递归计算左右子树深度:

int leftDepth = depth(node->left);
int rightDepth = depth(node->right);

然后当前子树的深度就是:

max(leftDepth, rightDepth) + 1

这个 +1 表示当前节点本身。

如何更新直径

对于当前节点 node,如果最长路径经过它,那么路径会从左子树某个最深节点走到右子树某个最深节点。

路径长度是边数。

左边从当前节点走到最深节点有 leftDepth 条边。

右边从当前节点走到最深节点有 rightDepth 条边。

所以经过当前节点的路径长度是:

leftDepth + rightDepth

代码中用全局变量 ans 记录目前见过的最大直径:

ans = max(ans, leftDepth + rightDepth);

递归遍历所有节点后,ans 就是整棵树的直径。

为什么使用后序遍历

计算当前节点的直径候选值时,需要先知道:

  • 左子树最大深度
  • 右子树最大深度

也就是说,必须先处理左右子树,再处理当前节点。

这正是后序遍历的顺序:

左子树 -> 右子树 -> 根节点

代码中先递归:

int leftDepth = depth(node->left);
int rightDepth = depth(node->right);

然后再更新当前节点对应的直径:

ans = max(ans, leftDepth + rightDepth);

所以这个解法本质上是后序 DFS。

路径不一定经过根节点

题目特别强调,直径路径可能经过根节点,也可能不经过根节点。

例如某棵树的最长路径完全位于左子树内部。

如果只计算根节点的:

root.leftDepth + root.rightDepth

就会漏掉这种情况。

所以算法必须在每个节点都计算一次:

leftDepth + rightDepth

并用全局答案取最大值。

这样无论最长路径经过哪个节点,都会在处理那个节点时被统计到。

边界情况

如果树只有一个节点:

root = [1]

没有任何边,所以直径是 0

递归中左右子树深度都是 0,更新:

ans = max(ans, 0 + 0) = 0

如果树只有两个节点:

root = [1,2]

最长路径就是这两个节点之间的一条边,所以直径是 1

如果树退化成一条链,直径就是链上节点数减 1

正确性证明

我们证明:算法返回的 ans 等于二叉树的直径。

结论 1:depth(node) 正确返回以 node 为根的子树最大深度

如果 node == nullptr,空树没有节点,深度为 0,返回正确。

如果 node 不为空,那么从 node 向下到最远叶子的路径,必然经过左子树或右子树。

左子树最大深度是 leftDepth,右子树最大深度是 rightDepth

因此当前子树最大深度是:

max(leftDepth, rightDepth) + 1

其中 +1 表示当前节点。

所以 depth(node) 的返回值正确。

结论 2:经过当前节点的最长路径长度是 leftDepth + rightDepth

如果一条最长路径经过当前节点,并且同时向左右两侧延伸,那么左侧最多能延伸到左子树最深节点,长度为 leftDepth 条边。

右侧最多能延伸到右子树最深节点,长度为 rightDepth 条边。

两部分通过当前节点连接后,总边数就是:

leftDepth + rightDepth

如果某一侧为空,对应深度为 0,公式仍然成立。

结论 3:全局直径一定会在某个节点处被统计到

任意两个节点之间的路径,在树中是唯一的。

这条路径上一定存在一个最高的公共节点,可以看作路径连接左右两侧的转折点。

当算法处理这个转折点时,它的左、右子树深度之和至少能覆盖这条路径长度。

因此,真正的全局最长路径一定会在某个节点执行:

ans = max(ans, leftDepth + rightDepth)

时被统计到。

结论 4:算法不会得到超过真实直径的答案

每一次用于更新 ansleftDepth + rightDepth,都对应一条真实存在的路径:

左侧最深节点 -> 当前节点 -> 右侧最深节点

所以它一定不会超过树中任意两点之间最长路径的长度。

因此 ans 不会被更新成非法值。

得出结论

由结论 1 可知,算法能正确计算每个节点左右子树深度。

由结论 2 可知,每个节点对应的直径候选值计算正确。

由结论 3 可知,算法不会漏掉真正的全局直径。

由结论 4 可知,算法不会得到超过真实直径的结果。

因此算法返回的 ans 就是二叉树的直径。

举例理解

以:

root = [1,2,3,4,5]

为例,这棵树可以表示为:

      1
     / \
    2   3
   / \
  4   5

从底向上计算:

节点 左深度 右深度 直径候选
4 0 0 0
5 0 0 0
2 1 1 2
3 0 0 0
1 2 1 3

最大候选值是:

3

对应路径可以是:

4 -> 2 -> 1 -> 3

路径中有 3 条边,所以答案是 3

复杂度分析

设二叉树节点数为 n,树的高度为 h

每个节点只会被 DFS 访问一次。

  • 时间复杂度:O(n)
  • 空间复杂度:O(h)

空间复杂度来自递归调用栈。

如果树退化成链,h = n;如果树比较平衡,h = log n