【LeetCode】树的DFS(前序、中序、后序)精选10题

目录

前序遍历:

1. 二叉树的所有路径(简单)

2. 求根节点到叶节点数字之和(简单)

3. 路径总和 III(中等)

中序遍历:

1. 递增顺序搜索树(简单)

2. 验证二叉搜索树(中等)

3. 二叉搜索树中第K小的元素(中等)

4. 把二叉搜索树转换为累加树(中等)

后序遍历:

1. 计算布尔二叉树的值(简单)

2. 二叉树剪枝(中等)

3. 二叉树中的最大路径和(困难)


二叉树的深度优先搜索又可以细分为前序遍历、中序遍历和后序遍历。

因为树的定义本身就是递归定义,因此采用递归的方法去实现树的三种遍历不仅容易理解而且代码很简洁。并且前中后序三种遍历的唯一区别就是访问根节点的时机不同,在做题的时候,选择一个适当的遍历顺序,对于算法的理解是非常有帮助的。

前序遍历:

前序遍历可以给左右两棵子树传递信息。

1. 二叉树的所有路径(简单)

路径以字符串形式存储,从根节点开始遍历,每次遍历时将当前节点的值加入到父节点传递的路径中,如果该节点为叶子节点,将路径存储到答案数组中。否则,将"->"加入到路径中并递归遍历该节点的左右子树。

该问题需要一个类的成员变量vector<string> ans作为答案。

重复的子问题——函数头设计

给一个节点root和它的父节点要往下传递的路径path。

void dfs(TreeNode* root, string path)

子问题在做什么——函数体设计

  1. 更新path:path += to_string(root->val);
  2. 如果是叶子结点:if (root->left == nullptr && root->right == nullptr)    {ans.push_back(path);    return;}
  3. 将"->"加入到路径中:path += "->";
  4. 递归左右子树:dfs(root->left, path);    dfs(root->right, path);

递归出口

当前节点是空节点,直接返回。

class Solution {
public:
    vector<string> binaryTreePaths(TreeNode* root) {
        dfs(root, "");
        return ans;
    }

private:
    void dfs(TreeNode* root, string path)
    {
        if (root == nullptr)
            return;

        path += to_string(root->val);
        if (root->left == nullptr && root->right == nullptr)
        {
            ans.push_back(path);
            return;
        }
        path += "->";
        dfs(root->left, path);
        dfs(root->right, path);
    }

    vector<string> ans;
};

2. 求根节点到叶节点数字之和(简单)

从根节点开始遍历,每次遍历时将当前节点的值和父节点传递的值整合,如果该节点为叶子节点,将整合后的值加到答案中。否则,递归遍历该节点的左右子树。

该问题需要一个类的成员变量int ans作为答案。

重复的子问题——函数头设计

给一个节点root和它的父节点要往下传递的值num。

void dfs(TreeNode* root, int num)

子问题在做什么——函数体设计

  1. 更新num:num = num * 10 + root->val;
  2. 如果是叶子结点:if (root->left == nullptr && root->right == nullptr)    {ans += num;    return;}
  3. 递归左右子树:dfs(root->left, num);    dfs(root->right, num);

递归出口

当前节点是空结点,直接返回。

class Solution {
public:
    int sumNumbers(TreeNode* root) {
        dfs(root, 0);
        return ans;
    }

private:
    void dfs(TreeNode* root, int num)
    {
        if (root == nullptr)
            return;

        num = num * 10 + root->val;
        if (root->left == nullptr && root->right == nullptr)
        {
            ans += num;
            return;
        }
        dfs(root->left, num);
        dfs(root->right, num);
    }

    int ans;
};

3. 路径总和 III(中等)

路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

节点1->节点2的路径 = 根节点->节点2的路径 - 根节点->节点1的路径

树的前缀和+回溯问题。

用path记录从根节点到当前节点的路径和(前缀和),用哈希表记录当前节点以上的出现的前缀和及出现的次数。

重复的子问题——函数头设计

给一个节点root和它的父节点要往下传递的路径path,还有targetSum。

返回值是二叉树中等于targetSum的路径数目。

int dfs(TreeNode* root, int targetSum, long long path)

子问题在做什么——函数体设计

  1. 更新path:path += root->val;
  2. 如果path-targetSum出现过,记录出现次数,没有出现过则出现次数为0:
    int ans = hash.count(path - targetSum) ? hash[path - targetSum] : 0;
  3. 当前节点的前缀和在哈希表中的值++:hash[path]++;
  4. 递归左右子树,把左右子树中等于targetSum的路径数目添加进答案中:
    ans += dfs(root->left, targetSum, path);    ans += dfs(root->right, targetSum, path);
  5. 回溯:hash[path]--;
  6. 返回二叉树中等于targetSum的路径数目:return ans;

