【Java数据结构】二叉树

目录

    • 树的特征
    • 树的概念
  • 二叉树
    • 两种特殊的二叉树
    • 二叉树的性质
    • 二叉树的基本操作
      • 4 种遍历二叉树的方式
      • 判断一棵树是不是完全二叉树
      • 获取二叉树总共的节点个数
      • 获取叶子节点的个数
      • 获取第 k 层的节点个数
      • 获取二叉树的高度
      • 检测值为 value 的元素是否存在
    • 二叉树基本操作完整代码


一棵树是由若干个不相交的子树组成的,所以树是递归定义的.
做二叉树相关的 oj 题会经常使用递归的方法解决.

树的特征

  • 子树之间是不相交的。
  • 除了根节点外,每个节点有且仅有一个父亲节点。
  • 一棵有 N 个节点的树有 N - 1 条边

树的概念

下图就是一棵树,用这棵树来举例树的概念。
Alt

  • 节点的度: 一个节点含有的子树个数。例如在上面这棵树中 A 的度为 6,E 的度为2, F 的度为 3。
  • 树的度: 一棵树中,所有 节点的度 的最大值。 例如在上面这棵树中 树的度就是 6,因为节点度的最大值是 A 的度。
  • 叶子节点 / 终端节点: 指 度为 0 的节点。例如在上面这棵树中 B C H I P Q K L M N 都是叶子节点。
  • 父亲节点 / 父节点:若一个节点有 孩子节点,那么这个节点就是其 孩子节点 的父节点。例如 A 是 B 的父亲节点。
  • 孩子节点 / 子节点:一个节点 含有一棵子树的根节点,这棵子树的根节点 就是 该节点 的孩子节点。例如 B 是 A 的子节点。
  • 根节点: 在一棵树中,没有 父亲节点 的节点。 例如上面这棵树中只有节点 A 是根节点。
  • 节点的层次: 从根开始,根是第一层,根的子节点是第二层,根的子节点的子节点是第三层,以此类推。例如:P 在第 4 层。
  • 树的高度 / 树的深度: 树中节点的最大层次。例如上面这棵树的高度是 4.

二叉树

根据树的递归定义,如果这棵树是二叉树,那么他的每棵子树都是二叉树。
二叉树要么为空,要么有一个根节点加上左子树和右子树。
二叉树中每个节点的度 <= 2

两种特殊的二叉树

  • 完全二叉树: 每一层(除最后一层)都被完全填满,且最后一层的节点必须从左到右依次排列,没有空缺。alt

  • 满二叉树:每个节点要么是叶子节点,要么有两个孩子节点,且每一层都必须被完全填满,没有空缺。满二叉树满足:层数为 k , 则节点总数就是 2k - 1.alt

二叉树的性质

  1. 若根节点的层数为 1 ,则一棵非空二叉树的第 i 层上最多有 2i-1 (i > 0) 个节点
  2. 若只有根节点的深度为 1 , 则深度为 k 的二叉树的最大节点就是 2k-1 (k >= 0)
  3. 对于任何一棵二叉树,如果其叶子节点的个数为 n0, 节点的度 为 2 的非叶子节点个数为 n2. 则有 n0 = n2 + 1.也就是说,对于任何一棵二叉树,叶子节点个数永远比度为 2 的节点个数多一个。 证明如下:Alt
  4. 具有 n 个节点的完全二叉树的深度 k 为 log2(n + 1) 向上取整。推导:由性质 2 可得知 n = 2k-1 --> 2k = n + 1 --> k = log2(n + 1).
  5. 对于具有 n 个节点的完全二叉树,如果按照从上到下从左到右的顺序对所有节点从 0 开始编号,则: Alt

二叉树的基本操作

4 种遍历二叉树的方式

前序遍历,中序遍历,后序遍历,层序遍历。
前中后序遍历的区别就是 访问根节点的时机不同
前序遍历:遇到根就打印 --> 遍历左子树 --> 遍历右子树。
中序遍历:左子树全部遍历完 --> 打印根 --> 遍历右子树。
后序遍历:左子树全部遍历完 --> 右子树全部遍历完 --> 打印根

