深蓝黑背景封面。左侧一棵九节点二叉树:根节点在顶部,左子树向下分出两条深链,右子树只有一个叶子;一条青色高亮路径从左下最深叶子经 6 条边连到右下最深叶子,路径的最高点是根的左孩子而不是根,其余边为灰色。顶部注「最长通路 6 条边,不经过根节点」。下方卡片写递归设计两条:返回值是「以本节点为端点、只往一边走的最长单边(高度)」,全局答案在每个节点处用「左臂 + 右臂」刷新。底部一行注「返回值设计对了,答案自然浮现」。右侧大标题「二叉树的最大通路长度,递归怎么设计」,副标题「返回高度,途中更新直径」。左上标注「面试经典 · 算法」,右上胶囊标注「Day 03 · 11-20」。 图 1:最长通路的最高点不一定是根,这决定了递归该怎么设计。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。「求二叉树的最大通路长度」——也叫二叉树的直径——是树形 DP 的入门招牌题。题目一句话:任意两个节点之间的最长路径有多少条边。很多人张口就是「左子树高度加右子树高度」,这个答法在根节点上碰巧经常对,但作为通用解法它是错的。

先拆掉那个直觉答法。

「根的左高加右高」隐含了一个假设:最长通路一定穿过根节点。构造一个反例很容易——让根的左子树又深又宽,右子树只有一个叶子。比如左子树里藏着一条 6 条边的通路,它的最高点是根的左孩子;而经过根的路径因为要分一条边给右边的独苗叶子,最多只有 5 条边。只在根上算一次,答案就漏了。

正确的观察是:最长通路一定存在某个「最高点」,路径从这个点左臂下来、右臂上去(允许某条臂长度为 0)。最高点可以是树上任意节点,所以——每个节点都要当一次候选最高点。

递归设计的关键:返回值和全局答案各管一件事。

这是本题真正的考点,很多人把两件事揉在一起就乱了。把它拆干净:

递归函数返回什么?返回「以本节点为端点、只往一边走的最长单边」,也就是高度 h = 1 + max(左子树高, 右子树高)。为什么只能报单边?因为父节点要用这个值接着往上拼路径,而路径穿过父节点时只能从一侧上来,两边都占就不是一条简单路径了。返回值是为父节点服务的。

全局答案在哪更新?在每个节点内部,顺手算一个候选直径 = 左臂 + 右臂,拿它刷新一个全局最大值。左臂加右臂描述的是「以本节点为最高点」的完整通路,这个值不上报、只用来竞争答案。每个节点都算一遍候选,最高点是谁都漏不掉。

一句话总结这个范式:返回值服务父节点,全局量在本节点侧更新。

代码实测。

用 Node.js 22.15.0 运行(node diameter.js),构造的正是上面那棵反例树:

class Node {
  constructor(v, l = null, r = null) { this.v = v; this.l = l; this.r = r; }
}
let best = 0;
function height(node) {
  if (!node) return 0;
  const lh = height(node.l), rh = height(node.r);
  best = Math.max(best, lh + rh);   // 候选直径在本节点侧更新
  return 1 + Math.max(lh, rh);      // 上报给父节点的只是单边高度
}
//        A
//      B   I
//     C D
//    E   F
//   G     H
const root = new Node('A',
  new Node('B',
    new Node('C', new Node('E', new Node('G'))),
    new Node('D', null, new Node('F', null, new Node('H')))),
  new Node('I'));
height(root);
console.log('最大通路长度(边数)=', best);

输出:

最大通路长度(边数)= 6

手工数一遍:G—E—C—B—D—F—H 恰好 6 条边,最高点是 B 而不是根 A;如果在根处算,左高 4 加右高 1 只有 5,漏掉正解。代码与推演一致。

复杂度也顺手交代:每个节点只做一次 O(1) 的取 max 和比较,整棵树一次后序遍历,总时间 O(N),递归栈深度等于树高。如果面试官追问非递归写法,用栈模拟后序遍历即可,候选更新的时机不变。

还有一个表述细节值得提前对齐:通路长度按边数还是节点数算,两种口径差 1,答题前先说清自己用哪种,避免和面试官的参考答案对不上。

深蓝黑背景概念图。左侧一棵九节点二叉树,每个节点旁标注上报高度 h:叶子为 0,向上逐级加 1,根的左孩子 h=3、根 h=4;左孩子处有青色标注框「候选 = 左臂 3 + 右臂 3 = 6,全局最大」,根处灰色标注「候选 = 4 + 1 = 5」,说明最大值在子树中产生、不一定在根。右侧三张卡片:青色卡「递归返回什么」——以本节点为端点、只往一边走的最长路径,h = 1 + max(左, 右),因为穿过父节点时只能选一边;紫色卡「全局答案在哪更新」——在每个节点处算候选直径 = 左臂 + 右臂并取全局最大,每个节点都算一遍就漏不掉;灰卡「复杂度」——每节点 O(1),整树一次遍历 O(N)。底部总结条:树形 DP 范式 —— 返回值服务父节点,全局量在本节点侧更新。 图 2:每个节点既向上报高度,又在本地更新全局候选,两条线互不干扰。

这个范式能迁移到一大片题。

「返回值服务父节点、全局量就地更新」是所有树形 DP 的通用骨架:求最大路径和(节点带权值)只是把左臂右臂换成带权路径和,还要处理负值时宁可不要这条臂;求二叉树最大坡度、最长同值路径,改的也只是候选公式。答题时把这个骨架亮出来,再针对题面套候选公式,面试官就知道你不是背的。

面试现场的推荐节奏:先复述题面确认「通路长度按边数算、允许不过根」,然后主动给出反例说明为什么不能只在根算,再讲返回值与全局量分工,最后写代码并交代 O(N) 复杂度。你做这道题时有没有掉进过「根节点陷阱」?或者被追问过「节点带负权值怎么办」?评论区交流一下。