递归出口

当前节点是空结点,直接返回0。

class Solution {
public:
    int pathSum(TreeNode* root, int targetSum) {
        hash[0] = 1;
        return dfs(root, targetSum, 0);
    }

private:
    int dfs(TreeNode* root, int targetSum, long long path)
    {
        if (root == nullptr)
            return 0;

        // 更新path
        path += root->val;
        // 如果path-targetSum出现过,记录出现次数,没有出现过则出现次数为0
        int ans = hash.count(path - targetSum) ? hash[path - targetSum] : 0;
        // 当前节点的前缀和在哈希表中的值++
        hash[path]++;

        // 递归左右子树,把左右子树中等于targetSum的路径数目添加进答案中
        ans += dfs(root->left, targetSum, path);
        ans += dfs(root->right, targetSum, path);

        // 回溯
        hash[path]--;

        return ans;
    }

    long long path; // 从根节点到当前节点的路径和(前缀和)
    unordered_map<long long, int> hash; // 记录当前节点以上的出现的前缀和及出现的次数
};

中序遍历:

如果一棵树是二叉搜索树,那么它的中序遍历的结果一定是一个严格递增的序列。

1. 递增顺序搜索树(简单)

中序遍历,每遍历到一个节点,就把前驱结点的right指针指向当前节点。

就像插入链表节点一样,构造一个哨兵头节点和尾节点,将节点按中序遍历的顺序依次插入,由于递归函数要用到尾节点,所以将尾节点设置成类的成员变量。

重复的子问题——函数头设计

void dfs(TreeNode* root)

子问题在做什么——函数体设计

  1. dfs(root->left);
  2. tail->right = root;    root->left = nullptr;    tail = tail->right;
  3. dfs(root->right);

递归出口

当前节点是空节点,直接返回。

class Solution {
public:
    TreeNode* increasingBST(TreeNode* root) {
        TreeNode* preHead = new TreeNode;
        tail = preHead;
        dfs(root);
        return preHead->right;
    }

private:
    void dfs(TreeNode* root)
    {
        if (root == nullptr)
            return;

        dfs(root->left);
        tail->right = root;
        root->left = nullptr;
        tail = tail->right;
        dfs(root->right);
    }

    TreeNode* tail;
};

2. 验证二叉搜索树(中等)

定义一个类的成员变量,用来记录中序遍历过程中的前驱节点的值。那么就可以在中序遍历的过程中,先判断是否和前驱结点构成递增序列,如果递增,修改前驱结点的值为当前结点的值。

重复的子问题——函数头设计

bool isValidBST(TreeNode* root)

子问题在做什么——函数体设计

先递归判断左子树是否是二叉搜索树,如果不是,返回false(剪枝);

然后判断当前节点的值是否>前驱节点的值,如果不是,返回false(剪枝),如果是,更新prev为当前节点的值;

然后递归判断右子树是否是二叉搜索树,如果不是,返回false(剪枝);

我们已经把该剪的枝剪完了,最后返回true。

  1. 剪枝:if (isValidBST(root->left) == false)    return false;
  2. 剪枝:if (root->val <= prev)    return false;
  3. 更新:prev = root->val;
  4. 剪枝:if (isValidBST(root->right) == false)    return false;
  5. return true;

递归出口

当前节点是空节点,返回true。

class Solution {
public:
    bool isValidBST(TreeNode* root) {
        if (root == nullptr)
            return true;

        // 判断左子树是否是二叉搜索树,如果不是二叉搜索树,则剪枝
        if (isValidBST(root->left) == false)
            return false;

        // 判断当前节点的值是否>前驱节点的值,如果不是,则剪枝
        if (root->val <= prev)
            return false;
        // 更新prev
        prev = root->val;

        // 判断右子树是否是二叉搜索树,如果不是二叉搜索树,则剪枝
        if (isValidBST(root->right) == false)
            return false;

        return true;
    }

private:
    long long prev = LONG_LONG_MIN; // 测试用例卡边界值,初始值要比INT_MIN小
};

3. 二叉搜索树中第K小的元素(中等)

