遍历思路与子问题思路:详解二叉树的基本操作

news2024/9/21 4:24:16

二叉树的结构定义:

public class BinaryTree {
    //内部类 表示一个结点
    static class TreeNode {
        TreeNode left;    //左子树
        TreeNode right;    //右子树
        char value;    //结点值
        TreeNode(char value) {
            this.value = value;
        }
    }

    public TreeNode root;    //根节点
    ...
}

二叉树的基本操作包括:

// 前序遍历
void preOrder(Node root);

// 中序遍历
void inOrder(Node root);

// 后序遍历
void postOrder(Node root);

// 获取树中节点的个数
int size(Node root);

// 获取叶子节点的个数
int getLeafNodeCount(Node root);

// 获取第K层节点的个数
int getKLevelNodeCount(Node root,int k);

// 获取二叉树的高度
int getHeight(Node root);

// 检测值为value的元素是否存在
Node find(Node root, int val);

//层序遍历
void levelOrder(Node root);

// 判断一棵树是不是完全二叉树
boolean isCompleteTree(Node root);

本文以二叉树的几种基本操作为例,介绍实现二叉树操作的两种常见思路:遍历思路子问题思路。 


目录

一、二叉树的遍历思路与子问题思路 

1、遍历思路

2、子问题思路

3、图示

二、二叉树的基本操作

1、获取树中结点的个数

遍历思路

子问题思路

复杂度分析

2、获取叶子节点的个数

遍历思路

子问题思路

3、获取第K层结点的个数

子问题思路

4、求树的高度

子问题思路

复杂度分析

5、二叉树的中序遍历

遍历思路

子问题思路

6、二叉树的后序遍历

遍历思路

子问题思路 

7、检测值为value的元素是否存在,若存在返回该元素的结点

子问题思路


一、二叉树的遍历思路与子问题思路 

在二叉树是递归定义的,在二叉树相关的习题中也常常需要运用到递归。遍历思路与子问题思路是递归解决二叉树问题的两种常见思路。下面以“前序遍历二叉树”为例,简要介绍这两种思路及其实现。

题干

LeetCode 144. 二叉树的前序遍历

设计一个方法,给定二叉树的根节点 root ,返回它节点值的 前序 遍历。

注意:在力扣提供的代码模板中,前序遍历的方法是带有返回值List<Integer>的。因此,我们需要自行定义一个List<Integer> list集合来存放结果。

1、遍历思路

遍历思路很好理解:递归依次访问二叉树中的元素,每遇到一个元素就进行一次操作。例如:每遇到一个元素,就将这个元素打印;或每遇到一个元素,就使计数器加一。

在这道前序遍历的题中,遍历思路体现在:每递归到一个元素,就将这个元素放入list集合中。

依照这个思路,我们可以写出以下代码:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */

class Solution {
    List<Integer> list = new ArrayList<>();    //创建存储结果的集合list

    public List<Integer> preorderTraversal(TreeNode root) {
        if(root == null) {
            return list;
        }

        list.add(root.val);    //将当前元素加入集合list

        preorderTraversal(root.left);    //递归左树
        preorderTraversal(root.right);    //递归右数

        return list;    //返回结果
    }
}

但是,事实上这样的解答并不是很好,因为代码模板中给定的前序遍历方法是有返回值的。而上面的代码在递归时并没有接收返回值。也就是说,它的返回值被忽略的。

事实上,如果采用上面这种思路编写该代码,不需要返回值也是可行的:

public class BinaryTree {

    List<Integer> list = new ArrayList<>();    //创建存储结果的集合list

    //前序遍历
    public void preorderTraversal(TreeNode root) {
        if(root == null) {
            return;
        }

        list.add(root.val);    //将当前元素加入集合list

        preorderTraversal(root.left);    //递归左树
        preorderTraversal(root.right);    //递归右数
    }
}

或是拆成两个方法书写,本质不变但有细微改动:

    public List<Integer> preorderTraversal_(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        preOrder(root,list);
        return list;
    }
    public void preOrder(TreeNode root,List<Integer> list) {
        if(root == null) {
            return;
        }
        list.add(root.value);
        preOrder(root.left,list);
        preOrder(root.right,list);
    }

或是抛开该题,不用集合存放,直接以打印的方式输出前序遍历结果:

public class BinaryTree {
    //前序遍历
    public void preorderTraversal(TreeNode root) {
        if(root == null) {
            return;
        }

        System.out.print(root.value + " ");
        preOrder(root.left);
        preOrder(root.right);
    }
}

