跑路年年有,删库也天天有,关键是这次我在旁边当“人肉监控”。
Leader让我用 SQL 更新数据,我刚拿到账号密码准备登录数据库,他突然说先别动,估计是怕实习生手一抖把公司送走。结果他拉我站后面观摩,手速飞起,我眼睁睁看他点了那个红得发亮的按钮——清空表数据。我刚想喊“快回滚!”,他又顺手点了 commit,啪一下提交成功。

网友看完都笑疯,“删库到跑路,原来是 leader 带队”。哈哈,要我说,那声“啊”真的比报警器还响。这件事教会我两个习惯:操作前先看备份,再看权限,最后时刻盯着 Leader 的鼠标,看它是不是飘向了红色区域。
那天下班挺晚,人已经有点晕了,还在工位上啃一道叫“路径总和”的树题。你肯定见过经典版本:给你一棵二叉树和一个目标值 target,问是否存在从根结点到某个叶子节点的路径,其节点值之和恰好等于 target。这感觉特像发工资,一路花销,到月底刚好清零,才算一条“有效路径”。
我第一反应是那种最笨的写法:枚举所有路径,存起来挨个算和。但想了三秒就放弃了,这种写法上线肯定挨喷。树这种结构,自带“递归”体质,不用递归都对不起它。
我先在纸上画了个典型的例子:根是 5,左子树 4 -> 11 -> 7,2,右边 8 -> 13,4 -> 1。然后就想,走到每个节点时,我只关心两件事:从根到这儿的累计和,以及我是否已经到达叶子节点。逻辑很简单。
于是先敲了个标准的 TreeNode 结构体,Java 老三样:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int v) {
this.val = v;
}
}
接下来是核心算法。我的思路是:一路向下进行 DFS(深度优先搜索),把当前路径和作为参数传递下去。一旦遇到叶子节点,并且累计和刚好等于目标值,就立即返回 true,并沿着递归链传递上去。如果所有路径走完都没找到,就返回 false。
class Solution {
public boolean hasPathSum(TreeNode root, int target) {
// 树都没有,还谈啥路径
if (root == null) {
return false;
}
return dfs(root, 0, target);
}
private boolean dfs(TreeNode node, int currentSum, int target) {
if (node == null) {
return false;
}
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 就行”。不对,题目强调的是“从根到叶子”的完整路径。你半路钱花光了,那不叫到终点,那叫中途破产。必须左右孩子都为空,才算一条完整路径。
后来我又写了个非递归版本,给那些看见递归就头疼的小伙伴。思路一样,用栈手动模拟状态,一个栈存节点,另一个栈存走到该节点时的累计和。
class IterSolution {
public boolean hasPathSum(TreeNode root, int target) {
if (root == null) return false;
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) {
return true;
}
// 注意先右后左,保证顺序和递归近似,其实顺序无所谓
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);
}
}
return false;
}
}
这个写法就像手动把递归过程一层层展开:栈顶是当前处理的节点,旁边的 sumStack 就是对应的“走到这里花了多少钱”。每次弹出一对检查,不是目标叶子就继续推入子节点。
说实话,路径总和这种题,面试官考的不只是你会不会写 DFS,更像是一次“代码体检”:看你是否会乱用全局变量、是否忘记“叶子节点”的准确定义、是否在累加或减法时把自己绕晕。有人喜欢设置全局 found 标志位,在递归里改来改去,最后各种边界情况出 bug。
这题写熟了,以后再遇到“求根到叶子的所有路径”、“最小路径和”、“路径总和 II(找所有路径)”这些问题,都是换汤不换药,改改递归里的几行逻辑就行。
无论是数据库里的一次手滑,还是算法题里的一次递归,背后都是对规则和状态的精确把握。教训和经验,都值得在 云栈社区 这样专业的技术圈子里聊聊。行了,我去算算今天的“工资路径总和”,看还够不够买杯奶茶提神。