程序员老鬼

leader当着我的面把数据清空了,我看着他点了一下红色的按钮(清空表数据),正要说快回滚,leader就点到了commit上,就这么提交了

跑路年年有,删库也天天有,关键是这次我在旁边当“人肉监控”。

leader让我用SQL更新数据,我刚拿到账号密码准备登库,他突然说先别动,估计怕实习生手一抖把公司送走。结果他拉我站后面观摩,手速飞起,我眼睁睁看他点了那个红得发亮的按钮——清空表数据。我刚想喊“快回滚!”,他又顺手点了commit,啪一下提交成功。

Image

网友看完都笑疯“删库到跑路,原来是leader带队”。

哈哈,要我说,这声“啊”真的比报警器还响。以后我学乖了:先看备份,再看权限,最后再看leader的鼠标是不是飘到红色区。

面试题:路径总和

那天下班挺晚的,我人已经有点晕了,还在工位上啃一道叫“路径总和”的树题,你们肯定见过那个经典版本:给你一棵二叉树,加上一个 target,总之问你从根结点走到某个叶子,路上节点值加一加,能不能刚好等于 target。听着特像工资发下来刚够还房贷车贷花呗的那种…对吧,走到月底刚好清零,才算“有效路径”。

我当时脑子里第一反应是那种特别蠢的写法:从根往下枚举所有路径,存个 list,然后每条路径算一遍和。想了三秒,算了,这种写法上线肯定被人喷。树这种东西,本来就自带“递归”体质的,你要是不用递归,树都要跟你急。

我先在本子上随手画了个树,根是 5,左子树 4 -> 11 -> 7,2,右边 8 -> 13,4 -> 1。很典型的面试原题那种。然后就开始念叨:走到每个节点的时候,其实我关心的是“从根到这一步的路径和”,以及“我是不是已经到叶子了”。逻辑就俩问题,很简单。

于是敲了个最原始的结构体出来,Java 老三样:

classTreeNode{
int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int v) {
this.val = v;
    }
}

然后正式上菜。我的思路是:一路往下 DFS,每到一个节点,把“当前路径和”带着一起下去;一旦发现到了叶子节点,并且当前和刚好等于目标,就直接返回 true,整个递归链一路冒泡上去。要是左右都走完了还没撞上,就 false。

classSolution{

publicbooleanhasPathSum(TreeNode root, int target){
// 树都没有,还谈啥路径,对吧
if (root == null) {
returnfalse;
        }
return dfs(root, 0, target);
    }

privatebooleandfs(TreeNode node, int currentSum, int target){
if (node == null) {
returnfalse;
        }
int sum = currentSum + node.val;

// 叶子节点:左右都空
if (node.left == null && node.right == null) {
return sum == target;
        }

// 左右随便有一个能搞定,就算成功
return dfs(node.left, sum, target)
                || dfs(node.right, sum, target);
    }
}

这个写完我自己看了下,其实就像你在走夜路,手里拿着个小账本,路过一个节点就记一笔钱,到路的尽头看一下账本上的总和是不是目标数,不是就原路返回接着换条路走。整件事就俩状态:现在走到哪、目前记了多少钱,没有别的花活。

这里容易犯错的一点是,有人会以为“只要中间某个节点的和等于 target 就行”,这个是不对的,题目强调的是“根到叶子的路径”。你中途路过便利店钱刚好花完,那不叫到终点,那叫没钱了走不动了。一定得左右孩子都为空,那才算一条完整路径。

有一次我在排查线上一个数据库性能问题,顺手在草稿本上画了一棵真实业务里的“决策树”,每条路都有一个“代价”,突然想到这题,其实跟我们那堆 if-else 嵌套决策是一个模型:从入口条件一路砍下来,最后落到叶子,就是一个具体行为分支,路径上的代价加起来刚好是预算上限就 OK。

后来我又手贱写了个非递归版本,主要是给那些看见递归就头疼的小伙伴。思路一样,用栈手动把“节点 + 当前和”压进去:

classIterSolution{

publicbooleanhasPathSum(TreeNode root, int target){
if (root == null) returnfalse;

        Deque<TreeNode> nodeStack = new ArrayDeque<>();
        Deque<Integer> sumStack = new ArrayDeque<>();

        nodeStack.push(root);
        sumStack.push(root.val);

while (!nodeStack.isEmpty()) {
            TreeNode node = nodeStack.pop();
int sum = sumStack.pop();

if (node.left == null && node.right == null && sum == target) {
returntrue;
            }

// 注意先右后左,保证顺序跟递归差不多,不过其实无所谓
if (node.right != null) {
                nodeStack.push(node.right);
                sumStack.push(sum + node.right.val);
            }
if (node.left != null) {
                nodeStack.push(node.left);
                sumStack.push(sum + node.left.val);
            }
        }
returnfalse;
    }
}

这个写法就像你自己在模拟一层层递归展开的过程:栈顶是你现在要处理的节点,旁边那一摞 sumStack 就是对应的“走到这里已经花了多少钱”。每次弹出来一对,看看是不是叶子,是不是刚好对账,不对就继续。

顺便吐槽一句,路径总和这种题,在面试官眼里其实不主要是考你会不会写 DFS,大家都会;它更像是一个体检:看你会不会乱改全局变量、会不会忘记判断“叶子”的定义、会不会在 sum 上做减法减到最后整崩溃。还有人喜欢全局搞个 found 标志位,递归里乱七八糟改,最后各种 case 出 bug,我见过不止一个同学现场翻车。

反正吧,这题你写顺手了,以后再遇到什么“从根到叶子求路径呀”“找最小路径和呀”“打印所有路径呀”,其实换汤不换药,就改改递归里那几行逻辑。行了,不说了,我去给今天的工资路径算总和了,看还能剩下几块钱买杯奶茶…