题目描述
思路解析动画文字版
做法干净利落:DFS 时给每个节点带一个参数 pathMax = 根到它父亲这一路的最大值。进入节点时比一下:如果 当前值 ≥ pathMax,说明来路上没有比它大的,它就是好节点,计数 +1;然后把 pathMax 更新成 max(pathMax, 当前值) 传给左右孩子。下面逐节点看这趟 DFS。
准备 · 整棵树:先看清这棵树:根是 3。接下来从根出发做 DFS——每进入一个节点,拿它的值和「根到这里的路径最大值」比一比:不比它小就是好节点。然后更新路径最大值往下传,子树探完就回溯。我们边走边数。
好节点!3 ≥ 路径最大值 −∞:从根 3 出发。根上方没有任何节点,路径最大值初始是 −∞,所以 3 ≥ −∞ 一定成立——根永远是好节点,计数变成 1。接着把路径最大值更新为 3 传给孩子,往下深入。
不是好节点:1 < 路径最大值 3:走到节点 1,它扛的路径最大值是 3。1 小于 3,说明根到它的路上有比它大的节点,所以它不是好节点,计数保持 1。路径最大值仍是 3,继续往它的子树探。
好节点!3 ≥ 路径最大值 3:深度优先走到节点 3。看它身上扛的「根到这里的路径最大值」是 3,而 3 ≥ 3 成立——路径上没有比它大的,它是好节点!计数 +1 = 2。再把路径最大值更新成 max(3, 3) = 3 继续往下传。
不是好节点:1 < 路径最大值 3:走到节点 1,它扛的路径最大值是 3。1 小于 3,说明根到它的路上有比它大的节点,所以它不是好节点,计数保持 2。路径最大值仍是 3,继续往它的子树探。
回溯 · 离开 1:节点 1 的整棵子树已经探完,回溯:把它从当前路径弹出,路径最大值退回到进它之前的 3(这样它的兄弟分支用的是正确的祖先最大值,不会被这一侧影响),并标蓝表示「这块走过了」。回到节点 3,继续它的另一条岔路。
好节点!6 ≥ 路径最大值 3:深度优先走到节点 6。看它身上扛的「根到这里的路径最大值」是 3,而 6 ≥ 3 成立——路径上没有比它大的,它是好节点!计数 +1 = 3。再把路径最大值更新成 max(3, 6) = 6 继续往下传。
回溯 · 离开 6:节点 6 的整棵子树已经探完,回溯:把它从当前路径弹出,路径最大值退回到进它之前的 3(这样它的兄弟分支用的是正确的祖先最大值,不会被这一侧影响),并标蓝表示「这块走过了」。回到节点 3,继续它的另一条岔路。
回溯 · 离开 3:节点 3 的整棵子树已经探完,回溯:把它从当前路径弹出,路径最大值退回到进它之前的 3(这样它的兄弟分支用的是正确的祖先最大值,不会被这一侧影响),并标蓝表示「这块走过了」。回到节点 1,继续它的另一条岔路。
不是好节点:2 < 路径最大值 3:走到节点 2,它扛的路径最大值是 3。2 小于 3,说明根到它的路上有比它大的节点,所以它不是好节点,计数保持 3。路径最大值仍是 3,继续往它的子树探。
回溯 · 离开 2:节点 2 的整棵子树已经探完,回溯:把它从当前路径弹出,路径最大值退回到进它之前的 3(这样它的兄弟分支用的是正确的祖先最大值,不会被这一侧影响),并标蓝表示「这块走过了」。回到节点 1,继续它的另一条岔路。
回溯 · 离开 1:节点 1 的整棵子树已经探完,回溯:把它从当前路径弹出,路径最大值退回到进它之前的 3(这样它的兄弟分支用的是正确的祖先最大值,不会被这一侧影响),并标蓝表示「这块走过了」。回到节点 3,继续它的另一条岔路。
好节点!4 ≥ 路径最大值 3:深度优先走到节点 4。看它身上扛的「根到这里的路径最大值」是 3,而 4 ≥ 3 成立——路径上没有比它大的,它是好节点!计数 +1 = 4。再把路径最大值更新成 max(3, 4) = 4 继续往下传。
不是好节点:1 < 路径最大值 4:走到节点 1,它扛的路径最大值是 4。1 小于 4,说明根到它的路上有比它大的节点,所以它不是好节点,计数保持 4。路径最大值仍是 4,继续往它的子树探。
好节点!6 ≥ 路径最大值 4:深度优先走到节点 6。看它身上扛的「根到这里的路径最大值」是 4,而 6 ≥ 4 成立——路径上没有比它大的,它是好节点!计数 +1 = 5。再把路径最大值更新成 max(4, 6) = 6 继续往下传。
回溯 · 离开 6:节点 6 的整棵子树已经探完,回溯:把它从当前路径弹出,路径最大值退回到进它之前的 4(这样它的兄弟分支用的是正确的祖先最大值,不会被这一侧影响),并标蓝表示「这块走过了」。回到节点 1,继续它的另一条岔路。
好节点!5 ≥ 路径最大值 4:深度优先走到节点 5。看它身上扛的「根到这里的路径最大值」是 4,而 5 ≥ 4 成立——路径上没有比它大的,它是好节点!计数 +1 = 6。再把路径最大值更新成 max(4, 5) = 5 继续往下传。
回溯 · 离开 5:节点 5 的整棵子树已经探完,回溯:把它从当前路径弹出,路径最大值退回到进它之前的 4(这样它的兄弟分支用的是正确的祖先最大值,不会被这一侧影响),并标蓝表示「这块走过了」。回到节点 1,继续它的另一条岔路。
回溯 · 离开 1:节点 1 的整棵子树已经探完,回溯:把它从当前路径弹出,路径最大值退回到进它之前的 4(这样它的兄弟分支用的是正确的祖先最大值,不会被这一侧影响),并标蓝表示「这块走过了」。回到节点 4,继续它的另一条岔路。
好节点!5 ≥ 路径最大值 4:深度优先走到节点 5。看它身上扛的「根到这里的路径最大值」是 4,而 5 ≥ 4 成立——路径上没有比它大的,它是好节点!计数 +1 = 7。再把路径最大值更新成 max(4, 5) = 5 继续往下传。
好节点!5 ≥ 路径最大值 5:深度优先走到节点 5。看它身上扛的「根到这里的路径最大值」是 5,而 5 ≥ 5 成立——路径上没有比它大的,它是好节点!计数 +1 = 8。再把路径最大值更新成 max(5, 5) = 5 继续往下传。
回溯 · 离开 5:节点 5 的整棵子树已经探完,回溯:把它从当前路径弹出,路径最大值退回到进它之前的 5(这样它的兄弟分支用的是正确的祖先最大值,不会被这一侧影响),并标蓝表示「这块走过了」。回到节点 5,继续它的另一条岔路。
回溯 · 离开 5:节点 5 的整棵子树已经探完,回溯:把它从当前路径弹出,路径最大值退回到进它之前的 4(这样它的兄弟分支用的是正确的祖先最大值,不会被这一侧影响),并标蓝表示「这块走过了」。回到节点 4,继续它的另一条岔路。
回溯 · 离开 4:节点 4 的整棵子树已经探完,回溯:把它从当前路径弹出,路径最大值退回到进它之前的 3(这样它的兄弟分支用的是正确的祖先最大值,不会被这一侧影响),并标蓝表示「这块走过了」。回到节点 3,继续它的另一条岔路。
回溯 · 离开 3:节点 3 的整棵子树已经探完,回溯:把它从当前路径弹出,路径最大值退回到进它之前的 −∞(这样它的兄弟分支用的是正确的祖先最大值,不会被这一侧影响),并标蓝表示「这块走过了」。整棵树都探完了。
答案 · 好节点总数 = 8:整棵树走完,把所有好节点标绿:它们的值是 3, 3, 6, 4, 6, 5, 5, 5,共 8 个。每一个都满足「从根到它的路上没有比它大的」。注意非好节点(如路上撞到过更大的祖先)不计入。答案 = 8。
参考代码
class Solution { public int goodNodes(TreeNode root) { return dfs(root, Integer.MIN_VALUE); // 根上方无节点,max 取最小 } private int dfs(TreeNode node, int pathMax) { if (node == null) return 0; // 空节点:0 个好节点 int good = node.val >= pathMax ? 1 : 0; // 不比路径最大值小 → 好节点 int nextMax = Math.max(pathMax, node.val); // 把最大值带给子树 good += dfs(node.left, nextMax); // 左子树的好节点数 good += dfs(node.right, nextMax); // 右子树的好节点数 return good; }}复杂度
- 时间:O(n),每个节点恰好访问一次
- 空间:O(h),递归栈深 = 树高 h;最坏(链)O(n),平衡 O(log n)
易错点
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
验证二叉搜索树
LeetCode 98 · 中等 · 沿着 树 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题