我们只需要找到中序遍历序列的第k个节点即可。定义一个全局的计数器count,初始化为k,每扫描一个节点就count--,直到count==0时,扫描到第k个节点,记录答案ans(类的成员变量)。记录结果后,后续的遍历即失去意义,应提前返回。

重复的子问题——函数头设计

给一个节点,让当前节点的子树进行中序遍历。

void dfs(TreeNode* root)

子问题在做什么——函数体设计

先对左子树进行中序遍历,再扫描当前节点,执行count--,再判断count是否为0,==0则记录ans然后直接返回,!=0则继续对右子树进行中序遍历。

  1. dfs(root->left);
  2. if (--count == 0)    {ans = root->val;    return;}
  3. dfs(root->right);

递归出口

当前节点是空节点,直接返回。

class Solution {
public:
    int kthSmallest(TreeNode* root, int k) {
        count = k;
        dfs(root);
        return ans;
    }

private:
    void dfs(TreeNode* root)
    {
        if (root == nullptr)
            return;

        dfs(root->left);
        if (--count == 0)
        {
            ans = root->val;
            return;
        }
        dfs(root->right);
    }

    int count;
    int ans;
};

4. 把二叉搜索树转换为累加树(中等)

反序中序遍历:按照节点值从大到小遍历,先遍历右子树,再操作根节点,再遍历左子树。

该问题需要一个类的成员变量int sum记录所有比当前节点值大的节点值之和。

重复的子问题——函数头设计

TreeNode* convertBST(TreeNode* root)

子问题在做什么——函数体设计

  1. convertBST(root->right);
  2. sum += root->val;    root->val = sum;
  3. convertBST(root->left);
  4. return root;

递归出口

当前节点是空节点,返回nullptr。

class Solution {
public:
    TreeNode* convertBST(TreeNode* root) {
        if (root == nullptr)
            return nullptr;

        convertBST(root->right);
        sum += root->val;
        root->val = sum;
        convertBST(root->left);
        return root;
    }

private:
    int sum = 0;
};

后序遍历:

先递归处理左右子树,再处理当前节点,就是后序遍历。

1. 计算布尔二叉树的值(简单)

重复的子问题——函数头设计

bool evaluateTree(TreeNode* root)

子问题在做什么——函数体设计

递归求得左右子树的布尔值,然后将该节点的运算符对两个子树的值进行运算。

  1. bool left = evaluateTree(root->left);    bool right = evaluateTree(root->right);
  2. 非叶子节点值为2表示逻辑或:return left || right;
    非叶子节点值为3表示逻辑与:return left && right;

递归出口

当前节点是叶子节点时,直接返回其布尔值。

class Solution {
public:
    bool evaluateTree(TreeNode* root) {
        if (root->left == nullptr)
            return root->val;

        bool left = evaluateTree(root->left);
        bool right = evaluateTree(root->right);
        return root->val == 2 ? left || right : left && right;
    }
};

2. 二叉树剪枝(中等)

重复的子问题——函数头设计

TreeNode* pruneTree(TreeNode* root)

子问题在做什么——函数体设计

递归处理左右子树,并将新左右子树重新链接到当前节点;

如果当前节点是值为0的叶子节点,将其移除;

如果不是,返回当前节点。

  1. root->left = pruneTree(root->left);
  2. root->right = pruneTree(root->right);
  3. if (root->left == nullptr && root->right == nullptr && root->val == 0)    return nullptr;
  4. return root;

递归出口

当前节点是空节点,返回nullptr。

class Solution {
public:
    TreeNode* pruneTree(TreeNode* root) { 
        if (root == nullptr)
            return nullptr;    
        
        root->left = pruneTree(root->left);
        root->right = pruneTree(root->right);
        if (root->left == nullptr && root->right == nullptr && root->val == 0)
        {
            // delete root; //面试题如果节点是new出来的,需要delete,防止内存泄露,笔试题可以不写
            return nullptr;
        }
        return root;
    }
};

3. 二叉树中的最大路径和(困难)

创建一个类的成员变量ans,记录左子树->根节点->右子树的最大路径和。如果左子树贡献的最大路径和< 0,说明没有必要把左子树中的路径加入进去,认为左子树的贡献值为0即可,右子树也同理。

重复的子问题——函数头设计

函数返回值是从当前节点前往左子树或右子树的路径和的最大值

int dfs(TreeNode* root)