如此,每遇到一个元素,就将其放入list集合中。而list集合是成员变量,可以直接通过该类的对象进行访问。注意,由于所有元素都将存放在同一个list集合,这个list集合要定义成成员变量,放在方法的外面。如果放在了方法的内部,那么每一次递归,都将创建一个新的list,最终返回的list便不再是存放了所有元素的集合了。

2、子问题思路

子问题思路是将一个整体的大问题不断分割成规模更小但逻辑相同的小问题,再将小问题得到的结果整合到一块中。例如,校长要统计一个学校的所有学生人数,但校长不亲自直接清点每个学生,不一个一个地数,而是派任务给院长,让院长来统计每个院的人数;院长再让班长统计每个班的人数。如此,班长汇总后的结果上报给院长,院长汇总后的结果再上报给校长,校长便知道了整个学校的人数。

这样的表述可能有些抽象,我们代入到前序遍历的解题中,应该会更好理解。

在本题中采取子问题思路:前序遍历整棵二叉树 = 前序遍历左子树+前序遍历右子树。

我们可以充分用上方法的返回值。在这个方法内部,创建集合List<Integer> list,每执行这个方法,都创建一个集合list,代表遍历到当前元素时的遍历结果。将当前元素存入list集合中。

再定义List<Integer> left与List<Integer> right集合,分别接收递归左子树与递归右子树的返回值,即记录左子树与右子树的遍历结果。

获取到左子树的遍历结果后,通过list.addAll()立即将左子树的结果加入list集合;获取到右子树的遍历结果后,也同样立即将右子树的遍历结果加入到list集合。如此往复,到该二叉树的根结点时,就能获取到整棵树的遍历结果,最终将list返回即可。

因此,代码如下:

    public List<Integer> preorderTraversal(TreeNode root) {
        //在方法内部创建集合 存放结果
        List<Integer> list = new ArrayList<>();

        if (root == null) {
            return list;
        }

        list.add(root.val);    //将当前元素加入集合中

        List<Integer> left = preorderTraversal(root.left);    //递归遍历左树
        list.addAll(left);    //将左树遍历结果加入当前集合中
        List<Integer> right = preorderTraversal(root.right);    //递归遍历右树
        list.addAll(right);    //将右树遍历结果加入当前集合中

        return list;
    }

3、图示

如下是前序遍历A的左子树的示意图。


二、二叉树的基本操作

1、获取树中结点的个数

遍历思路

定义成员变量size作计数器,每遇到一个结点,令size++,最终统计出所有结点个数。注意,该写法有一定的缺陷。当对同一个对象调用两次size()方法时,size的数值会叠加。即使加static,也会有不同对象调用size()方法时的数值叠加问题,从而导致结果错误。

    //获取二叉树中节点个数 遍历思路
    public int size;
    public void size(TreeNode root) {
        if(root == null) {
            return;
        }
        this.size++;
        size(root.left);
        size(root.right);
    }

子问题思路

整个二叉树结点个数 = 左子树的节点个数+右子树的节点个数+1(自己本身)。保存递归后的左子树结点个数与右子树结点个数,left+right+1即所求。

    //获取二叉树中节点个数 子问题思路
    public int size(TreeNode root) {
        if(root == null) {
            return 0;
        }
        int left = size(root.left);
        int right = size(root.right);
        return left+right+1;
    }

复杂度分析

时间复杂度:O(N) (要递归二叉树上的每一个结点)

空间复杂度:O(logN) (递归一定会在栈上开辟内存。而在递归右子树时,左子树已经递归完了。即给右子树开辟栈帧时,左子树的栈帧不会同时占用。在满二叉树的情况下,递归开辟的内存空间就等于二叉树的高度。当然,如果是单分支的树,那么空间复杂度就是O(N)。但一般这样的情况较少。)

2、获取叶子节点的个数

遍历思路

用成员变量leafCount标记叶子节点的数目。当左子树与右子树都为null时,这个结点是叶子结点,此时令leafCount++即可。

    //获取叶子节点的个数
    int leafCount;
    public void getLeafNodeCount3(TreeNode root) {
        if(root == null) {
            return;
        }
        if(root.left == null && root.right == null) {
            leafCount++;
        }
        getLeafNodeCount(root.left);
        getLeafNodeCount(root.right);
    }

子问题思路

整棵二叉树的叶子结点个数 = 左子树的叶子结点个数+右子树的叶子结点个数

叶子结点 = 左子树和右子树都为null的结点

