算法-二叉树常见问题详解

news2024/12/28 5:38:12

文章目录

    • 1. 二叉树的三种遍历方式的实质
    • 2. 二叉树的序列化与反序列化
    • 3. 根据前序中序反序列创建二叉树
    • 4. 二叉树的路径问题
    • 5. LCA公共祖先问题
    • 6. 二叉搜索树的LCA问题
    • 7. 验证搜索二叉树
    • 8. 修建搜索二叉树
    • 9. 二叉树打家劫舍问题

1. 二叉树的三种遍历方式的实质

这个相信大家都不会陌生, 但是大家学习这个知识点的时候, 往往并不会在意这三种遍历的顺序到底有什么意义(一般都只会打印一下值即可), 而且对这个是怎么来的也不是很清晰, 下面我们通过代码的注释仔细解释一下

	public static void dfs(TreeNode node){
        //递归的终止条件(其实也就是深度优先搜索)
        //即当递归(其实也就是搜索到空节点的时候, 就直接结束(返回),但是该函数无返回信息)
        if(node == null){
            return;
        }
        
        //下面是第一次来到该节点的时机(对应直接对节点进行操作,前序)
        System.out.println(node.val);
        
        //往左子树搜索
        dfs(node.left);
        
        //下面是第二次来到该节点的时机(左子树搜索完毕之后进行操作,中序)
        System.out.println(node.val);
        
        //往右子树搜索
        dfs(node.right);
        
        //下面是第三次来到该节点的时机(左右子树都搜索完毕进行操作,后序)
        System.out.println(node.val);
    }

我们现在创建一颗树, 树的结构是[1,2,3,4,null], 看一下上面的代码的结果

	public static void main(String[] args) {
        TreeNode node1 = new TreeNode(1);
        TreeNode node2 = new TreeNode(2);
        TreeNode node3 = new TreeNode(3);
        TreeNode node4 = new TreeNode(4);

        node1.left = node2;
        node1.right = node3;
        node2.left = node4;

        dfs(node1);
    }

运行结果是 1 2 4 4 4 2 2 1 3 3 3 1

我们仔细研究一下上面的打印结果
把每一个数字
第一次出现的结果提取出来也就是
1 2 4 3(前序)
第二次出现的结果提取出来也就是
4 2 1 3(中序)
第三次出现的结果提取出来也就是
4 2 3 1(后序)
上述的结果其实正式对应二叉树中的递归序
也就是几大遍历顺序的实质
下面我们做题的时候要时刻分析我们的遍历顺序!

2. 二叉树的序列化与反序列化

对于一个树状存储的数据来说我们需要将其序列化转换为文本文件(字符串)便于存储与传输,然后在通过反序列化的方式将其还原出来, 值得一提的是, 反序列化只能操作前序或者后序或者层序的字符串,而不能操作中序的字符串, 原因是中序的字符串是不唯一的,比如下面这一行代码

	public static void main(String[] args) {
        
        //第一棵树
        TreeNode node1 = new TreeNode(1);
        TreeNode node2 = new TreeNode(1);
        node1.left = node2;
        
        //第二棵树
        node1.right = node2;
    }

第一颗树的序列化结果是 " # 1 # 1 # "
第二棵树的序列化结果也是 " # 1 # 1 # "
但是二者不是同一棵树, 所以中序的序列化是有问题的
首先展示的我们序列化的代码(前序举例子)

	//创建一个拼接字符的
    static StringBuilder sp = new StringBuilder();
    //序列化的过程其实就是前序遍历输出结果的时候进行拼接即可
    public static String creatStringUsePreOrder(TreeNode node){
        //递归终止条件
        if(node == null){
            sp.append("#,");
            return sp.toString();
        }
        
        sp.append(node.val + ",");
        //递归的返回值其实没有被接收(
        //实质上只有最高层级的结果被调用者接收了)
        creatStringUsePreOrder(node.left);
        creatStringUsePreOrder(node.right);
        return sp.toString();
    }

