543刷题记录
543. 二叉树的直径
你提供的这个写法非常棒,它不仅是正确的,而且是解决这个问题的最优解法之一,时间复杂度为 O(N),N 是节点的数量。
这是一种非常精妙的“一次遍历”解法,它在一次深度优先搜索(DFS)中同时完成了两项任务。我们来详细分析一下它为什么是正确的。
核心思想:函数的多重职责
这个算法的核心在于 dfs 这个递归函数。它被设计用来同时做两件事:
-
[返回值]:
dfs(node)返回 以node为起点的、向下延伸的最长路径长度(边数)。我们称这个为node的“最大深度”或“最大链长”。 -
[副作用]:
dfs(node)在执行过程中,会顺便计算以node为“转折点”的直径,并用这个值去更新全局(在C++中是被lambda捕获)的ans变量。
逐行代码分析
C++
class Solution {
public:int diameterOfBinaryTree(TreeNode* root) {// 1. 'ans' : // 这是我们的“全局”最大直径。// [&] 捕获允许 dfs lambda 修改它。int ans = 0; // 2. 'dfs' lambda:// C++23 风格的递归 lambda。// auto&& dfs 是为了让 lambda 内部可以调用自己。// '-> int' 指明了返回类型是 int。auto dfs = [&](this auto&& dfs, TreeNode* node) -> int {// 3. 基础情况 (Base Case):// if (node == nullptr) { return -1; }// 这是整个算法最精妙的地方。// 为什么返回 -1?// 考虑一个叶子节点(leaf),它的左右孩子都是 nullptr。// 当它计算 l_len 时,会调用 dfs(nullptr),返回 -1。// 然后 l_len = -1 + 1 = 0。// 这完全正确:一个叶子节点,它向左下方延伸的最长链长是 0。// 这个 -1 是一个“哨兵值”,它使得 +1 运算对于空节点和叶子节点都能完美生效。if (node == nullptr) {return -1; }// 4. 递归计算 (后序遍历)// int l_len = dfs(node->left) + 1;// int r_len = dfs(node->right) + 1;// 我们来分析 l_len:// - dfs(node->left) 返回了左子树的“最大链长”。// - +1 加上了从 'node' 到 'node->left' 的这条边。// - 所以, 'l_len' = 从 'node' 出发,向左下方延伸的最长路径长度。// 例子:// - 如果 node->left 是 nullptr, dfs(nullptr)返回-1, l_len = -1 + 1 = 0。 (正确)// - 如果 node->left 是叶子, dfs(leaf)返回0, l_len = 0 + 1 = 1。 (正确)int l_len = dfs(node->left) + 1; int r_len = dfs(node->right) + 1;// 5. [副作用]:更新全局最大直径// ans = max(ans, l_len + r_len);// 这是解题的核心。// 在当前节点 'node',我们已经知道了:// - l_len: 向左的最长链// - r_len: 向右的最长链// 如果最长路径以 'node' 为“转折点”,那么这条路径的长度就是 l_len + r_len。// 我们用这个“以node为中心的直径”去和已经找到的“全局最大直径 ans”比较,// 并更新 ans。// 注意:真正的直径可能并不经过 'root',它可能完全在某个子树中。// 但因为我们对 *每个节点* 都执行这个操作,所以我们一定会找到那个// 真正的“转折点”并计算出正确的最大值。ans = max(ans, l_len + r_len);// 6. [返回值]:返回当前节点的最大链长// return max(l_len, r_len);// 这一步是为了 'node' 的父节点准备的。// 'node' 的父节点并不关心“以node为转折点的直径”(l_len + r_len)。// 'node' 的父节点只关心:“如果我把 'node' 作为我的路径的一部分,// 我能向下延伸多远?”// 答案是:取 'node' 向左和向右中较长的那一条链。// 所以我们返回 max(l_len, r_len)。return max(l_len, r_len); };// 7. 启动 DFSdfs(root);// 8. 返回结果// 经过 dfs(root) 的调用,ans 已经被(通过副作用)// 更新为整棵树中所有节点计算出的 (l_len + r_len) 的最大值。return ans;}
};
总结
这个写法是正确的,因为它巧妙地利用了递归的返回值和副作用:
-
返回值
max(l_len, r_len):用于向上传递“深度”信息,供父节点计算它自己的l_len/r_len。 -
副作用
ans = max(ans, l_len + r_len):用于在当前节点结合左右子树信息,计算“以当前节点为中心的直径”,并更新全局答案。
return -1 的基础情况是让这一切数学关系(+1)得以自洽的关键。
