【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;// 更新pathpath += root->val;// 如果path-targetSum出现过,记录出现次数,没有出现过则出现次数为0int 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;// 更新prevprev = 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;// 更新ansans = max(ans, rootVal);return root->val + max(leftVal, rightVal);}int ans = INT_MIN;
};

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C++】深入剖析C++11 initializer_list 新的类功能 可变模板参数

目录 一、std::initializer_list 1、std::initializer_list是什么类型 2、std::initializer_list 的应用场景 ①给自定义容器赋值 ② 传递同类型的数据集合 二、新的类功能 1、默认成员函数 2、关键字default 3、关键字delete 三、可变参数模板 一、std::initialize…

Oracle 数据库全面升级为 23ai

从 11g 到 12c 再到 19c&#xff0c;今天&#xff0c;我们迎来了 23ai &#xff01; “ Oracle AI Vector Search allows documents, images, and relational data that are stored in mission-critical databases to be easily searched based on their conceptual content Ge…

Visual Studio C++ 的一个简单示例

Visual Studio 项目属性设置&#xff1a; 项目属性→C/C→常规→附加包含目录 C:\Intel\include\iconv\include;项目属性→链接器→常规→附加库目录 C:\Intel\include\iconv\lib;项目属性→链接器→输入→附加依赖项 iconv.lib;提示缺少"iconv.dll"&#xff0c;…

架构设计 | 分布式与集群有什么区别

作为一名从业多年的程序员&#xff0c;对于分布式和集群这两种架构有着深入的了解。简单来说&#xff0c;分布式是将一个任务分拆到多个节点共同完成&#xff0c;而集群则是多个节点执行相同的任务。具体来说&#xff0c;分布式和集群的区别体现在以下几个方面&#xff1a; 一…

2024年钉钉群直播回放如何永久保存

工具我已经打包好了&#xff0c;有需要的自己取一下 链接&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;1234 --来自百度网盘超级会员V10的分享 1.首先解压好我给大家准备好的压缩包 2.再把逍遥一仙下载器压缩包也解压一下 3.打开逍遥一仙下载器文件夹里面的M3U8…

开源的贴吧数据查询工具

贴吧数据查询工具 这是一个贴吧数据查询工具&#xff0c;目前仍处于开发阶段。 本地运行 要本地部署这个项目&#xff0c;请 克隆这个仓库并前往项目目录 git clone https://github.com/Dilettante258/tieba-tools.git cd tieba-tools安装依赖 pnpm install运行项目 np…

输入序列太长 gan CGAN

transformer序列长度大导致计算复杂度高 GAN 2. 训练过程 第一阶段&#xff1a;固定「判别器D」&#xff0c;训练「生成器G」。使用一个性能不错的判别器&#xff0c;G不断生成“假数据”&#xff0c;然后给这个D去判断。开始时候&#xff0c;G还很弱&#xff0c;所以很容易被…

Java并发编程面试问题与答案

1. 什么是线程安全&#xff1f; 答&#xff1a; 线程安全意味着多个线程可以同时访问一个类的实例而不引起任何问题或不一致的结果。线程安全的代码会通过同步机制来确保所有线程都能正确地访问共享资源。 2. 解释Java中的synchronized关键字。 答&#xff1a; synchronized…

three.js入门指南

WebGL和Three.js的概念 什么是WebGL WebGL是基于OpenGL ES 2.0的Web标准&#xff0c;可以通过HTML5 Canvas元素作为DOM接口访问。 也就是WebGL是作为OpenGL的网页端入口。它作为一个底层标准&#xff0c;然后我们可以通过JavaScript代码&#xff0c;在网页上实现三维图形的渲…

./build.sh:行1: g++: 未找到命令的错误问题在centos操作系统下面如何解决

目录 g: 未找到命令报错解释g: 未找到命令解决方法 centos操作系统&#xff0c;执行一个bash&#xff0c;bash命令很简单&#xff0c;就是用g编译一个C的程序。报告错误&#xff1a; ./build.sh:行1: g: 未找到命令 g: 未找到命令报错解释 这个错误表明在执行名为 build.sh 的…

【Mac】mac 安装 prometheus 报错 prometheus: prometheus: cannot execute binary file

1、官网下载 Download | Prometheus 这里下载的是prometheus-2.51.2.linux-amd64.tar.gz 2、现象 解压之后启动Prometheus 启动脚本&#xff1a; nohup ./prometheus --config.fileprometheus.yml > prometheus.out 2>&1 & prometheus.out日志文件&#xff…

容器虚拟机 资源 容器自动化构建 动手写docker OCI开放容器标准 云原生技术 k8s

容器虚拟机 容器和虚拟机都是用于资源隔离和管理的技术,它们在资源管理和使用上有所不同。 虚拟机是一种基于软件的模拟技术,可以使得一台物理计算机同时运行多个操作系统和应用程序。虚拟机技术具有资源隔离、灵活性和迁移能力等特点。通过虚拟机,可以实现物理资源的动态…

基于Springboot的校运会管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的校运会管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&a…

谈谈TCP Socket中读取数据的函数---read、recv、readv

read函数 read函数从文件描述符&#xff08;包括TCP Socket&#xff09;中读取数据&#xff0c;并将读取的数据存储到指定的缓冲区中。原型 ssize_t read(int fd, void *buf, size_t count); -fd&#xff1a;要读取数据的文件描述符&#xff0c;可以是TCP Socket -buf&#xf…

Xamarin.Android项目使用ConstraintLayout约束布局

Xamarin.AndroidX.ConstraintLayout Xamarin.Android.Support.Constraint.Layout Xamarin.AndroidX.ConstraintLayout.Solver Xamarin.AndroidX.DataBinding.ViewBinding Xamarin.AndroidX.Legacy.Support.Core.UI Xamarin.AndroidX.Lifecycle.LiveData ![在这里插入图片描述]…

11个2024年热门的AI编码助手

大家好&#xff0c;人工智能&#xff08;AI&#xff09;领域的大型语言模型&#xff08;LLMs&#xff09;已经逐渐发展成熟&#xff0c;并且深入到了我们日常的工作当中。在众多AI应用中&#xff0c;编码助手尤为突出&#xff0c;是开发人员编写更高效、准确无误代码的必备辅助…

公司网页设计与制作

创意与专业相结合——公司网页设计与制作 在当今数字化时代&#xff0c;公司网页已经成为企业展示形象和吸引客户的关键渠道之一。因此&#xff0c;一个引人注目且功能强大的网页设计和制作变得至关重要。成功的公司网页设计与制作需要兼具创意与专业&#xff0c;以确保吸引用户…

微服务总览

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

【非常实战具体】k8s中deployment和StatefulSet构建的pod的区别

在Kubernetes中&#xff0c;Deployment和StatefulSet都是控制器对象&#xff0c;用于管理和扩展应用程序的Pod。它们之间的主要区别在于它们处理Pod的方式和适用的应用程序类型。 以下是Deployment和StatefulSet之间的主要区别&#xff1a; 有状态应用程序 vs 无状态应用程序…

Linux USB转串口设备路径的查找方法

1、USB转串口设备 USB转串口设备是在嵌入式软件开发过程中经常要使用的&#xff0c;常常用于对接各种各样的串口设备。如果一台linux主机上使用多个usb转串口设备时&#xff0c;应用程序中就需要知道自己操作的是哪个串口设备。串口设备在系统上电时&#xff0c;由于驱动加载的…