子问题在做什么——函数体设计

  1. 递归左右子树,分别计算出它们的最大贡献值,如果< 0,则认为是0:
    int leftVal = max(dfs(root->left), 0);    int rightVal = max(dfs(root->right), 0);
  2. 计算左子树->当前节点->右子树的最大路径和:
    int rootVal = leftVal + rightVal + root->val;
  3. 更新ans:ans = max(ans, rootVal);
  4. 返回从当前节点前往左子树或右子树的路径和的最大值:
    return root->val + max(leftVal, rightVal);

递归出口

当前节点是空节点,直接返回0。

class Solution {
public:
    int maxPathSum(TreeNode* root) {
        dfs(root);
        return ans;
    }

private:
    int dfs(TreeNode* root)
    {
        if (root == nullptr)
            return 0;

        // 递归左右子树,分别计算出它们的最大贡献值
        int leftVal = max(dfs(root->left), 0);
        int rightVal = max(dfs(root->right), 0);

        // 左子树->当前节点->右子树的最大路径和
        int rootVal = leftVal + rightVal + root->val;

        // 更新ans
        ans = max(ans, rootVal);

        return root->val + max(leftVal, rightVal);
    }

    int ans = INT_MIN;
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/591128.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

8.k8s中网络资源service

目录 一、service资源概述 二、service资源类型 1.ClusterIP类型 2.service的nodeport类型 3.service的loadbalancer类型&#xff08;了解即可&#xff09; 4.service的externalname类型&#xff08;了解即可&#xff09; 三、nodeport的端口范围设置和svc的endpoint列表 1.修…

Jupyter Notebook魔术命令

Jupyter Notebook是一个基于网页的交互式笔记本&#xff0c;支持运行多种编程语言。 Jupyter Notebook 的本质式一个Web应用程序&#xff0c;便于创建和共享文学化程序文档&#xff0c;支持实现代码&#xff0c;数学方程&#xff0c;可视化和markdown。用途包括&#xff1a;数据…

Redis 实战2

系列文章目录 本文将从字典的实现、哈希算法、解决键冲突、rehash、渐进式rehash几方面来阐述 Redis 实战Ⅱ 系列文章目录字典的实现哈希算法解决键冲突rehash渐进式 rehash渐进式 rehash 执行期间的哈希表操作 字典 API总结 字典的实现 Redis 的字典使用哈希表作为底层实现&…

解决layui的bug 在layui tree 组件中 禁用选中父节点后自动选中子节点功能

最近做权限管理后台&#xff0c;用了layui tree 组件&#xff0c;发现选中了父节点后&#xff0c;自动选中了子节点。不满足现实业务需求。所以微调了下源代码。 在用树形组件中&#xff0c;在用文档中 tree.setChecked(demoId, [2, 3]); //批量勾选 id 为 2、3 的节点 用这句…

All In ai,Oracle 23C没了,等来了Oracle 23ai

今年一月份的Blog介绍Oracle命名规则的时候&#xff0c;说到Oracle的命名是紧紧跟随时代浪潮的前言科技的&#xff0c;在文章的最后还大胆预测也许Oracle的下一个版本就叫25A了&#xff0c;结果Oracle根本等不及&#xff0c;把原来已经海量宣传的Oracle 23C直接改名为23ai&…

【Mac】Lightroom Classic 2024 v13.1安装教程

软件介绍 Lightroom Classic 2024是Adobe公司推出的一款专业的数字图像处理软件&#xff0c;旨在为摄影师提供强大的工具和功能&#xff0c;以管理、编辑和分享他们的照片作品。以下是Lightroom Classic 2024的主要特点和功能&#xff1a; 数字照片管理&#xff1a; 提供直观…

k8s集群安装

目录 部署步骤概览 1、基础环境部署 2、docker环境部署 3、配置k8s集群 4、集群初始化 5、安装dashboard软件 写在前面&#xff1a;本文安装单点master多node的k8s集群&#xff0c;主要用于k8s学习或k8s环境测试&#xff1b;部署的是1.23版本&#xff0c;在1.24版本起&am…

Android 官网Ota介绍

构建 OTA 软件包 | Android 开源项目 | Android Open Source Project

【RabbitMQ】可靠性策略(幂等,消息持久化)

MQ可靠性策略 发送者的可靠性问题生产者的重连生产者确认 MQ的可靠性数据持久化Lazy Queue 消费者的可靠性问题消费者确认机制消息失败处理 业务幂等性简答问题 发送者的可靠性问题 生产者的重连 可能存在由于网络波动&#xff0c;出现的客户端连接MQ失败&#xff0c;我们可以…

无人机+无人车:自组网协同技术及应用前景详解

无人车&#xff0c;也被称为自动驾驶汽车、电脑驾驶汽车或轮式移动机器人&#xff0c;是一种通过电脑系统实现无人驾驶的智能汽车。这种汽车依靠人工智能、视觉计算、雷达、监控装置和全球定位系统协同合作&#xff0c;使得电脑可以在没有任何人类主动操作的情况下&#xff0c;…

SpringBoot 基础简介

目录 1. SpringBoot 概述 1.1. 为什么会有springboot 1.1.1. 传统Spring 的两个缺点 1.1.2. Springboot 功能 2. SpringBoot 快速搭建 2.1. 创建Maven项目​编辑​编辑​编辑 2.2. 导入SpringBoot起步依赖 2.3. 定义controller 2.4. 添加引导类 2.5. 启动访问 3. Sprin…

香港理工大学内地事务总监陆海天教授确认出席“边缘智能2024 - AI开发者峰会”并发表主题演讲

隨著AI技術的日新月異&#xff0c;我們正步入一個邊緣計算智能化與分布式AI相互融合的新紀元。這一變革不僅推動了分布式智能創新應用的飛速發展&#xff0c;還使得邊緣智能——這一結合邊緣計算和智能技術的新興領域&#xff0c;逐漸成為引領AI發展的重要力量。通過其分布式和…

在家连学校的服务器

在家连接学校的服务器。 Step1: 首先下载一个vscode的插件 Visual Studio Code - Code Editing. Redefined 我的服务区是ubuntu20.04&#xff0c;x64的&#xff0c;所以下载这个。 Step2: 下载到本地之后&#xff0c;想办法将这个文件拷贝到你的服务器上。 Step3: 解压该包…

零基础该如何自学linux运维?

零基础该如何自学linux运维&#xff1f;以下是建议帮助你入门Linux运维的一些建议。 一、自学建议&#xff1a; 理解基础概念&#xff1a;首先&#xff0c;你需要对Linux操作系统的基本概念有所了解&#xff0c;包括文件系统、用户权限、进程管理等。安装Linux系统&#xff1…

AI-数学-高中-47导数与几何意义

原作者视频&#xff1a;【导数】【考点精华】7导数与几何意义考点解析&#xff08;基础&#xff09;_哔哩哔哩_bilibili 该点处切点的斜率 该点处导函数的值 示例1&#xff1a; 导数问题解决最常用方法&#xff1a;参数分离&#xff0c;在左边函数有解的值域范围内。 示例2&…

Jackson-jr 对比 Jackson

关于Jackson-jr 对比 Jackson 的内容&#xff0c;有人在做了一张下面的图。 简单点来说就 Jackson-jr 是Jackson 的轻量级应用&#xff0c;因为我们在很多时候都用不到 Jackson 的很多复杂功能。 对很多应用来说&#xff0c;我们可能只需要使用简单的 JSON 读写即可。 如我们…

微服务总览

微服务保护 微服务总览 微服务总览 接入层&#xff1a;反向代理功能&#xff0c;可以将用户域名访问的地址以负载均衡的方式代理到网关地址&#xff0c;并且并发能力非常高&#xff0c;并且会采用主备nginx的方式防止nginx寄了&#xff0c;备份nginx监控主nginx状态&#xff0c…

CMakeLists.txt 文件内容分析

一. 简介 前一篇文章学习了针对只有一个 .c源文件&#xff0c;cmake工具是如何使用编译的&#xff0c;文章如下&#xff1a; cmake的使用方法:单个源文件的编译-CSDN博客 本文对 所编写的 CMakeLists.txt文件的内容进行分析。从而了解如何编写一个 CMakeLists.txt文件。 二…

ElasticSearch01(ES简介,安装ES,操作索引,操作文档,RestAPI)【全详解】

目录 一、ES简介 1. 数据库查询的问题 2. ES简介 1 ElasticSearch简介 2 ElasticSearch发展 3. 倒排索引【面试】 1 正向索引 2 倒排索引 4. ES和MySql 5. 小结 二、安装ES 1. 方式1:使用docker安装 1 准备工作 2 创建ElasticSearch容器 3 给ElasticSearch配置i…

百度网盘里的文件怎么打印?

在日常生活和工作中&#xff0c;我们经常需要打印各种文件&#xff0c;包括学习资料、工作报告、合同文件等。有时候&#xff0c;这些文件保存在百度网盘等云存储服务中&#xff0c;我们该如何方便地打印出来呢&#xff1f;今天&#xff0c;就为大家介绍一种便捷的方法——通过…
最新文章