掌握这两个结论,可以写出如下代码:

    //获取叶子节点的个数
    public int getLeafNodeCount2(TreeNode root) {
        if(root == null) {
            return 0;
        }

        if(root.left == null && root.right == null) {
            return 1;
        }

        int left = getLeafNodeCount2(root.left);    //左子树中叶子节点的个数
        int right = getLeafNodeCount2(root.right);    //右子树中叶子节点的个数

        return left+right;
    }

    //获取叶子节点的个数
    public int getLeafNodeCount(TreeNode root) {
        if(root == null) {
            return 0;
        }

        int left = getLeafNodeCount(root.left);
        int right = getLeafNodeCount(root.right);

        if(left == 0 && right == 0) {
            return 1;
        }
        return left+right;
    }

从结果上看。两段代码都是正确的。


注意:在一些问题上,子问题的思路分析能够使问题变得更清晰、简便。


3、获取第K层结点的个数

子问题思路

该题的思路比较巧妙。要求第K层的结点个数,一整棵树的第K层 = 它的左子树的第K-1层 = 它的右子树的第K-1层。

一棵树第K层的结点个数 = 左子树第K-1层的结点个数+右子树第K-1层的结点个数。

可以写出如下代码,如果对代码不理解,可以参照前序遍历的图示将递归画图展开。 

    //获取第k层结点的个数
    public int getKLevelNodeCount(TreeNode root,int k) {
        if(root == null) {
            return 0;
        }
        if(k == 1) {
            return 1;
        }

        int left = getKLevelNodeCount(root.left,k-1);
        int right = getKLevelNodeCount(root.right,k-1);

        return left+right;
    }

4、求树的高度

🔗LeetCode-104. 二叉树的最大深度

子问题思路

根据二叉树高度的定义:树的高度或深度是树中结点的最大层次。

一棵二叉树的高度 = 左子树高度与右子树高度的最大值 + 1

由此通项公式,可以写出求二叉树高度的代码:

    //二叉树的高度
    public int getHeight(TreeNode root) {
        if(root == null) {    //空树高度为0 返回0
            return 0;
        }

        int left = getHeight(root.left);    //记录左子树的高度
        int right = getHeight(root.right);    //记录右子树的高度
        return Math.max(left,right) + 1;    //返回左子树和右子树的高度的最大值+1,即树的高度
    }

特别注意:若不用变量left和right保存左右子树的高度递归结果,而是直接将递归语句代入求最大值的语句,是不合适的:

虽然在本地IDE中运行,结果不会有错,但在在线OJ中运行,会超出时间限制。因为没有用额外的变量来保存递归结果的话,最终return语句中实际上有四个递归语句,有大量的递归的重复计算,这是非常耗时的。

复杂度分析

时间复杂度:O(N),每一个结点都要求一次以它为根的树的高度,也就是每一个结点实际都会访问的。

空间复杂度:O(height),和树的高度有关系。若是单分支的树,则高度为N,空间复杂度即为O(N),若是满二叉树,则高度为logN,空间复杂度即为O(logN。

5、二叉树的中序遍历

与前面前序遍历的代码相似,只是改变了对元素的操作与递归左右子树的顺序(左根右)。

遍历思路

    //中序遍历 递归 遍历思路
    public void inOrder(TreeNode root) {
        if(root == null) {
            return;
        }
        inOrder(root.left);
        System.out.print(root.value + " ");
        inOrder(root.right);
    }

子问题思路

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
        public List<Integer> inorderTraversal(TreeNode root) {

        List<Integer> list = new ArrayList<>();

        if (root == null) {
            return list;
        }

        List<Integer> left = inorderTraversal(root.left);
        list.addAll(left);

        list.add(root.val);

        List<Integer> right = inorderTraversal(root.right);
        list.addAll(right);

        return list;
    }
}

6、二叉树的后序遍历

与前面前序遍历的代码相似,只是改变了对元素的操作与递归左右子树的顺序(左右根)。

遍历思路

    //后序遍历 递归 遍历思路
    public void postOrder(TreeNode root) {
        if(root == null) {
            return;
        }
        postOrder(root.left);
        postOrder(root.right);
        System.out.print(root.value + " ");
    }

子问题思路 

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
        public List<Integer> postorderTraversal(TreeNode root) {

        List<Integer> list = new ArrayList<>();

        if (root == null) {
            return list;
        }

        List<Integer> left = postorderTraversal(root.left);
        list.addAll(left);

        List<Integer> right = postorderTraversal(root.right);
        list.addAll(right);

        list.add(root.val);

        return list;
    }
}

7、检测值为value的元素是否存在,若存在返回该元素的结点

