Problem: 543. 二叉树的直径
题目:给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。
两节点之间路径的 长度 由它们之间边数表示。
整体思路
这段代码旨在解决一个经典的树问题:二叉树的直径 (Diameter of Binary Tree)。问题要求计算二叉树中任意两个节点之间最长路径的长度。这条路径可能穿过根节点,也可能不穿过根节点,完全包含在某个子树中。
该实现采用了一种非常巧妙的 深度优先搜索 (DFS) 的递归方法,并结合一个全局变量(或称成员变量) 来记录最终结果。
核心思想:深度与直径的关系
- 二叉树的直径:对于任意一个节点
node
,穿过该节点的最长路径长度等于其左子树的最大深度加上其右子树的最大深度。 - 全局最大直径:整个二叉树的直径,就是所有节点“穿过该节点的最长路径”中的最大值。
- 函数的双重职责:为了实现这一点,递归函数
dfs(node)
被设计为具有双重职责:
a. 返回值:它返回以node
为根的子树的最大深度(从node
到其最远叶子节点的路径长度)。这个返回值是给其父节点使用的,用于计算父节点那一层的直径和深度。
b. 副作用:在计算深度的过程中,它顺便计算出穿过node
的直径,并用这个值去更新全局变量ans
。
- 二叉树的直径:对于任意一个节点
递归函数
dfs(node)
的具体逻辑- 递归的终止条件 (Base Case):
if (node == null)
。如果当前节点为空,它不构成任何路径,其“深度”可以定义为 -1。返回 -1 的好处是,当其父节点计算dfs(null) + 1
时,结果为 0,这正好代表一个空子树的深度为0,逻辑上非常自洽。 - 递归的递推关系 (Recursive Step):
a.int leftLength = dfs(node.left) + 1;
:递归地计算左子树的深度,然后加 1(算上从node
到其左孩子的这条边)。leftLength
就代表了从node
到其左子树最远叶子节点的路径长度。
b.int rightLength = dfs(node.right) + 1;
:同理,计算从node
到其右子树最远叶子节点的路径长度。
c.ans = Math.max(ans, leftLength + rightLength);
:这是利用“副作用”更新全局结果。leftLength + rightLength
正是穿过当前节点node
的最长路径的长度。用它和当前的全局最大值ans
比较,并保留较大者。
d.return Math.max(leftLength, rightLength);
:这是函数的“主业”。它需要向其父节点报告以node
为根的子树的深度,这个深度就是从node
出发往下走的最长路径,即leftLength
和rightLength
中的较大者。
- 递归的终止条件 (Base Case):
通过这种方式,算法在一次后序遍历的过程中,既计算了每个节点作为子问题所需的深度信息,又顺带更新了全局的直径信息,非常高效。
完整代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
// ans 是一个成员变量,用于在递归过程中记录和更新全局的最大直径。
private int ans = 0;
/**
* 计算二叉树的直径。
* @param root 二叉树的根节点
* @return 树的直径
*/
public int diameterOfBinaryTree(TreeNode root) {
// 调用递归辅助函数,该函数会通过副作用更新 ans 的值。
dfs(root);
// 返回最终记录的全局最大直径。
return ans;
}
/**
* 深度优先搜索(DFS)的递归辅助函数。
* 主要职责:返回以 node 为根的子树的最大深度(路径上的边数)。
* 副作用:计算穿过 node 的直径,并更新全局最大直径 ans。
* @param node 当前子树的根节点
* @return 以 node 为根的子树的最大深度
*/
private int dfs(TreeNode node) {
// 递归终止条件:如果节点为空,其深度视为 -1。
// 返回 -1 的好处是,其父节点计算 ... + 1 后,空子树的深度贡献为 0。
if (node == null) {
return -1;
}
// 步骤 1: 递归计算左子树的深度,并加 1 得到从当前节点到左子树最远叶子的路径长度。
int leftLength = dfs(node.left) + 1;
// 步骤 2: 递归计算右子树的深度,并加 1 得到从当前节点到右子树最远叶子的路径长度。
int rightLength = dfs(node.right) + 1;
// 步骤 3 (副作用): 计算穿过当前节点 node 的直径 (左路径 + 右路径)。
// 并用它来更新全局最大直径 ans。
ans = Math.max(ans, leftLength + rightLength);
// 步骤 4 (主职责): 返回以 node 为根的子树的深度,即左右路径中的较长者。
// 这个返回值是给 node 的父节点使用的。
return Math.max(leftLength, rightLength);
}
}
时空复杂度
时间复杂度:O(N)
- 节点访问:在整个
dfs
递归过程中,每个节点都会被访问一次。当访问一个节点时,会执行一些常数时间的操作(Math.max
,加法,两次递归调用)。 - 综合分析:
- 算法的总时间消耗与树中的节点数量
N
成正比。 - 因此,时间复杂度为 O(N)。
- 算法的总时间消耗与树中的节点数量
空间复杂度:O(H),最坏情况 O(N)
主要存储开销:该算法的额外空间开销主要来自 递归调用栈 (Call Stack)。递归的深度取决于树的高度
H
。递归栈深度分析:
- 对于一个平衡二叉树,树的高度
H
约等于log N
。此时,递归栈的空间复杂度为 O(log N)。 - 对于一个极不平衡的二叉树(例如,一个链状的树),树的高度
H
可能等于N
。此时,递归栈的空间复杂度会达到 O(N)。
- 对于一个平衡二叉树,树的高度
综合分析:
算法的空间复杂度由递归调用栈的深度决定,即树的高度 H
。因此,空间复杂度为 O(H)。在最坏情况下(链状树),空间复杂度为 O(N)。