已知前序遍历和中序遍历的结果,可以把二叉树创建出来,
已知中序遍历和后序遍历的结果,可以把二叉树创建出来,
已知前序遍历和后序遍历的结果,不能把二叉树创建出来。
因为前序遍历和后序遍历可以确定根节点,中序遍历可以确定左右树有那些节点。

接下来分别用 java 代码实现 前中后序遍历 和 层序遍历,其中前中后序遍历分别用递归和非递归两种方法实现。

前序遍历题目链接: 点击这里
前序遍历递归思路:遇到根就打印 --> 遍历左子树 --> 遍历右子树
Alt
以这棵二叉树为例,此时根就是 1 ,立即打印 1 之后遍历左子树,
Alt

此时左子树的根就是 2 ,立即打印 2 之后继续遍历 2 的左子树,
Alt

2 的左子树的根是 3, 立即打印 3 之后继续遍历 3 的左子树,
alt

发现此时 3 的左子树没有根,即 根为 null 的时候结束递归,返回空.
这时 3 的左子树已经全部遍历完成,遍历 3 的右子树,发现 3 的右子树返回的也是空。
然后此时 根为 3 的二叉树已经全部遍历完成了,根为 2 的左子树的返回值就是 3,之后的过程以此类推。

alt

前序遍历非递归思路:
变量 cur 表示此时遍历到的节点.
变量 top 存储出栈的元素并访问该节点的右树.

alt
如果该节点 (cur) 不为空,把 cur 指向的节点入栈,然后打印,最后让 cur 指向 左边的节点。重复这个步骤。
alt
一直循环到该节点为空的时候,说明 D 的左边已经遍历完成,
出栈 D 并让 top 指向 D 为了访问 D 的右边。
alt
当 D 的左边右边都遍历完成之后, B 的左树都遍历完成了,把 B 出栈并让 top 重新指向 B 把 D 给覆盖了.遍历 B 的右树,以此类推…直到栈为空的时候且 cur 没有指向任何节点的时候,说明遍历完成了。

代码实现:

//前序遍历递归实现
class Solution {public List<Integer> preorderTraversal(TreeNode root) {//创建一个顺序表用于存储遍历的结果List<Integer> preorder = new ArrayList<>();//递归终止条件:如果这个根是空的,返回空的顺序表if(root == null) {return preorder;}//打印根preorder.add(root.val);//遍历左树,并把遍历左树的结果存储到变量 left 上List<Integer> left = preorderTraversal(root.left);//把遍历左树的结果全部存到 ret 上preorder.addAll(left);//遍历右树,并把遍历右树的结果存储到变量 right 上List<Integer> right = preorderTraversal(root.right);//把遍历右树的结果全部存到 ret 上preorder.addAll(right);return preorder;}
}
//前序遍历非递归实现
class Solution {public List<Integer> preorderTraversal(TreeNode root) {List<Integer> preorder = new LinkedList<>();Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;TreeNode top = new TreeNode();//用非递归方式进行前序遍历while(cur != null || !stack.isEmpty()) {while(cur != null) {stack.add(cur);preorder.add(cur.val);//打印节点cur = cur.left;}top = stack.pop();cur = top.right;}return preorder;}
}

中序遍历题目链接: 点击这里
知道前序遍历的思路就能举一反三,中序遍历主要提一下非递归的注意点:
中序遍历与前序遍历的代码只有一点不同:根节点打印的时机。
Alt

代码实现:

//中序遍历递归实现
class Solution {public List<Integer> inorderTraversal(TreeNode root) {//创建一个链表用于存储遍历的结果List<Integer> inoreder = new ArrayList<>();//递归终止条件if(root == null) {return inoreder;}//先遍历左树List<Integer> left = inorderTraversal(root.left);inoreder.addAll(left);//打印根节点inoreder.add(root.val);//遍历右树List<Integer> right = inorderTraversal(root.right);inoreder.addAll(right);return inoreder;}
}
//中序遍历非递归实现
class Solution {public List<Integer> inorderTraversal(TreeNode root) {//创建一个链表用于存储中序遍历的结果LinkedList<Integer> inoreder = new LinkedList<>();//中序遍历的非递归实现Stack<TreeNode> stack = new Stack<>();//栈用于暂时保存还未输出的节点TreeNode cur = root;while(cur != null || !stack.isEmpty()) {//遍历左树,左树不为空,把节点记录到栈中并继续遍历该节点的左树while(cur != null) {stack.push(cur);cur = cur.left;}//如果左树为空,打印栈顶元素,也就是该左树这个整体的 rootTreeNode top = stack.pop();inoreder.add(top.val);//找到右树cur = top.right;}return inoreder;}
}

后序遍历题目链接: 点击这里
后序遍历的递归思路与前中序遍历相比只是打印元素的时机不同罢了。
主要来看非递归的思路:
遍历左树的时候和中序遍历一样。

while(cur != null) {stack.push(cur);cur = cur.left;
}

alt
此时不能像中序遍历那样直接 pop,如果 top 的右树不为空,肯定有新的节点入栈,先遍历完 top 的右树之后,再打印根节点。
alt

alt
此时 D 的右树 K 已经打印完成,top 重新指向 D, 此时发现 top.right != null,但是 D 的右树确实已经都打印了,此时应该要把 D 出栈并打印 D。
为了检查 D 的右边 K 是否已经打印了,再创建一个变量 prev 用于指向已经打印完的右树,如果 top.right == prev , 说明 top 的右树已经全部打印完成,把 D 出栈并打印 D。以此类推。

alt

代码实现:

//后序遍历递归实现
class Solution {public List<Integer> postorderTraversal(TreeNode root) {//创建一个顺序表储存遍历的结果List<Integer> postorder = new ArrayList<>();//终止条件if(root == null) {return postorder;}//遍历左树List<Integer> left = postorderTraversal(root.left);postorder.addAll(left);//遍历右树List<Integer> right = postorderTraversal(root.right);postorder.addAll(right);//打印根postorder.add(root.val);return postorder;}
}
//后序遍历非递归实现
class Solution {public List<Integer> postorderTraversal(TreeNode root) {//创建一个顺序表储存遍历的结果List<Integer> postorder = new ArrayList<>();Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;TreeNode prev = null;while(cur != null || !stack.isEmpty()) {//遍历左树与中序遍历相同while(cur != null) {stack.push(cur);cur = cur.left;    }//此时不能像中序遍历那样直接 pop,//如果 top 的右树不为空,肯定有新的节点入栈,//先遍历完 top 的右树之后,再打印根节点。TreeNode top = stack.peek();//prev 指的是最近被打印的那个节点,//如果没有设置 prev 这个变量且 top 的右树不为空,就会死循环,//cur 永远指向 top.right 无法跳出循环;if(top.right == null || top.right == prev) {stack.pop();postorder.add(top.val);//此时 prev 指向最新打印的节点prev = top;}else {cur = top.right;}}return postorder;}
}

层序遍历题目链接: 点击这里
思路:
alt
alt

代码实现:

class Solution {public List<List<Integer>> levelOrder(TreeNode root) {List<List<Integer>> list = new ArrayList<>();//存储层序遍历的结果Queue<TreeNode> queue = new LinkedList<>();queue.offer(root);while(!queue.isEmpty()) {ArrayList arrayList = new ArrayList<>();//存储该层的所有元素int qSize = queue.size();//size 记录该层的元素个数//把 cur 所有 非null 的左右树入队while(qSize != 0) {TreeNode cur = queue.poll();qSize--; if(cur != null) {queue.offer(cur.left);queue.offer(cur.right);//把 cur 的左右树遍历完成之后存储 cur 的值arrayList.add(cur.val);}}//把该层所有的元素存储到 arrayList if(arrayList.size() > 0) {list.add(arrayList);}}return list;}
}

判断一棵树是不是完全二叉树

思路:
利用层序遍历的方式遍历这棵树,如果这棵树是完全二叉树,在队列中不可能出现既有空且后面又有非空节点的情况。当队列里只有 null 的时候就是完全二叉树。

代码实现:

class Solution {
// 判断一棵树是不是完全二叉树,// 二叉树遍历到 null 的时候停止, 此时队列里只有 null 就是完全二叉树boolean isCompleteTree(TreeNode root){Queue<TreeNode> queue = new LinkedList<>();queue.offer(root);while(!queue.isEmpty()) {TreeNode cur = queue.poll();//因为这个方法会在队列中存储 null ,所以会出现 cur == null的情况//一旦遍历到 null 的时候就结束循环,检查队列中是否出现 非null 的元素if(cur != null) {queue.offer(cur.left);queue.offer(cur.right);}else {break;}}//把队列里的元素一个一个出队, 如果有元素 != null 就不是完全二叉树while(!queue.isEmpty()) {TreeNode cur = queue.poll();if(cur != null) {return false;}}return true;
}

获取二叉树总共的节点个数

用子问题思路:二叉树总共节点个数 == 左子树的节点个数 + 右子树的节点个数 + 1(根节点)
递归结束条件:当根节点为空时,就是没有节点,返回 0 个。

代码实现:

// 获取树中 结点的个数 == 左子树的结点个数 + 右子树的结点个数 + 根结点int size(TreeNode root) {if(root == null) {return 0;}return size(root.left) + size(root.right) + 1;}

获取叶子节点的个数

子问题思路: 整棵树的叶子节点个数 == 左子树的叶子节点个数 + 右树叶子节点个数
递归结束条件:

  1. 当根节点为空时,就是没有叶子节点,返回 0 ;
  2. 当该节点的左树和右树都为空时,这个节点就是叶子节点,返回 1.

代码实现:

    // 子问题思路-求叶子结点个数// 整棵树的叶子结点个数 == 左子树的叶子结点个数 + 右子树的叶子结点个数int getLeafNodeCount(TreeNode root){//判断叶子结点的条件, 如果该结点是叶子结点,返回 1if(root == null) {return 0;}else if(root.left == null && root.right == null){return 1;}else {return getLeafNodeCount(root.left) +getLeafNodeCount(root.right);}}

获取第 k 层的节点个数

用子问题思路: 获取 k 层的节点个数 == 获取左子树第 k - 1 层的节点个数 + 获取右子树第 k - 1 层的节点个数。
递归结束条件:

  1. 根节点为空时,没有节点,返回 0 ;
  2. 当 k == 1 时,返回 1,因为左子树的第一层和右子树的第一层都只有一个节点。

代码实现:

    // 获取第K层节点的个数 == 左子树的第 k-1 层结点个数 + 右子树的第 k-1 层结点个数int getKLevelNodeCount(TreeNode root,int k){if(root == null) {return 0;}else if(k == 1) {return 1;}else {return getKLevelNodeCount(root.left, k-1) +getKLevelNodeCount(root.right, k-1);}}

获取二叉树的高度

用子问题思路: 二叉树的高度 == (左子树的高度 与 右子树的高度 取最大值) + 1
递归结束条件: 根节点为空时,高度为 0 ,返回 0。

代码实现:

    // 获取二叉树的高度 == 左子树的高度和右子树的高度取最大值 + 1int getHeight(TreeNode root){if(root == null) {return 0;}else {int leftHeight = getHeight(root.left);int rightHeight = getHeight(root.right);return leftHeight > rightHeight ? leftHeight + 1 :rightHeight + 1;}}

检测值为 value 的元素是否存在

用子问题思路:根节点的左子树是否存在 || 根节点的右子树是否存在

递归结束条件:

  1. 如果根节点为空,就不存在,返回空;
  2. 如果根节点是 value ,即存在,返回该节点的引用.

代码实现:

    // 检测值为value的元素是否存在 1.空树  2. 根是不是 val 不是继续  3.左子树和右子树TreeNode find(TreeNode root, int val){if(root == null) {return null;} else if (root.val == val) {return root;}else {TreeNode left = find(root.left, val);TreeNode right = find(root.right, val);return left != null ? left : right;}}

二叉树基本操作完整代码

点击这里查看


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

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

相关文章

VS code 安装使用配置 Continue

Continue 插件介绍 Continue 是一款高效的 VS Code 插件&#xff0c;提供类似 GitHub Copilot 的功能&#xff0c;旨在提升开发者的编程效率。其配置简单&#xff0c;使用体验流畅&#xff0c;深受开发者喜爱。 主要功能特点 智能代码补全 Continue 能够基于当前代码上下文生…

年化60.7%,最大回撤-16.5%,RSRS标准分择时效果差不多

原创内容第653篇&#xff0c;专注量化投资、个人成长与财富自由。 中秋节&#xff0c;祝大家中秋快乐&#xff01; 人有悲欢离合&#xff0c;月有阴晴圆缺&#xff0c;此事古难全。但愿人长久&#xff0c;千里共婵娟。 今天引入RSRS来择时&#xff0c;看下策略效果。 年化60.7…

Python编码系列—Python代理模式:为对象赋予超能力的魔法

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

C++掉血迷宫

目录 开头程序程序的流程图程序游玩的效果下一篇博客要说的东西 开头 大家好&#xff0c;我叫这是我58。 程序 #include <iostream> #include <string> #include <cstring> using namespace std; enum RBYG {R 1,B 2,Y 4,G 7, }; struct heal {int ix…

【例题】lanqiao549 扫雷

输入 3 4 0 1 0 0 1 0 1 0 0 0 1 0输出 2 9 2 1 9 4 9 2 1 3 9 2解题思路 分类讨论&#xff1a; 如果原来的方格整数为1&#xff0c;输出9如果原来的方格整数为0&#xff0c;输出周围8个&#xff08;最多八个&#xff09;的地雷数量和 代码 如何遍历一个方格mp[i][j]周围…

c++中引用是通过指针的方式实现

其实在汇编层面上&#xff0c;引用的代码和指针的代码是一致的。 先看指针情况下的代码分析&#xff0c;如下所示&#xff1a; #include <iostream>using namespace std;void fuzhi(int *x)//引用传参 {*x 10; }int main(int argc, char** argv) {int a 0;int b;a …

架构设计——概念和基础

&#x1f3e0;1 架构基础 想要搞清楚架构到底指什么&#xff0c;架构与框架的区别&#xff0c;就需要了解梳理系统、子系统、模块、组件、框架和架构 1.1系统与子系统 1.1.1系统 wiki:系统泛指由一群有关联的个体组成&#xff0c;根据某种规则运作&#xff0c;能完成个别元…

Python编码系列—Python外观模式:简化复杂系统的快捷方式

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

QT安装时出现错误(镜像)

QT下载网站 下载网址 QT安装时出现错误 解决方法 按“win+R”键弹出“运行”窗口,输入"cmd",点击确定; 打开如下图运行框,将Qt文件拖到窗口里->空一格输入“–mirror https://mirrors.aliyun.com/qt”->按enter键进入,即可成功安装 正式安

gazebo遇到的阶段性问题汇总

目录 1 gazebo中碰撞模型崩坏或者飞的问题2 编译报错解决方法 3 控制器无法正常启动解决方法 4 xacro:macro 定义函数5 xacro:property 定义变量的值报错截图解决方法 6 gazebo 模型视觉穿模&#xff08;已设置碰撞体积&#xff09;解决方法穿模截图 1 gazebo中碰撞模型崩坏或者…

王道408考研数据结构-绪论

1.1 数据结构的基本概念 数据结构 数据结构是相互之间存在一种或多种特定关系的数据元素的集合。在任何问题中&#xff0c;数据元素 都不是孤立存在的&#xff0c;它们之间存在某种关系&#xff0c;这种数据元素相互之间的关系称为结构(Structure)。 数据结构包括三方面的内…

中秋的“超级月亮”在哪?来竹海幻境寻找心中的白月光

夜幕低垂&#xff0c;一场视觉盛宴悄然拉开序幕——《桃花江竹海幻境》&#xff08;下文简称《竹海幻境》&#xff09;剧场中。一轮轮明月仿佛穿越时空的使者&#xff0c;与葱郁的竹林交相辉映&#xff0c;与天际那轮皎洁的明月共同编织出一幅“超级月亮”的绝美画卷&#xff0…

sizeof与strlen()函数的对比总结

目录 1.sizeof操作符1.1sizeof操作符特点 2.strlen( )函数2.1 函数简介2.2 创建字符串 3.sizeof 和 strlen的对比 1.sizeof操作符 在学习操作符的时候&#xff0c;我们学习了 sizeof &#xff0c; sizeof 计算变量所占内存内存空间⼤⼩的&#xff0c;单位是字节&#xff0c;如…

C++的类与对象下

目录 1.初始化列表 2.隐式类型转换 1.单参数 2.多参数&#xff08;C11提供的新功能&#xff09; 3.static成员 4.友元 5.内部类 6.匿名对象 1.初始化列表 C祖师爷规定初始化列表是成员变量定义与初始化的地方。 class Time { public:Time(int hour):_hour(hour){cout &…

从虚拟机安装CentOS到自定义Dockerfile构建tomcat镜像

写在开头 整个过程中涉及的三方软件均来源于三方的官网&#xff0c;因此需要有一个稳定良好的访问公网网络的环境&#xff0c;可能需要科学上网 下载并安装 VMware Workstation Player 下载 需要先注册登录&#xff1a;https://login.broadcom.com/signin 下载页面&#xff1a…

7-23 还原二叉树

代码&#xff1a; #include<iostream> using namespace std; int n; char a[55],b[55]; int dfs(int l,int r,int x,int y){ // printf("**l%d,r%d,x%d,y%d\n",l,r,x,y);if(l>r) return 0; // if(lr) return 1;int i;for(ix;i<y;i){if(a[l]b[i]) break;…

信息安全工程师(6)网络信息安全现状与问题

一、网络信息安全现状 威胁日益多样化&#xff1a;网络攻击手段不断翻新&#xff0c;从传统的病毒、木马、蠕虫等恶意软件&#xff0c;到勒索软件、钓鱼攻击、DDoS攻击、供应链攻击等&#xff0c;威胁形式多种多样。这些攻击不仅针对个人用户&#xff0c;还广泛影响企业、政府等…

【OJ刷题】双指针问题5

这里是阿川的博客&#xff0c;祝您变得更强 ✨ 个人主页&#xff1a;在线OJ的阿川 &#x1f496;文章专栏&#xff1a;OJ刷题入门到进阶 &#x1f30f;代码仓库&#xff1a; 写在开头 现在您看到的是我的结论或想法&#xff0c;但在这背后凝结了大量的思考、经验和讨论 目录 1…

Mac下nvm无法安装node问题

背景 最近换用mac开发&#xff0c;然后使用nvm&#xff08;版本0.40.1&#xff09;进行node安装的时候出现了一些问题 使用 nvm ls-remote发现只有 iojs 版本 原因可能是nodejs升级了某个协议导致的 解决方案 可以使用 NVM_NODEJS_ORG_MIRRORhttp://nodejs.org/dist nvm ls-re…

关于一道逻辑思维训练题的理解(手表、闹钟、标准时间的骗局)

说有一块手表&#xff0c;比闹钟每时慢30秒&#xff0c;而闹钟比标准时间每时快30秒&#xff0c;那么&#xff0c;这块手表是准时的么 &#xff1f; 这道题就是个带时间刻度的四维骗局 就是个文字游戏 接下来我们来分析一下&#xff0c;为什么说它是个骗局&#xff0c;简直与…