序列化的过程是比较简单的,其实就是前序遍历加上字符串的拼接, 那如何将字符串还原为一颗二叉树呢, 下面是我们的实现(已经用split方法去除连接的","而转化为一个String[]数组)

//定义一个下标遍历字符串(思考为什么定义到外侧)
    private static int index = 0;
    public static TreeNode creatTreeUsePreString(String[] s){
        //递归终止条件
        if(s[index++].equals("#")){
            return null;
        }
        
        //递归创建二叉树
        //(前序遍历的方案, 树的连接其实是从底部连接的, 逐级返回)
        TreeNode root = new TreeNode(Integer.parseInt(s[index++]));
        root.left = creatTreeUsePreString(s);
        root.right = creatTreeUsePreString(s);
        return root;
    }

上述代码我们从递归的角度解释, 该函数的作用就是(创建一个二叉树), 那创建一颗二叉树需要创建出左子树, 也需要创建出一颗右子树, 所以出现了子问题的反复调用, 我们只需要把这个函数想象为一个"黑匣子"…

3. 根据前序中序反序列创建二叉树

其实该问题就是给我们一个两个数组(无重复数据), 一个是前序遍历的结果, 一个是中序遍历的结果, 然后让我们创建出一颗完整的二叉树, 我们这个问题给定的两个数组, 可以是前序加中序, 也可以是中序加后序, 但是不可以是前序加后序(创建不出唯一的二叉树), 例子自己想…, 比如下面这个例子

TreeNode node1 = new TreeNode(1);
TreeNode node2 = new TreeNode(2);

//第一颗树
node1.left = node2;
//第二颗树
node1.right = node2

下面我们分析一下如何创建一个二叉树(前序加上中序)
假设有一个函数 func, 这个函数的功能就是创建二叉树, 那么该函数的参数应该就是(传入数据的所有信息)
> func(int[] pre,int l1,int r1,int[] in,int l2,int r2) 我们假设数组的长度都是5, 那么我们创建二叉树的过程一定是从前序的结果开始的, 首先我们new出来根节点, 然后找到根节点在中序数组中所处的位置, 也就是在这个左侧就是我们的左树的部分(我们可以知道节点的规模), 右侧就是右树, 也就是说我们可以知道每一个元素在中序的位置然后控制左右边界的位置, 最终完成二叉树的创建, 由于这个过程相对抽象, 我们下面举一个例子方便大家理解
在这里插入图片描述
下面是我们的代码实现(用HashMap加速查询的过程)

/**
     * 从前序跟中序构建一颗完整的二叉树
     * 用的是一个封装的构建函数来构建 f(pre,l1,r1,in,l1,r1)
     */

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        if (preorder == null || inorder == null || preorder.length != inorder.length) return null;
        //构建出来一张表加快构建二叉树的过程
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < inorder.length; ++i) {
            map.put(inorder[i], i);
        }
        return creatTree(preorder, 0, preorder.length - 1, inorder, 0, inorder.length, map);
    }

    private TreeNode creatTree(int[] pre, int l1, int r1, int[] in, int l2, int r2, HashMap<Integer, Integer> map) {
        //递归的终止条件
        if (l1 > r1) {
            return null;
        }
        if (r1 == l1) {
            return new TreeNode(pre[r1]);
        }

        //借助HashMap加快查找的过程
        int k = map.get(pre[l1]);
        TreeNode node = new TreeNode(pre[l1]);
        node.left = creatTree(pre, l1 + 1, l1 + k - l2, in, l2, k, map);
        node.right = creatTree(pre, l1 + k - l2 + 1, r1, in, k + 1, r2, map);
        return node;
    }

从递归的角度来说, 创建二叉树的这个函数相当于就是一个"黑盒", 我们要相信它可以根据我们给定的参数创建出一颗完整的树, 然后我们需要去创建我们的左子树, 创建右子树(就出现了子问题复现的情况), 所以出现了递归

后序加上中序其实是同理的

4. 二叉树的路径问题