子问题思路

检测值为value的元素在整棵二叉树是否存在 = 检测左树是否存在该节点(如果存在便结束检测并返回) + 检测右树是否存在该结点(如果存在便结束检测并返回)

该代码的思路类似于前序遍历的思路。每遍历到一个结点,就判断这个结点的值是否与给定的value相匹配。

    //检测值为value的元素是否存在  子问题思路
    public TreeNode find(TreeNode root, int val) {
        if(root == null) {
            return null;
        }
        if(root.value == val) {
            return root;
        }

        TreeNode left = find(root.left,val);
        if(left != null) {
            return left;
        }
        TreeNode right = find(root.right,val);

        return right;
    }

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

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

相关文章

云原生时代下,应用全生命周期管理之道

引言 过去 10 年间&#xff0c;云计算已经从单一的 IT 服务演变成为新一代的软件架构范式&#xff0c;进而赋能企业管理和生产模式的创新。云计算也经历了从“资源上云”到“深度用云”的发展阶段。 在云原生时代&#xff0c;应用全生命周期管理之道成为企业关注的一个焦点。在…

蓝牙耳机什么品牌的音质好?300左右音质最好的蓝牙耳机推荐

随着蓝牙技术的发展&#xff0c;蓝牙耳机品牌也越来越多。要说什么品牌的音质好&#xff1f;首先还是要根据自己的预算出发。在此&#xff0c;我来给大家推荐几款300左右音质最好的蓝牙耳机&#xff0c;可以当个参考。 一、南卡小音舱Lite2蓝牙耳机 参考价&#xff1a;239 发…

Self-supervised learning of a facial attribute embedding from video

Self-supervised learning of a facial attribute embedding from video 译题&#xff1a;视频中人脸属性嵌入的自监督学习 论文题目Self-supervised learning of a facial attribute embedding from video译题视频中人脸属性嵌入的自监督学习时间2018年开源代码地址https://…

《Java》基本类型的比较和引用类型的比较

目录 基本类型 引用数据类型 基本类型之间的比较 基于Comparable的比较 总结 &#x1f451;作者主页&#xff1a;Java冰激凌 &#x1f4d6;专栏链接&#xff1a;Java 基本类型 Java中提供了基本类型有八种 分别是 byte short int long float double char boolean 基本类型…

EL 表达式--各种运算-代码演示--EL 的 11 个隐含对象--pageContext 对象介绍--JSTL 标签库介绍--core 核心库--综合代码

目录 EL 表达式 EL 表达式介绍 代码示例 EL 常用输出形式 代码演示 Book.java el_input.jsp EL 运算操作 基本语法语法&#xff1a; 关系运算 逻辑运算 算数运算 EL 的 empty 运算 应用实例 empty.jsp EL 的三元运算 应用实例 EL 的 11 个隐含对象&#xff0c…

Unity-ML-Agents-训练生成的results文件解读-PushBlock

前言 训练结果文件路径&#xff1a;E:\ml-agents-release_19\results\push_block_test_02&#xff08;具体路径以自己电脑为准&#xff09; ML-Agents安装和PushBlock训练过程请见&#xff1a;&#xff08;注意&#xff1a;push_block_test_02没有全部训练完毕&#xff09; …

同样是测试,朋友到了30k,我才12K,这份测试面试8股文确实牛

程序猿在世人眼里已经成为高薪、为人忠诚的代名词。 然而&#xff0c;小编要说的是&#xff0c;不是所有的程序员工资都是一样的。 世人所不知的是同为程序猿&#xff0c;薪资的差别还是很大的。 众所周知&#xff0c;目前互联网行业是众多行业中薪资待遇最好的&#xff0c;…

推荐几款项目管理工具,提高你的团队协作效率

如何管理团队才能使团队发挥最大的价值&#xff0c;如果团队缺少协作&#xff0c;就会因为团队的内耗和冲突导致项目无法完成&#xff0c;如何提高团队协作效率呢&#xff1f;我们可以借助团队协作类的项目管理工具。 几个常见的项目管理工具&#xff1a; 1、进度猫 进度猫是…

MySQL高级第十五篇:MVCC多版本并发控制原理剖析

MySQL高级第十五篇&#xff1a;MVCC多版本并发控制原理剖析 一、什么是MVCC&#xff1f;二、快照读与当前读&#xff1f;1. 快照读2. 当前读 三、MVCC实现原理&#xff08;ReadView&#xff09;1. 隐藏字段2. Read View3. 思路设计4. ReadView使用规则5. MVCC整体操作流程 四、…

