深度优先遍历(DFS)

server/2024/12/14 11:23:09/

深度优先遍历(DFS)

  • 1. 计算布尔二叉树的值
  • 2. 求根节点到叶节点数字之和
  • 3.二叉树剪枝
  • 4.验证二叉搜索树
  • 5. 二叉搜索树中第 K 小的元素
  • 6. 二叉树的所有路径

深度优先遍历(DFS,全称为Depth First Traversal),是我们树或者图这样的数据结构中常⽤的⼀种遍历算法。这个算法会尽可能深的搜索树或者图的分⽀,直到⼀条路径上的所有节点都被遍历完毕,然后再回溯到上⼀层,继续找⼀条路遍历。
在⼆叉树中,常⻅的深度优先遍历为:前序遍历中序遍历以及后序遍历
因为树的定义本⾝就是递归定义,因此采⽤递归的⽅法去实现树的三种遍历不仅容易理解⽽且代码很
简洁。并且前中后序三种遍历的唯⼀区别就是访问根节点的时机不同,在做题的时候,选择⼀个适当的遍历顺序,对于算法的理解是⾮常有帮助的。

1. 计算布尔二叉树的值

题目链接:2331. 计算布尔二叉树的值

算法思路:
这道题需要采用DFS,先访问左边的叶子节点,再访问右边的叶子节点,最后再访问根节点,因此可以采用后序遍历

算法流程:

  1. 根据子问题设计出函数头:只需要一个参数root即可,返回值为boolean类型
  2. 只关心某一个子问题,设计函数体:首先得到左边叶子的值,再得到右边叶子的值,再根据跟节点的值,将左右叶子的值进行操作
  3. 递归的出口:当访问到叶子节点时,返回该叶子节点的值

实现代码:

class Solution {public boolean evaluateTree(TreeNode root) {if(root.left == null && root.right == null) return root.val == 0 ? false : true;boolean left = evaluateTree(root.left);boolean right = evaluateTree(root.right);return root.val == 2 ? left | right : left & right;}
}

2. 求根节点到叶节点数字之和

题目链接:129. 求根节点到叶节点数字之和

解题思路:

  1. 将父亲节点的值与当前节点的值整合起来向下传递,再将传递下来的值与下一个节点的值整合,并向下传递,直到遇到叶子节点时,将其与根节点的值整合后返回
  2. 每个非叶子节点将左子树和右子树返回的值相加,并返回

在这里插入图片描述

递归函数设计:

  1. 函数头:需要包含一个根节点参数,一个前序和参数,返回值为int
  2. 函数体:更新前序和,向下传递,直至遇到叶子节点返回更新后的前序和,非叶子节点将左右返回的前序和相加并返回
  3. 递归的出口:遇到叶子节点,返回前序和与叶子节点值的整合值

实现代码:

class Solution {public int sum;public int sumNumbers(TreeNode root) {return dfs(root, 0);}private int dfs(TreeNode root, int preSum) {preSum = 10 * preSum + root.val;if(root.left == null && root.right == null) return preSum;int ret = 0;if(root.left != null) ret += dfs(root.left, preSum);if(root.right != null) ret += dfs(root.right, preSum);return ret;}
}

3.二叉树剪枝

题目链接:814. 二叉树剪枝

自己的解题思路:
从宏观上看,如果一个节点的左边可以移除,右边也可以移除,并且该节点的值为0,那么返回true给上一个节点,表明以该节点为根节点的树都可以移除。因此,该题需要采用后续遍历

实现代码:

class Solution {public TreeNode pruneTree(TreeNode root) {boolean rootTree = dfs(root);if(rootTree) return null;return root;}private boolean dfs(TreeNode root) {if(root == null) return true;boolean leftTree = dfs(root.left);if(leftTree) root.left = null;boolean rightTree = dfs(root.right);if(rightTree) root.right = null;return leftTree && rightTree && root.val == 0;}
}

答案解题思路:

  1. 先处理掉左子树的剪枝,再处理右子树的剪枝,最后判断当前当前这棵树是否需要剪掉,因此需要采用后序遍历
  2. 如果需要剪掉,那么父亲节点的左(或右)节点就需要置为NULL,即需要向上返回一个NULL;而当不需要剪掉时,为了保证递归解决问题的统一性,向上返回当前节点
  3. 递归出口为当遍历到空节点时,向上返回NULL

实现代码:

class Solution {public TreeNode pruneTree(TreeNode root) {if (root == null)return null;root.left = pruneTree(root.left);root.right = pruneTree(root.right);if (root.left == null && root.right == null && root.val == 0)root = null;return root;}
}

4.验证二叉搜索树

题目链接: 98. 验证二叉搜索树

