深度优先搜索:如何在二叉树中找出“好节点”【迭代法、状态管理技巧、DFS】

一、题目分析

题目要求:

给定一棵二叉树,定义一个"好节点"为:从根节点到该节点路径上,没有任何节点的值比该节点的值大。要求我们返回二叉树中好节点的数量。

示例:

  • 示例 1:

    复制代码
    输入: [3,1,4,3,null,1,5]
    输出: 4

    解释: 根节点 3 是好节点。节点 4 是路径 3->4 中的最大值,节点 5 是路径 3->4->5 中的最大值,节点 3 是路径 3->1->3 中的最大值,所以总共有 4 个好节点。

  • 示例 2:

    复制代码
    输入: [3,3,null,4,2]
    输出: 3

    解释: 根节点 3 是好节点。节点 4 是路径 3->4 中的最大值。节点 3 是路径 3->3 中的最大值。节点 2 不是好节点,因为路径上有个节点的值比 2 大。

  • 示例 3:

    复制代码
    输入: [1]
    输出: 1

    解释: 根节点是好节点。

1448. 统计二叉树中好节点的数目 - 力扣(LeetCode)

二、解题思路

我们需要遍历整棵二叉树,并在遍历的过程中记录当前路径上遇到的最大值。对于每个节点,如果当前节点的值大于等于当前路径的最大值,那么这个节点就是"好节点"。

三、代码实现

我们可以通过 深度优先搜索(DFS) 来实现这个遍历,并且在递归的过程中传递当前路径上的最大值。

1. 递归 DFS 实现

递归的 DFS 方法非常适合这种遍历二叉树的问题。

代码如下:

cpp 复制代码
class Solution {
public:
    int goodNodes(TreeNode* root, int maxVal = INT_MIN) {
        if (!root) return 0;
        int cnt = 0;
        if (root->val >= maxVal) {
            cnt++;
            maxVal = root->val;
        }
        cnt += goodNodes(root->left, maxVal);
        cnt += goodNodes(root->right, maxVal);
        return cnt;
    }
};

代码讲解:

  1. 递归边界: 如果当前节点为空(nullptr),返回 0,表示没有"好节点"。
  2. 判断当前节点是否为"好节点": 如果当前节点的值大于等于 maxVal(当前路径的最大值),那么这个节点就是好节点,cnt 加 1,同时更新 maxVal 为当前节点的值。
  3. 递归遍历左、右子树: 继续遍历当前节点的左、右子树,传递更新后的 maxVal。
  4. 累加"好节点"数量: 返回左右子树的"好节点"数量之和。

示例分析

假设有如下二叉树:

复制代码
       3
      / \
     1   4
    /   / \
   3   1   5
  1. 根节点 3: 从根到自己是好节点,maxVal = 3,计数 1。
  2. 左子节点 1: 路径 [3, 1],1 < maxVal,不是好节点。
  3. 左子节点的左子节点 3: 路径 [3, 1, 3],3 >= maxVal,是好节点,计数 2。
  4. 右子节点 4: 路径 [3, 4],4 > maxVal,是好节点,更新 maxVal 为 4,计数 3。
  5. 右子节点的左子节点 1: 路径 [3, 4, 1],1 < maxVal,不是好节点。
  6. 右子节点的右子节点 5: 路径 [3, 4, 5],5 > maxVal,是好节点,计数 4。

最终,输出 4。

2. 迭代 DFS 实现

如果使用栈来实现迭代的 DFS,需要额外注意在回溯到父节点时如何正确恢复 maxVal。

迭代代码:

cpp 复制代码
class Solution {
public:
    int goodNodes(TreeNode* root) {
        if (!root) return 0;
        
        stack<pair<TreeNode*, int>> s; // 栈中存放节点和当前路径的最大值
        s.push({root, root->val});
        int cnt = 0;

        while (!s.empty()) {
            auto [node, maxVal] = s.top();
            s.pop();
            
            if (node->val >= maxVal) {
                cnt++;
                maxVal = node->val; // 更新路径最大值
            }
            
            if (node->right) s.push({node->right, maxVal});
            if (node->left) s.push({node->left, maxVal});
        }

        return cnt;
    }
};