该问题涉及到回溯这一算法的概念, 其实递归的过程天然就带着回溯, 那我们为什么要用回溯呢, 是因为在该类问题当中, 我们用一个stack收集节点的时候, 如果该节点的左右深度优先搜索完毕之后, 我们需要把该节点弹出, 进行其他深度路径的搜索, 其实回溯的实际也正式我们该节点的搜索任务结束的时机, 从内存的角度来看, 也是该函数栈帧弹栈的时机

/**
     * 二叉树的所有路径
     * 1. 返回值是void类型 2. 遍历的顺序我们采用的是前序遍历的顺序
     * 3. 中途会进行节点的弹出其实也就是回溯的思路(回溯和递归是不分家的)
     */
    private List<String> binaryTreePathsRes = new ArrayList<>();
    private ArrayDeque<Integer> stack = new ArrayDeque<>();

    public List<String> binaryTreePaths(TreeNode root) {
        binaryTreePathsFunc(root);
        return binaryTreePathsRes;
    }

    private void binaryTreePathsFunc(TreeNode node) {
        //递归的终止条件
        if (node == null) return;
        stack.add(node.val);
        //证明递归到了叶子节点该收获了, 不能在null节点处收获
        if (node.left == null && node.right == null) {
            StringBuilder sp = new StringBuilder();
            Iterator<Integer> it = stack.iterator();
            while (it.hasNext()) sp.append(it.next() + "->");
            sp.delete(sp.lastIndexOf("-"), sp.lastIndexOf(">") + 1);
            binaryTreePathsRes.add(sp.toString());
            stack.removeLast();
        }

        //下面就是深度优先搜索的过程
        binaryTreePathsFunc(node.left);
        binaryTreePathsFunc(node.right);
        stack.removeLast();
    }

还有一个搜索和为targerSum的路径然后返回, 其实是一样的思路, 定义一个全局变量sum, 每次遍历到的时候进行 sum += root.val , 搜索完毕之后进行回溯操作, sum -= root.val, 思路其实是一样的

5. LCA公共祖先问题

其实就是求两个节点p,q在一颗二叉树的最近的公共祖先, 也是著名的LCA问题(该问题本身是很复杂的,我们先弄一个入门题), 我们知道, p,q对于一颗树来说, 他们可能在一支上(此时q或者是p就是最近的公共祖先), 又或者是分属于两颗不同的树, 此时树的根节点就是最近的公共祖先, 我们现在创建, 其实也就是dfs对左右子树进行深度优先搜索的思路)
在这里插入图片描述

其实代码是很简单的

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null || root == q || root == p){
            return root;
        }
		
		//往左边搜索
        TreeNode ln = lowestCommonAncestor(root.left,p,q);
        //往右边搜索
        TreeNode rn = lowestCommonAncestor(root.right,q,p);

        if(ln == null && rn == null) return null;
        if(ln != null && rn != null) return root;
        return ln == null ? rn : ln;
    }
}

其实就是一个简单的搜索的逻辑
总结一下本节, 我们递归要如何想到, 我们要把这个方法当成一个黑盒子, 然后确定返回值, 终止条件等等, 另外递归的过程其实就是深度优先搜索, 搜索和递归是不分家的, 还有搜索跟回溯是不分家的

6. 二叉搜索树的LCA问题

常规树的LCA解决之后, 二叉搜索树的LCA问题其实也被包括在里面了, 但是是否有一个更好的思路来解决二叉搜索树的最近公共祖先呢?
我们都知道, 对于二叉搜索树来说, 左侧的所侧节点都小于当前节点, 右侧都大于
所以我们可以把问题抽象为下面这个问题, 记作当前的节点为cur
cur == p || cur == q : cur就是我们要找到的祖先节点
cur > Math.max(p,q) : cur = cur.left (向左侧移动)
cur < Math.min(p,q) : cur = cur.right (向右侧移动)
Math.min(p,q) < cur < Math.max(p,q) 此时 cur就是最近公共祖先
翻译为实现代码如下

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        return p.val > q.val ? lca(root,q,p) : lca(root,p,q);
    }
    
    //该方法默认的p为较小的值, q为较大的值
    public TreeNode lca(TreeNode root,TreeNode p,TreeNode q){
        while(p != root && q != root){
            if(root.val > p.val && root.val < q.val){
                return root;
            }
            root = root.val > q.val ? root.left : root.right;
        }
        return root;
    }
}

