这哥们儿估计当场懵了。

干活干得好好的,大群里突然被艾特,说你别直接 git pull 啊,提交记录会被你拉乱,后面查问题都不好查。那场面,换谁谁不尴尬。

更真实的是,他还补了一句:我都这么用三年了,没人说过啊。 哈哈,这才是职场里最常见的坑。不是你不会,而是很多东西没人教,大家默认你应该懂。等真出事了,才在群里公开处刑。

打开网易新闻 查看精彩图片

其实不少人用 git pull ,就是图省事,拉一下代码继续干。至于背后到底是 merge 还是 rebase,提交线会不会绕成一团,平时根本没人细看。

但大厂项目人一多,这玩意儿确实容易炸。你一个 merge 节点,我一个 merge 节点,历史记录看着跟毛线球一样。出了 bug 再追,脑瓜子都疼。

二叉树的直径,坑不在递归,坑在你以为自己算的是“最长路径”,结果代码里一直在算“树高”。

这题我第一次看也觉得简单,左子树高度 + 右子树高度,不就完了么。问题是,最长路径不一定经过根节点。它可能藏在某个左子树里面,也可能藏在右子树里面,根节点压根没参与。

比如这棵树:

 1
/ \
2 3
/ \
4 5

最长路径是:

4 -> 2 -> 5

长度是 2 条边。

注意,是边数,不是节点数。这里很多人会顺手返回 3,因为路径上有 3 个节点。LeetCode 这题要的是边数,这个地方我一般会先盯一眼,不然调半天都觉得自己没错。

这题真正好用的做法,是递归算每个节点往下能走多深。顺手在递归过程中,把“经过当前节点的最长路径”更新一下。

代码可以这么写:

functiondiameterOfBinaryTree(root) {
let maxPath = 0;

functionheight(node) {
if (node === ) {
return0;
}

const leftHeight = height(node.left);
const rightHeight = height(node.right);

// 当前节点如果作为拐点,最长路径就是左边深度 + 右边深度
const pathThroughNode = leftHeight + rightHeight;

if (pathThroughNode > maxPath) {
maxPath = pathThroughNode;
}

// 返回给父节点时,只能选一边往上接
returnMath.max(leftHeight, rightHeight) + 1;
}

height(root);
return maxPath;
}

这里有个细节别写歪了。

更新答案时用的是:

leftHeight + rightHeight

返回高度时用的是:

Math.max(leftHeight, rightHeight) + 1

这两个东西不是一回事。

当前节点自己算直径时,可以左边拿一条,右边拿一条,拼起来。

但当前节点往父节点返回时,只能返回一条链。你不可能左手牵着左子树,右手牵着右子树,再让父节点继续接上去,那就不是一条路径了,树都被你走成网了。

可以拿刚才那棵树跑一下。

节点 4,高度是 1。

节点 5,高度是 1。

到了节点 2,左边高度 1,右边高度 1,所以经过节点 2 的路径长度是:

1 + 1 = 2

maxPath 更新为 2。

节点 2 往上返回高度时,只能返回:

Math.max(1, 1) + 1 = 2

到了根节点 1,左边高度 2,右边高度 1,经过根节点的路径长度是 3,也就是:

4 -> 2 -> 1 -> 3

所以最终答案会变成 3。

这就是递归这题的手感:一边向下拿高度,一边顺手更新全局最长路径。

如果非要把它写成“每个节点都重新算左右高度”,也能做,但那种写法会重复扫很多节点。树一歪,时间复杂度就不好看了。

上面这个写法,每个节点只进一次递归。

时间复杂度 O(n)。

空间复杂度看递归栈,最坏 O(n),树比较平衡就是 O(log n)。