代码讲解:

  • 栈的定义 :使用一个栈 s,栈中的每个元素是一个 pair,其中 first 是当前遍历的节点,second 是到达该节点时路径中的最大值。

    cpp 复制代码
    stack<pair<TreeNode*, int>> s;
  • 初始值:将根节点和它的值作为初始最大值一起入栈。

    cpp 复制代码
    s.push({root, root->val});
  • 遍历过程:每次从栈顶取出一个元素,比较当前节点的值和当前路径的最大值。

    cpp 复制代码
    while (!s.empty()) {
        auto [node, maxx] = s.top();
        s.pop();
    
        // 判断当前节点是否为好节点
        if (node->val >= maxx) {
            cnt++;
        }
    
        // 更新路径最大值
        maxx = max(maxx, node->val);
    
        // 将左右子节点入栈
        if (node->right) s.push({node->right, maxx});
        if (node->left) s.push({node->left, maxx});
    }

注意细节:

  • 路径中的最大值传递:在遍历到每个节点时,必须确保路径中的最大值被正确传递到子节点。如果不正确传递,可能会导致判断错误。
  • 遍历顺序:由于栈的特性(后进先出),在处理当前节点时,需要先将右子节点入栈,然后再将左子节点入栈。这保证了在模拟递归时,左子树会先于右子树遍历。
  • 初始值的选择 :栈的初始状态需要包含根节点和根节点的值,因为根节点的值在它自己的路径中始终是最大的。
    假设输入的二叉树是 [3, 1, 4, 3, null, 1, 5],即树的结构如下:

结合示例解释

复制代码
        3
       / \
      1   4
     /   / \
    3   1   5
  1. 初始化 :s.push({root, 3}),栈初始为 [(3, 3)]。
  2. 第一步 :弹出 (3, 3),3 >= 3,是好节点,cnt++,maxx 保持为 3,将右子节点 (4, 3) 和左子节点 (1, 3) 入栈。
  3. 第二步 :弹出 (1, 3),1 < 3,不是好节点,maxx 保持为 3,将子节点 (3, 3) 入栈。
  4. 第三步 :弹出 (3, 3),3 >= 3,是好节点,cnt++,maxx 保持为 3,无子节点。
  5. 第四步 :弹出 (4, 3),4 > 3,是好节点,cnt++,更新 maxx = 4,将右子节点 (5, 4) 和左子节点 (1, 4) 入栈。
  6. 第五步 :弹出 (1, 4),1 < 4,不是好节点,maxx 保持为 4,无子节点。
  7. 第六步 :弹出 (5, 4),5 > 4,是好节点,cnt++,maxx 更新为 5,无子节点。

最终结果:cnt = 4。

四、总结

通过 DFS 遍历二叉树,实时跟踪当前路径的最大值来判断节点是否为"好节点"。递归与迭代两种方法各有优势,递归写法简洁直观,迭代写法则适用于避免递归栈溢出。理解如何在 DFS 中维护状态是解决二叉树类问题的关键。

相关推荐
hetao17338375 小时前
2026-10-03~04 hetao1733837 的刷题记录
c++·算法
Zootopia6266 小时前
飞行力学知识梳理1|飞行性能与稳定性
人工智能·python·算法·机器学习·无人机·学习方法·信息与通信
longlongzihan6 小时前
从LeetCode 70. 爬楼梯看动态规划:如何精准推导状态转移方程?
c++·算法·leetcode·动态规划
2601_960356387 小时前
2027校招采购岗解析:数学基础如何迁移到需求预测、成本与供应商分析
人工智能·算法
h_a_o777oah7 小时前
【树论】树链剖分:将树形结构转化为线性结构以处理树上路径问题(洛谷 P3384 附C++代码模板)
数据结构·算法·线段树·acm·树链剖分·树论·重链剖分
HEJOO97 小时前
指针的算术运算详解
java·数据结构·算法
Coder-magician8 小时前
《代码随想录》刷题打卡day47:并查集-part01
数据结构·算法·图论
垆边人似月.8 小时前
VLAN 资源池(100~200 分档)
数据结构·算法
潼心1412o9 小时前
C++初阶(长期更新)第6讲:类和对象(下)
开发语言·c++·算法
星光开发者9 小时前
基于决策树随机森林算法的小说推荐系统的设计与实现-计算机毕设【课程设计】78485
vue.js·算法·微信小程序·django·php·课程设计·express