通过二叉树例题深入理解递归问题

news2024/11/18 21:39:53

目录

引入:

例1:二叉树的前序遍历:

例2: N叉树的前序遍历:

 例3:二叉树的最大深度:

例4:二叉树的最小深度 

例5:N叉树的最大深度:

例6:左叶子之和:

例7:翻转二叉树:

例8: 路径总和:

例9:路径总和II:

例10:二叉树展开为链表:


引入:

递归是一种在函数定义中使用函数自身的方法。它是一种常见的编程技巧,用于解决可以被分解为相同问题的子问题的问题。递归函数通常包含两个部分:基本情况和递归情况。

基本情况是指递归函数停止调用自身的条件。当满足基本情况时,递归函数将不再调用自身,而是返回一个特定的值或执行特定的操作。

递归情况是指递归函数调用自身解决更小规模的子问题。通过不断地调用自身,递归函数可以将原始问题分解为更小的子问题,直到达到基本情况。

让我们再看二叉树的遍历,其实它的结构大概是这样(基本框架):

public void traverse(TreeNode root) {
        //前序遍历的位置
        traverse(root.left);
        //中序遍历的位置
        traverse(root.right);
        //后续遍历的位置
    }

你或许会认为前中后续遍历不过就是要操作的代码位于这三个不同的位置而已,其实不然。我们抛开操作代码不谈,就只看这段代码,你就会发现他的主要遍历过程是这样:

只不过我们在其遍历的过程中对某个节点进行了不同的操作罢了。如前序遍历,我们按照这个过程进入二叉树的遍历(绿->蓝->红),由于我们是在遍历前加入不同的操作,也就是按照:

①->②->③->④->⑤->⑥->⑦分别进行各项操作,前中后序是遍历二叉树过程中处理每一个节点的三个特殊时间点。 二叉树的问题,大致就是让你在这三个不同的时间节点注入巧妙的代码逻辑,来达成各项操作。关键要注意我们要在每个节点做什么,实施怎样的操作来完成我们的目标。通过递归将一个大的二叉树问题转化为一个个子树问题,直到不可再分就结束。

前序位置的代码在刚刚进入一个二叉树节点的时候执行;

后序位置的代码在将要离开一个二叉树节点的时候执行;

中序位置的代码在一个二叉树节点左子树都遍历完,即将开始遍历右子树的时候执行。

例1:二叉树的前序遍历:

题目:给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

输入:root = [1,null,2,3]
输出:[1,2,3]

题目链接:144. 二叉树的前序遍历 - 力扣(LeetCode)

题解:通过联系二叉树的遍历过程,前序遍历就是让我们在框架前将该节点加入到list中

class Solution {
    List<Integer> list = new LinkedList<>();//用于存储遍历的信息
    public List<Integer> preorderTraversal(TreeNode root) {
        //递归的结束条件
        if(root == null){
            return list;
        }
        list.add(root.val);//根据框架,我们将该节点加入到list中
        preorderTraversal(root.left);
        preorderTraversal(root.right);
        return list;
    }
}

二叉树的中后序遍历也是一样,将list分别放到框架中间和后面 

例2: N叉树的前序遍历:

注:由于N叉树(不止包含左右子节点,可能有多个)的特性,其没有中序遍历,其大致框架是:

  public void traverse(TreeNode root){
        if(root == null){
            return ;
        }

        //前序遍历的位置
        for(Node child : root.children){
            traverse(child);
        }
           //后序遍历的位置
        }

题目:

给定一个 n 叉树的根节点  root ,返回 其节点值的 前序遍历 。

n 叉树 在输入中按层序遍历进行序列化表示,每组子节点由空值 null 分隔(请参见示例)。

 

题目链接:589. N 叉树的前序遍历 - 力扣(LeetCode)

题解:

class Solution {
    List<Integer> res = new LinkedList<Integer>();//用于存储二叉树的节点信息
    public List<Integer> preorder(Node root) {
        traverse(root);
        return res;
    }
    void traverse(Node root){
        if(root == null){
            return ;
        }

         res.add(root.val);
        for(Node child : root.children){
            traverse(child);
        }
    }
}

 例3:二叉树的最大深度:

题目:

给定一个二叉树 root ,返回其最大深度。 