算法思路:
二叉搜索树的特点是:采用中序遍历得到的是一个严格递增的序列。因此,这道题采用中序遍历来解题。我们需要初始化一个无穷小(要比二叉搜索树里的最小值还要小,而题意-231 <= Node.val <= 231 - 1,比int类型的最小值还要小,可以用long类型的最小值)的全局变量prev用来记录root的前驱节点值,比较prev的值与当前节点的值,然后prev修改为当前节点的值

算法流程:

  1. 定义一个全局变量,初始化为无穷小,用来记录前驱节点的值
  2. 递归的出口:当root == null时,返回true
  3. 先检查左子树是否是二叉搜索树,再判断当前节点是否满足二叉搜索树的性质(即前驱节点的值小于当前节点的值),再继续修改prev的值为当前节点的值,再去判断右子树是否是二叉搜索树
  4. 只有当左子树为二叉搜索树,右子树为二叉搜索树,当前的节点也满足二叉搜索树的性质时,这棵树才是二叉搜索树

实现代码:

class Solution {public long prev = Long.MIN_VALUE;public boolean isValidBST(TreeNode root) {if(root == null) return true;boolean leftTree = isValidBST(root.left);//剪枝if(!leftTree) return false;boolean cur = true;if(root.val <= prev) cur = false;//剪枝if(!cur) return false;prev = root.val;boolean rightTree = isValidBST(root.right);return leftTree && rightTree && cur;}
}

当左子树不是二叉搜索树,就表明已经这棵树不是二叉搜索树了,也就没有必要进行后续的操作了,直接向上返回false即可;同理,当当前节点不满足二叉搜索树的性质时,也直接向上返回false。剪枝操作是为了加快搜索过程

5. 二叉搜索树中第 K 小的元素

题目链接:230. 二叉搜索树中第 K 小的元素

解题思路: 利用中序遍历,当访问到第k个“根节点”时,该节点对应的val值就是我们要找的第 K 小的元素

算法流程:

  1. 递归的出口:当根节点为空时,返回-1,表明没有找到
  2. 递归去左子树上去找,判断当前根节点是否是所需要找的节点(即判断k是否等于1),将k–,再递归去右子树上找
class Solution {public int K;public int kthSmallest(TreeNode root, int k) {K = k;return kthSmallest(root);}private int kthSmallest(TreeNode root) {if(root == null) return -1;int leftFind = kthSmallest(root.left, K);//剪枝if(leftFind != -1) return leftFind;if(K == 1) return root.val;K--;int rightFind = kthSmallest(root.right, K);return rightFind;}
}

注意: 这里需要定义一个全局变量K,而不是使用k作为递归的参数。因为当回溯时,k的值同样会回溯,这不符合算法的流程

6. 二叉树的所有路径

题目链接:257. 二叉树的所有路径

解题思路:

  1. 除了根节点,其他节点前面都需要添加一个“->”
  2. 需要一个字符串来添加路径,因此函数头需要一个字符串
  3. 采用前序遍历,将根节点放入字符串中,再去遍历左子树、右子树
  4. 当遍历到根节点时,将这时的字符串放入字符数组中
  5. 递归的出口是:root == null

按照思路,我们能写出如下的代码

class Solution {private List<String> lists;public List<String> binaryTreePaths(TreeNode root) {lists = new LinkedList<String>();StringBuffer strBuff = new StringBuffer();dfs(root, strBuff);return lists;}private void dfs(TreeNode root, StringBuffer path) {if(root == null) return;if(!path.isEmpty()) path.append("->");path.append(root.val);if(root.left == null && root.right == null) {lists.add(path.toString());return;}dfs(root.left, path);dfs(root.right, path);}
}

实际上,这个代码是存在问题的!因为我们在递归完成后,没有对当前添加到 path的节点值进行删除操作,这就会导致在处理完这个路径后,后续的路径会附加在这个路径的字符串后面,从而导致最终的结果不正确
在这里插入图片描述
这是为什么呢?当函数返回时,path的值不是会回退到上一个函数的值吗?

答:当一个函数执行完毕返回时,函数内部的局部变量(比如基本数据类型的变量等)所占用的栈空间会被自动回收,其状态确实会 “撤销”,也就是这些局部变量会随着函数栈帧的销毁而消失。

例如,如果函数中有一个简单的 int 类型局部变量 count,每次进入函数它有不同的值,函数返回后,这个 count 变量所占用的内存空间会被释放,下次再进入该函数时,它可以重新被初始化并使用,从这个角度看是有自动 “撤销” 的过程。

递归确实会返回到上一个状态,但现在我们现在创建的字符串是一个对象,由于可变对象的状态管理不同于基本数据类型(如整数或字符),我们需要手动管理这种状态,以确保路径的正确记录

因此,我们需要在原代码的基础上,对字符串进行回溯(“恢复现场”)。另外,当我们解题时,如果使用了全局变量,也要去考虑该变量的回溯

修改后的代码:

class Solution {private List<String> lists;public List<String> binaryTreePaths(TreeNode root) {lists = new LinkedList<String>();StringBuffer strBuff = new StringBuffer();dfs(root, strBuff);return lists;}private void dfs(TreeNode root, StringBuffer path) {if(root == null) return;if(path.length() > 0) path.append("->");path.append(root.val);if(root.left == null && root.right == null) {lists.add(path.toString());} else {dfs(root.left, path);dfs(root.right, path);}//回溯int lastIndex = path.lastIndexOf(String.valueOf(root.val)) - 2;if(lastIndex >= 0) {path.delete(lastIndex, path.length());}}
}

另外,我们还可以通过创建一个局部变量——path的副本。在每次递归调用时,使用传入路径的副本。这允许每次递归都维护独立的路径,避免修改其他层级的路径。通过将 path 的内容复制到新的 StringBuffer _path 中,确保上下文保持准确,不会影响其他节点的路径。

代码如下:

class Solution {private List<String> lists;public List<String> binaryTreePaths(TreeNode root) {lists = new LinkedList<String>();StringBuffer strBuff = new StringBuffer();dfs(root, strBuff);return lists;}private void dfs(TreeNode root, StringBuffer path) {if(root == null) return;StringBuffer _path = new StringBuffer(path);if(_path.length() > 0) _path.append("->");_path.append(root.val);if(root.left == null && root.right == null) {lists.add(_path.toString());return;}dfs(root.left, _path);dfs(root.right, _path);}
}

http://www.ppmy.cn/server/150075.html

相关文章

【软件工程】一篇入门UML建模图(用例图、对象图、顺序图与协作图)

​ &#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;软件开发必练内功_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1…

使用html 和JavaScript 实现一个点餐系统

1.完整的点餐系统页面 2. 主要功能和改进&#xff1a; 菜单管理: 上架和下架菜品的功能正常工作。新增菜品和修改菜品信息的功能正常工作。 购物车模块: 在总价后面增加了“会员价”一栏&#xff0c;展示每个菜品在会员折扣下的总价。结算时根据是否是会员来计算相应的总金额&…

2023年12月GESPC++三级真题解析

一、单选题&#xff08;每题2分&#xff0c;共30分&#xff09; 题目123456789101112131415答案 C D C C C A A D C C A B A C B 1.下面C数组的定义中&#xff0c;会丢失数据的是( )。 A.char dict_key[] {p,t,o}; B.int dict_value[] {33,22,11}; C.ch…

通过模拟对CLIP进行解释:如何通过梯度提升正样本的相似度?

通过模拟对CLIP进行解释&#xff1a;如何通过梯度提升正样本的相似度&#xff1f; 具体CLIP可以参考笔者的另外的博客&#xff1a; CLIP 的核心训练代码与对比损失的解释&#xff1a;中英双语 和 对比损失&#xff08;Contrastive Loss&#xff09;与大模型&#xff1a;Contra…

如何解决 java.lang.IndexOutOfBoundsException 异常问题?亲测有效的解决方法!

IndexOutOfBoundsException 是 Java 中常见的运行时异常&#xff0c;表示访问了无效的索引&#xff08;数组、集合、字符串等&#xff09;。本文将从原因分析到解决方法&#xff0c;并提供真实案例和代码示例&#xff0c;帮你彻底解决这个问题。 1. 问题分析 抛出 IndexOutOfB…

重生之我在异世界学编程之C语言:深入函数递归篇

大家好&#xff0c;这里是小编的博客频道 小编的博客&#xff1a;就爱学编程 很高兴在CSDN这个大家庭与大家相识&#xff0c;希望能在这里与大家共同进步&#xff0c;共同收获更好的自己&#xff01;&#xff01;&#xff01; 函数递归与迭代 引言正文一、递归的基本概念二、递…

vue3+setup使用rtsp视频流实现实时监控,全屏,拍摄,自动拍摄等功能(纯前端)

vue3setup使用rtsp视频流实现实时监控&#xff0c;全屏&#xff0c;拍摄&#xff0c;自动拍摄等功能(纯前端) 概要 本文介绍了如何在Vue应用中通过WebRTC技术获取摄像头的rtsp视频流&#xff0c;同时展示了实时监控&#xff0c;全屏&#xff0c;拍摄&#xff0c;自动拍摄等功…

利用GeoWave导入矢量数据到HBase/Accumulo数据库

前言 最近在做有关地理时空大数据的实验&#xff0c;本文将介绍如何利用geowave框架&#xff0c;将矢量数据导入到HBase或Accumulo等NoSQL数据库中。 软件版本&#xff1a; Hadoop: 2.10.2 Zookeeper: 3.6.4 geowave: 1.2.0 Accumulo&#xff1a;1.9.3 HBase: 1.4.0 Ja…