响应式开发HTML5CSS3实现视频播放器的功能案例

目录 前言 一、本视频播放器需要实现的功能 ​二、代码分布结构 三、部分主要代码 1.index01.html 2.video1.css 3.video1.js 四、images图片资源及视频 五、运行效果 前言 1.本文讲解的响应式开发技术&#xff08;HTML5CSS3Bootstrap&#xff09;的HTML5视频播放器等…

随想录Day59--单调栈: 503.下一个更大元素II , 42. 接雨水

看到下一个更大&#xff0c;最先想到的就是单调栈。所以503.下一个更大元素II可以用单调栈的思路进行求解&#xff0c;其实这道题和496.下一个更大元素 I的思路是一样的&#xff0c;不过是多了一个首位相连的环状条件&#xff0c;这时候可以想到&#xff0c;把数组再复制遍历&a…

推荐系统|多目标建模|多目标优化|跨域多目标算法演进

目录 多目标建模总结 推荐系统——多目标优化 网易严选跨域多目标算法演进 背景介绍 多目标建模及优化 1.样本与特征 2. 模型结构迭代 3. 位置偏差与 Debias 4. 多目标 Loss 优化 5. 跨域多目标建模 多目标建模总结 http://t.csdn.cn/H514i 常见的指标有点击率CTR、…

电、气物联网联合管理监测方案

一、概述 水、电、气联合管理就是把同一个用户的用电计量和用水计量、用气计量统一到一个账户&#xff08;同时具有子账户&#xff09;&#xff0c;用一套软件进行统一管理&#xff0c;当账户余额不足时&#xff0c;可实行停电催费&#xff0c;从而既达到预付费的目的&#xff…

hue源码编译,替换cloudera manage hue,解决hue滚动条bug问题

一.安装依赖 yum install python python-dev python-setuptools python-pip \ libkrb5-dev libxml2-dev libxslt-dev libssl-dev \ libsasl2-dev libsqlite3-dev libldap2-dev \ libffi-dev nodejs npm cmake make gcc g++ 二.拉取源码 wget https://github.com/cloudera/hue/a…

机器学习笔记之K近邻学习算法

机器学习笔记之K近邻学习算法 引言回顾&#xff1a;投票法回顾&#xff1a;明可夫斯基距离 K \mathcal K K近邻算法算法描述 K \mathcal K K值的选择小插曲&#xff1a;懒惰学习与急切学习 KD \text{KD} KD树描述及示例 K \mathcal K K近邻 VS \text{ VS } VS 贝叶斯最优分类器…

汽车基础软件信息安全与AUTOSAR

AUTOSAR 信息安全框架和关键技术分析 随着汽车网联化和智能化,汽车不再孤立,越来越多地融入到互联网中。在这同时,汽车也慢慢成为潜在的网络攻击目标,汽车的网络安全已成为汽车安全的基础,受到越来越多的关注和重视。AUTOSAR 作为目前全球范围普遍认可的汽车嵌入式软件架…

HDFS FileSystem 导致的内存泄露

目录 一、问题描述 二、问题定位和源码分析 一、问题描述 ftp程序读取windows本地文件写入HDFS&#xff0c;5天左右程序 重启一次&#xff0c;怀疑是为OOM挂掉&#xff0c;马上想着就分析 GC日志了。 ### 打印gc日志 /usr/java/jdk1.8.0_162/bin/java \-Xmx1024m -Xms512m …

Net2FTP搭建免费web文件管理器『打造个人网盘』

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; 前言 文件传输可以说是互联网最主要的应用之一&#xff0c;特别是智能设备的大面积使用&#xff0c;无论是个人存储文件资料&#xff0c;还是商业文件流转&#xff0c…

老杨说运维 | 数智时代,运维一体化如何落地实践?

在IT运维的发展过程中&#xff0c;随着分布式架构的加速推进&#xff0c;云原生技术加入应用&#xff0c;运维工具相比过去呈现出了更高强度的进化态势&#xff0c;即从多个相对独立的软件向EA形态的一体化系统进化。本次樱花论坛正是基于这一新的变革点&#xff0c;邀请了行业…

(十二)rk3568 NPU 中部署自己训练的模型,(2)模型转换

对于rknn 模型部署,本人使用*.pt -> *.onnx -> *.rknn的方式。 一、首先是pt文件到onnx文件的转换。 onnx文件导出时,需要修改models/yolo.py文件中的后处理部分。 注意:在训练时不要修改yolo.py的这段代码,训练完成后使用export.py进行模型导出转换时一定要进行修…