7. 验证搜索二叉树

这个问题如何想到递归呢, 首先二叉搜索树的左子树也是一个二叉搜索树, 右子树也是一个二叉搜索树, 递归的终点其实就是叶子节点, 对于任何一个树, 左子树的最大值, 一定要小于中间节点的值, 右侧子树的最小值一定要大于中间节点的值, 而对于任意一个树, 如何找到最大最小值呢, 其实就是左子树一直向右侧扎, 右子树一直向左侧扎, 代码实现如下

class Solution {
    public boolean isValidBST(TreeNode root) {
        //递归的终止条件
        if(root == null) return true;
        if(root.left == null && root.right == null) return true;
        
        //寻找左侧最大, 右侧最小(设置一个前驱节点)
        TreeNode curl = root.left;
        TreeNode prel = null;
        TreeNode curr = root.right;
        TreeNode prer = null;
        while(curl != null){
            prel = curl;
            curl = curl.right;
        }
        while(curr != null){
            prer = curr;
            curr = curr.left;
        }
        if(prel == null){
            if(root.val >= prer.val) return false;
        }
        if(prer == null){
            if(root.val <= prel.val) return false;
        }
        if(prel != null && prer != null){
            if(root.val <= prel.val || root.val >= prer.val) return false;
        }
        return isValidBST(root.left) && isValidBST(root.right);
    }
}

遍历方式其实就是后续遍历, 收集到左右子树的信息然后返回

8. 修建搜索二叉树

在这里插入图片描述

这个题的意思就是上面描述的这样, 分析的思路如下

 /**
     * 修剪二叉搜索树
     * 问题分析 : 如果当前节点的值小于左边界, 那么我们当前节点及其左边都不会被保留
     *           如果当前节点的值大于右边界, 那么我们当前节点及其右边都不会被保留
     *           如果当前节点的值介于范围内部, 那么就保留当前节点递归修建左子树跟右子树
     *
     * 代码逻辑 : 我们的 "黑盒函数" 的功能是修建一颗二叉树, 并返回修剪之后的头节点
     * 实在不行自己画递归图去理解
     */
	public TreeNode trimBST(TreeNode root, int low, int high) {
        //递归的终止条件
        if(root == null){
            return null;
        }

        if(root.val < low){
            return trimBST(root.right,low,high);
        }
        if(root.val > high){
            return trimBST(root.left,low,high);
        }

        root.left = trimBST(root.left,low,high);
        root.right = trimBST(root.right,low,high);
        return root;
    }

9. 二叉树打家劫舍问题

这道题其实已经是树形dp了, 但是我们今天的解法大家都能听懂, 我们用递归去做这道题, 我们首先定义一个全局变量yes, 和一个全局变量no, 前者代表的含义是对一个头节点来说, 我偷了头节点的最大收益, 后者来说是我
不偷头节点的最大收益, 设置我们的当前节点的状态就是n(不偷当前节点), y(偷当前节点), 所以我们的状态转移方程就是
y += this.no;
n += Math.max(this.yes,this.no);
我们下面的func函数执行完毕之后就会更新全局的yes和no变量

class Solution {
    //yes的意思是偷头节点的情况下, 我们能获得的最大收益
    public int yes = 0;
    //no的意思是不偷头节点的情况下, 我们能获得的最大收益
    public int no = 0;
    
    public int rob(TreeNode root) {
        func(root);
        return Math.max(this.yes,this.no);
    }
    
    //该函数的功能就是给定一个头节点, 可以得到我们此时节点的两种最优解的情况
    private void func(TreeNode root){
        //递归终止条件
        if(root == null){
            this.yes = 0;
            this.no = 0;
        }else{
            int y = root.val;
            int n = 0;
            func(root.left);
            y += this.no;
            n += Math.max(this.no,this.yes);
            func(root.right);
            y += this.no;
            n += Math.max(this.no,this.yes);
            this.yes = y;
            this.no = n;
        }
    }
}

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

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