二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。

输入:root = [3,9,20,null,null,15,7]
输出:3

题目链接:104. 二叉树的最大深度 - 力扣(LeetCode)

法一:通过二叉树的遍历将其转化为左右子树的最大深度+1(自身)的方式进行求解:

class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null){
            return 0;
        }

        int leftMax = maxDepth(root.left);//得到左树的最大深度
        int rightMax = maxDepth(root.right);//得到右树的最大深度

        return 1 + Math.max(leftMax,rightMax);//返回左右子树中的最大深度+1
    }
}

我们不进入递归理解,而是从该函数的定义出发,maxDepth这个方法就是求树的最大深度,那是不是按照上面的思路我们照搬,先求左子树的最大深度,在求左子树的最大深度,然后从中去最大+1就行了。 或者按照上述的二叉树遍历过程理解:将这颗树不断分解(后序遍历),直到其分解成叶子节点,当它为空时,返回左右子树中的最大+1,在从最小的子树出发,不断往上进行比较,求最值+1---》最终得到我们的结果

法二:迭代思想,通过前序遍历记录最大深度,后续遍历来减少最大深度,同时不断记录最大深度的值:

class Solution {
    int ret = 0;//不断刷新最大深度的值
    int depth = 0;//记录深度
    public void traverse(TreeNode root){
        if(root == null){
            return ;
        }
        depth++;//在前序遍历时记录深度不断增加
        ret = Math.max(depth,ret);//同时刷新最大值
        traverse(root.left);
        traverse(root.right);
        depth--;//后续遍历时记录深度减少
    }
    public int maxDepth(TreeNode root) {
        traverse(root);
        return ret;
    }
}

例4:二叉树的最小深度 

题目:

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。说明:叶子节点是指没有子节点的节点。

题目链接:111. 二叉树的最小深度 - 力扣(LeetCode)

题解:

法一:分解问题,不断求左右子树最小值:

// 「分解问题」的递归思路
class Solution2 {
    public int minDepth(TreeNode root) {
        // 基本情况:如果节点为空,返回深度为0
        if (root == null) {
            return 0;
        }

        // 递归计算左子树的最小深度
        int leftDepth = minDepth(root.left);
        // 递归计算右子树的最小深度
        int rightDepth = minDepth(root.right);

        // 特殊情况处理:如果左子树为空,返回右子树的深度加1
        if (leftDepth == 0) {
            return rightDepth + 1;
        }
        // 特殊情况处理:如果右子树为空,返回左子树的深度加1
        if (rightDepth == 0) {
            return leftDepth + 1;
        }

        // 计算并返回最小深度:左右子树深度的最小值加1
        return Math.min(leftDepth, rightDepth) + 1;
    }
}

法二:迭代思想:与最小深度类似,但这里需要多一个判断是否是叶子节点,是叶子节点是我们才刷新最小值,不然开始就是最小值后续不会在改变,小细节,开始就定义一个极大值,为后续的找最小值做准备

class Solution {
     int depth = 0;
     int res = Integer.MAX_VALUE;
    public  void traverse(TreeNode root){
        if(root == null){
            return ;
        }
        depth++;
        if(root.left == null && root.right == null){
            res = Math.min(res,depth);
        }
        traverse(root.left);
        traverse(root.right);
        depth--;
    }
    public int minDepth(TreeNode root) {
        if(root == null){
            return 0;
        }
        traverse(root);
        return res;
    }
}

例5:N叉树的最大深度:

题目:

给定一个 N 叉树,找到其最大深度。

最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。

N 叉树输入按层序遍历序列化表示,每组子节点由空值分隔(请参见示例)。

题目链接:559. N 叉树的最大深度 - 力扣(LeetCode)

题解:与二叉树的最大深度基本一致,就是要遍历所有子树:

class Solution {
    int res = 0;//记录最大值
    int depth = 0;//记录深度
    public int maxDepth(Node root) {
        if(root == null){
            return 0;
        }
       traverse(root);
        return res;
    }

    public void traverse(Node root){
        depth++;
        res = Math.max(res,depth);
        for(Node child : root.children){
            traverse(child);
        }
        depth--;
    }
}

例6:左叶子之和:

题目:给定二叉树的根节点 root ,返回所有左叶子之和。

题目链接:404. 左叶子之和 - 力扣(LeetCode)

题解:通过给函数传一个boolean值通过二叉树的遍历来判断是否是左叶子节点。然后将是左叶子节点的值相加

class Solution {
    int res = 0;
    public void dfs(TreeNode root,boolean isLeft){

        if(root == null){
            return ;
        }
        //判断是否是左叶子节点,是就加上
        if(root.left == null && root.right == null && isLeft){
            res += root.val;
        }
        dfs(root.left,true);
        dfs(root.right,false);
    }
    public int sumOfLeftLeaves(TreeNode root) {
        dfs(root,false);
        return res;
    }
}

例7:翻转二叉树:

题目:

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

题目链接:226. 翻转二叉树 - 力扣(LeetCode)

题解:在前序遍历的基础上交换左右子树即可(图解):

class Solution {
    public TreeNode invertTree(TreeNode root) {
        if(root == null){
            return null;
        }
      //交换左右子树
        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;
        invertTree(root.left);
        invertTree(root.right);
        return root;
    }
}

例8: 路径总和:

题目:给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。

题目链接:112. 路径总和 - 力扣(LeetCode)

题解:法一:在前序遍历位置记录路径的和(节点的val值累加),后序遍历的位置删除路径的和(节点的val值减少):

class Solution {
    boolean found = false;
    int curSum = 0;
    int target;
    public void traverse(TreeNode root){
        if(root == null){
            return ;
        }
     //前序遍历不断加入节点的和
        curSum += root.val;
     //判断是否是叶子节点,是就和target值比较
        if(root.left == null && root.right == null){
            if(curSum == target){
                found = true;
            }
        }
        traverse(root.left);
        traverse(root.right);
     //后序遍历删除节点的值
        curSum -= root.val;
    }
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if(root == null){
            return false;
        }
        this.target = targetSum;
        traverse(root);
        return found;
    }
}

法二:将路径和不断分解为子树的路径和:

lass Solution {
    /* 解法一、分解问题的思路 */
    // 定义:输入一个根节点,返回该根节点到叶子节点是否存在一条和为 targetSum 的路径
    public boolean hasPathSum(TreeNode root, int targetSum) {
        // base case
        if (root == null) {
            return false;
        }
        // root.left == root.right 等同于 root.left == null && root.right == null
        if (root.left == root.right && root.val == targetSum) {
            return true;
        }

        return hasPathSum(root.left, targetSum - root.val)
                || hasPathSum(root.right, targetSum - root.val);
    }

例9:路径总和II:

题目:

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

题目链接:113. 路径总和 II - 力扣(LeetCode)

题解:遍历一遍二叉树,就可以把所有符合条件的路径找出来。为了维护经过的路径,在进入递归的时候要在 path 列表添加节点,结束递归的时候删除节点。

class Solution {
    List<List<Integer>> res = new LinkedList<>();

    public List<List<Integer>> pathSum(TreeNode root, int sum) {
        if (root == null) return res;
        traverse(root, sum, new LinkedList<>());
        return res;
    }

    // 遍历二叉树
    private void traverse(TreeNode root, int sum, LinkedList<Integer> path) {
        if (root == null) return;

        int remain = sum - root.val;

        if (root.left == null && root.right == null) {
            if (remain == 0) {
                // 找到一条路径
                path.addLast(root.val);
                res.add(new LinkedList<>(path));
                path.removeLast();
            }
            return;
        }

        // 维护路径列表
        path.addLast(root.val);
        traverse(root.left, remain, path);
        path.removeLast();

        path.addLast(root.val);
        traverse(root.right, remain, path);
        path.removeLast();
    }
}

例10:二叉树展开为链表:

题目:

给你二叉树的根结点 root ,请你将它展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
  • 展开后的单链表应该与二叉树 先序遍历 顺序相同。

题目链接:114. 二叉树展开为链表 - 力扣(LeetCode)

题解:只要运用二叉树的递归遍历框架即可;后者的关键在于明确递归函数的定义,然后利用这个定义,这题就属于后者,flatten 函数的定义如下:给 flatten 函数输入一个节点 root,那么以 root 为根的二叉树就会被拉平为一条链表。流程:

1、将 root 的左子树和右子树拉平。

2、将 root 的右子树接到左子树下方,然后将整个左子树作为右子树。

class Solution {
    // 定义:将以 root 为根的树拉平为链表
    public void flatten(TreeNode root) {
        // base case
        if (root == null) return;
        // 先递归拉平左右子树
        flatten(root.left);
        flatten(root.right);

        /****后序遍历位置****/
        // 1、左右子树已经被拉平成一条链表
        TreeNode left = root.left;
        TreeNode right = root.right;

        // 2、将左子树作为右子树
        root.left = null;
        root.right = left;

        // 3、将原先的右子树接到当前右子树的末端
        TreeNode p = root;
        while (p.right != null) {
            p = p.right;
        }
        p.right = right;



    }
}

 

结语: 写博客不仅仅是为了分享学习经历,同时这也有利于我巩固自己的知识点,总结该知识点,由于作者水平有限,对文章有任何问题的还请指出,接受大家的批评,让我改进。同时也希望读者们不吝啬你们的点赞+关注+收藏,你们的鼓励是我创作的最大动力!

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

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

相关文章

Python实用技巧:处理JSON文件写入换行问题

Python实用技巧&#xff1a;处理JSON文件写入换行问题 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程 &#x1f448; 希望得到您的订阅…

springboot启动自动配置

1.自定义项命名启动类规范&#xff1a; 功能在前名字在后比如 aliyun-oss-spring-boot-starter starter表示启动 springboot版本需要2.7.5 2.创建springboot工程把该删除的文件删除 3.启动类 pom文件导入自定义配置类依赖 比如 <!--第2步--><!-- 启动类 pom…

CSP-202209-3-防疫大数据

CSP-202209-3-防疫大数据 解题思路 一、数据结构定义 对于大模拟的题&#xff0c;合适的数据结构选择十分重要&#xff0c;正确的数据结构选择能够有效的提升解题效率 // 漫游消息结构体 struct RoamingData {int date, user, region; };vector<RoamingData> roamin…

阿里云服务器2024年最新价格表,高性价比配置任你选

随着云计算技术的不断发展&#xff0c;越来越多的企业和个人开始选择云服务器来搭建自己的业务平台。作为国内领先的云服务提供商&#xff0c;阿里云一直致力于为广大用户提供稳定、高效、安全的云计算服务。2024年&#xff0c;阿里云再次升级其服务器产品线&#xff0c;推出了…

FMM 笔记:st-matching(colab上执行)【官方案例解读】

在colab上运行&#xff0c;所以如何在colab上安装fmm&#xff0c;可见FMM 笔记&#xff1a;在colab上执行FMM-CSDN博客 st-matching见论文笔记&#xff1a;Map-Matching for low-sampling-rate GPS trajectories&#xff08;ST-matching&#xff09;-CSDN博客 0 导入库 from…

MySQL 数据优化技巧:提升百万级数据聚合统计速度

MySQL 数据优化技巧&#xff1a;提升百万级数据聚合统计速度 MySQL 数据优化技巧&#xff1a;提升百万级数据聚合统计速度摘要引言索引优化1. 使用合适的索引类型2. 聚簇索引的应用 查询优化3. 减少数据检索范围4. 避免全表扫描 数据库设计优化5. 合理划分数据表6. 使用分区表 …

nginx设置缓存时间

一、设置缓存时间 当网页数据返回给客户端后&#xff0c;可针对静态网页设置缓存时间&#xff0c;在配置文件内的http段内server段添加location&#xff0c;更改字段expires 1d来实现&#xff1a;避免重复请求&#xff0c;加快访问速度 第一步&#xff1a;修改主配置文件 #修…

面试经典150题【11-20】

文章目录 面试经典150题【11-20】388.O(1) 时间插入、删除和获取随机元素238.除自身以外数组的乘积134加油站135.分发糖果42. 接雨水13.罗马数字12.整数 转 罗马数字58.最后一个单词的长度14.最长公共前缀151.反转字符串中的单词 面试经典150题【11-20】 388.O(1) 时间插入、删…

IO进程:信号灯集

程序代码&#xff1a; sem.h: 1 #ifndef __SEM_H__2 #define __SEM_H__3 4 //创建信号灯集并初始化&#xff0c;semcount表示灯个数5 int open_sem(int semcount);6 7 //申请资源操作&#xff0c;semno表示灯的编号8 int P(int semid,int semno);9 10 //释放资源操作&#xff…

[LWC] Work with Data + Error Handling

目录 Overview Summary Use Cases for Interacting with Salesforce Data Handling Server Errors Sample Code Reference Overview Summary Use Cases for Interacting with Salesforce Data Handling Server Errors Sample Code Prerequisite: 1. Copy the ldsUtils f…

Python进阶学习:json.dumps()和json.dump()的区别

Python进阶学习&#xff1a;json.dumps()和json.dump()的区别 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程 &#x1f448; 希望得到您…

驻场人员严重划水,愈演愈烈,要请领导出面吗?

你有没有遇到过团队成员偷懒的情况&#xff1f;比如你们一起完成某个项目目标&#xff0c;干着干着你发现&#xff0c;就只有你和几个核心人员比较上心&#xff0c;很多人都在划水。 你可能会觉得这是因为大家工作态度不好&#xff0c;甚至怀疑他们的人品&#xff0c;忍不住想…

js里面有引用传递吗?

一&#xff1a;什么是引用传递 引用传递是相对于值传递的。那什么是值传递呢&#xff1f;值传递就是在传递过程中再复制一份&#xff0c;然后再赋值给变量&#xff0c;例如&#xff1a; let a 2; let b a;在这个代码中&#xff0c;let b a; 就是一个值传递&#xff0c;首先…

【Java程序设计】【C00276】基于Springboot的就业信息管理系统(有论文)

基于Springboot的就业信息管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的就业信息管理系统 本系统分为前台功能模块、管理员功能模块、学生功能模块、企业功能模块以及导师功能模块。 前台功能模块&…

勒索攻击新趋势,DarkSide解密工具

勒索攻击新趋势 2020年通过勒索病毒攻击已经成为网络犯罪分子热崇追捧的一种方式&#xff0c;全球几乎每天都有企业被勒索病毒攻击勒索&#xff0c;而且勒索的金额也越来越高&#xff0c;从几万美元到几千万美元不等&#xff0c;越来越多的黑客组织使用勒索病毒对企业发起攻击…

【python】学习笔记03-循环语句

3.1 whlie循环的基础语法 - while循环的语法格式 - while循环的注意事项 条件需提供布尔类型结果&#xff0c;True继续&#xff0c;False停止 空格缩进不能忘 请规划好循环终止条件&#xff0c;否则将无限循环 """ 演示while循环基础练习题&#xff1a;求1-10…

JSON:简介与基本使用

目录 什么是JSON&#xff1f; JSON的基本结构 JSON的基本使用 在JavaScript中使用JSON 创建JSON对象 解析JSON字符串 生成JSON字符串 在其他编程语言中使用JSON 总结 什么是JSON&#xff1f; JSON&#xff0c;全称为JavaScript Object Notation&#xff0c;是一种轻量…

matlab生成模拟的通信信号

matlab中rand函数生成均匀随机分布的随机数&#xff0c;randn生成正态分布的随机数&#xff1b; matlab来模拟一个通信信号&#xff1b; 通信信号通过信道时&#xff0c;研究时认为它会被叠加上服从正态分布的噪声&#xff1b; 先生成随机信号模拟要传输的信号&#xff0c;s…

C++基础知识(七:多态)

一、多态 常说的多态&#xff0c;是发生在类之间的多态 函数重载(静态多态/编译时多态) 类之间的多态(动态多态/运行时多态) 【1】前提 继承是多态的前提 虚函数 什么是多态&#xff1a;相同的代码&#xff0c;实现不同的功能 【2】函数重写(override) 必须有继承关系父类中必须…

python中的类与对象(2)

目录 一. 类的基本语法 二. 类属性的应用场景 三. 类与类之间的依赖关系 &#xff08;1&#xff09;依赖关系 &#xff08;2&#xff09;关联关系 &#xff08;3&#xff09;组合关系 四. 类的继承 一. 类的基本语法 先看一段最简单的代码&#xff1a; class Dog():d_…