相关文章

Python从0到100(四十):Web开发简介-从前端到后端(文末免费送书)

前言&#xff1a; 零基础学Python&#xff1a;Python从0到100最新最全教程。 想做这件事情很久了&#xff0c;这次我更新了自己所写过的所有博客&#xff0c;汇集成了Python从0到100&#xff0c;共一百节课&#xff0c;帮助大家一个月时间里从零基础到学习Python基础语法、Pyth…

王权与自由steam服测试资格申请 王权与自由国际服测试资格申请

不少人都开始期待《王权与自由国际服》的内测&#xff0c;消息已经传出&#xff0c;这款游戏的内测马上会在 太平洋时间4月10日下午1点&#xff0c;也就是世界标准时间的晚上8点&#xff0c;开始封闭式Beta测试&#xff0c;测试时间有为一周&#xff0c;持续到4月17日的太平洋…

万字长文之分库分表里如何设计一个主键生成算法?【后端面试题 | 中间件 | 数据库 | MySQL | 分库分表】

分库分表是在面试里一个非常热门而且偏难的话题&#xff0c;下面了解UUID、自增主键和雪花算法的特点&#xff0c;并且在面试的时候刷出亮点。 前置 所谓的分库分表严格来说是分数据源、分库和分表。例如每个公司订单表的分库分表策略就是用了8个主从集群&#xff0c;每个主从…

Layout View

GoTo 数据网格和视图入门 本文档仅概述特定于LayoutView的功能。有关常用卡和选择功能的信息&#xff0c;请参阅根卡和布局视图一文。 Template Card 布局视图使用布局控件来排列卡片内的内容。对于每个网格列&#xff0c;都会生成一个LayoutViewField对象。切换到数据网格设…

django超市管理系统-计算机毕业设计源码53507

摘 要 随着社会经济的不断发展&#xff0c;超市作为零售行业的一部分&#xff0c;扮演着重要的角色。在信息技术的快速发展的背景下&#xff0c;计算机软件和硬件技术的普及应用在商业管理中起到了至关重要的作用&#xff0c;因此基于Django的超市管理系统应运而生&#xff0c;…

昇思25天学习打卡营第15天|基于MobileNetv2的垃圾分类

一、关于MobileNetv2 MobileNet网络专注于移动端、嵌入式或IoT设备的轻量级CNN网络。MobileNet网络使用深度可分离卷积&#xff08;Depthwise Separable Convolution&#xff09;的思想在准确率小幅度降低的前提下&#xff0c;大大减小了模型参数与运算量。并引入宽度系数 α和…

MySQL集群、Redis集群、RabbitMQ集群

一、MySQL集群 1、集群原理 MySQL-MMM 是 Master-Master Replication Manager for MySQL&#xff08;mysql 主主复制管理器&#xff09;的简称。脚本&#xff09;。MMM 基于 MySQL Replication 做的扩展架构&#xff0c;主要用来监控 mysql 主主复制并做失败转移。其原理是将真…

解决vscode项目中无法识别宏定义的问题

在c_cpp_properties.json中的"defines":[]中定义的宏无法被识别。 从而导致代码中的宏开关无法生效&#xff0c;造成代码的阅读不便利。 排查路线是&#xff1a; 关闭所有插件&#xff0c;删除当前工程目录下的.vscode文件夹。 经过一系列排查发现是C/C插件与clangd插…

能把进程和线程讲的这么透彻的,没有20年功夫还真不行【0基础也能看懂】

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

数据库基本查询(表的增删查改)

一、增加 1、添加信息 insert 语法 insert into table_name (列名) values (列数据1&#xff0c;列数据2&#xff0c;列数据3...) 若插入时主键或唯一键冲突就无法插入。 但如果我们就是要修改一列信息也可以用insert insert into table_name (列名) values (列数据1&am…

nginx的正向与反向代理

正向代理与反向代理的区别 虽然正向代理和反向代理都涉及代理服务器接收客户端请求并向服务端转发请求&#xff0c;但它们之间存在一些关键的区别&#xff1a; 正向代理&#xff1a; 在正向代理中&#xff0c;代理服务器代表客户端向服务器发送请求&#xff0c;并将服务…

【Linux】安装PHP扩展-igbinary

说明 本文档是在centos7.6的环境下&#xff0c;安装PHP7.4之后&#xff0c;安装对应的PHP扩展igbinary。 一、igbinary简述 igbinary 是一个 PHP 扩展&#xff0c;主要用于序列化和反序列化数据&#xff0c;其设计目的是为了提高序列化过程中的性能和内存效率。 优点&#…

wifi信号处理的CRC8、CRC32

&#x1f9d1;&#x1f3fb;个人简介&#xff1a;具有3年工作经验&#xff0c;擅长通信算法的MATLAB仿真和FPGA实现。代码事宜&#xff0c;私信博主&#xff0c;程序定制、设计指导。 &#x1f680;wifi信号处理的CRC8、CRC32 目录 &#x1f680;1.CRC概述 &#x1f680;1.C…

LeNet入门和Pytorch实现

1. LeNet简介 LeNet是一系列网络的合称&#xff0c;包括LeNet1-LeNet5&#xff0c;是卷积神经网络的开山之作。 文献&#xff1a;LeCun Y, Boser B, Denker J, et al. Handwritten digit recognition with a back-propagation network[J]. Advances in neural information pro…

鸿蒙开发:Universal Keystore Kit(密钥管理服务)【查询密钥是否存在(C/C++)】

查询密钥是否存在(C/C) HUKS提供了接口供应用查询指定密钥是否存在。 在CMake脚本中链接相关动态库 target_link_libraries(entry PUBLIC libhuks_ndk.z.so)开发步骤 构造对应参数。 指定密钥别名keyAlias&#xff0c;密钥别名最大长度为64字节。查询密钥需要的属性TAG&#…

DZS-12CE/S延时中间继电器 导轨安装 约瑟JOSEF

中间继电器型号&#xff1a; DZS-254 DZS-145 DZS-233 DZS-121 DZS-112 DZS-121 DZS-12BG DZS-12B DZS-213 DZS-234 DZS-11B/Q DZS-226 DZS-652 DZS-17E/302 DZS-12CE/S DZS-821 DZS-226 DZS-249 DZS-254G DZS-12E DZS-895 DZS-234 DZS-655G DZS-651 DZS-115 DZS-…

使用自制Qt工具配合mitmproxy进行网络调试

在软件开发和网络调试过程中&#xff0c;抓包工具是不可或缺的。传统的抓包工具如Fiddler或Charles Proxy通常需要设置系统代理&#xff0c;这会抓到其他应用程序的网络连接&#xff0c;需要设置繁琐的过滤&#xff0c;导致不必要的干扰。为了解决这个问题&#xff0c;我们可以…

一个引发openssl崩溃问题案例

1 背景 最近用libevent写了一个https代理功能&#xff0c;在调研的时候&#xff0c;遇到了一个项目用到了本地多个openssl库引发的ssl握手崩溃问题。 2 开发环境 项目库版本号依赖项libeventlibevent-2.1.8-stableopenssl 1.1openssl1.0u / 1.1.1w / 3.3.1...... 3 问题现象…

FlinkErr:org/apache/hadoop/hive/ql/parse/SemanticException

在flink项目中跑 上面这段代码出现如下这个异常&#xff0c; java.lang.NoClassDefFoundError: org/apache/thrift/TException 加上下面这个依赖后不报错 <dependency> <groupId>org.apache.thrift</groupId> <artifactId>libthrift</artifactId…

springmvc1

以前的servlet程序&#xff1a; springmvc 不同的处理器&#xff1a;不同的方法或者处理类 所有的请求都会经过dispathcherservlet的doservice方法&#xff1a; mvc原理&#xff1a; 前端控制器&#xff1a;jsp或者什么东西