【数据结构】神奇的二叉树


前言

在我第一次听到二叉树这个词的时候,脑海中就想起来下面的这个名场面,汤姆的“裤裆劈树”🤣🤣🤣

在这里插入图片描述
开个玩笑,在我们编程世界中,二叉树是一种特殊的 “树”,而要认识二叉树,我们有得先认识 “树” 是什么玩意


1. 树形结构

1.1 什么是树

树跟我们前面学到的数据结构都不一样,它为一种非线性的数据结构,是由 n(n>=0)个有限节点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树(根朝上,而叶朝下)

在这里插入图片描述


  • 根节点:没有前驱节点
  • 除了根节点,其余节点都可以被分成互不相交的子集合,而每个子集合又是一棵和树相似的子树。每棵子树的根节点都只有一个前驱节点,可以有零个或多个后继节点
  • 树是通过递归定义的

在这里插入图片描述

要注意:如果子树之间有交集,那就不能算是树了

1.2 名词概念

在这里插入图片描述

  • 节点(node):包含一个数据元素以及若干指向子树分支的信息
  • 节点的度:一个节点含有的子树的个数。上图中:C的度为2
  • 树的度:在一棵树中,最大的节点的度即为树的节点。上图中:树的度为2
  • 叶子节点:也叫做终端节点,度为零的节点。
  • 分支节点:也叫做非终端节点,度不为零的节点
  • 父节点:也叫做双亲节点。上图中:A为B的父节点
  • 子节点:也叫做孩子节点。上图中:D为B的子节点
  • 根节点:在一棵树中,没有双亲节点的节点
  • 节点的层次:根节点算作第1层,依次往下为2层、3层……
  • 树的高度或深度:树中节点的最大层次。(深度是相对节点位置的,而深度的最大值就等于树的高度)上图中:树的高度为4
  • 兄弟节点:具有相同父节点的节点互称为兄弟节点。上图中:B和C是兄弟节点
  • 堂兄弟节点:双亲在同一层的节点互为堂兄弟节点;上图中:D和E是堂兄弟节点
  • 节点的祖先:从根到该节点所经分支上的所有节点。上图中:A是所有节点的祖先

1.3 树的表现形式

之前我们在学习线性表的时候,是定义了个节点Node类,类内部定义了 val 值和 next 指向下一个节点的地址。在二叉树这里,我们也是类似的表示形式。树可以用孩子表示法、孩子双亲表示法、孩子兄弟表示法等等,我们在这里就用孩子表示法

	class TreeNode {public char val; //节点中存储的数据public TreeNode left; //左孩子public TreeNode right; //右孩子
}

在这里插入图片描述


2. 二叉树

2.1 概念

一棵二叉树是一个有限的节点集合,该集合为:

  • 要么为空
  • 要么是有一个根节点加上左右两棵称为左子树右子树的二叉树组成
    在这里插入图片描述

上图就是典型的二叉树,我们可以看出:

  • 二叉树不存在度大于2的节点
  • 二叉树是一棵有序树,它的子树有左右之分,次序不能颠倒
    在这里插入图片描述

二叉树的五种基本形态

在这里插入图片描述

三种特殊的形态

在这里插入图片描述

接下来我们将重点讲解满二叉树完全二叉树


2.2 两种特殊的二叉树

满二叉树:如果每层的节点数都达到最大值,则这棵二叉树就是满二叉树。即如果二叉树的节点总数为 2*K - 1(K为树的层数),那它就是满二叉树

完全二叉树:除了最后一层,所有层的节点都被完全填满,而且最后一层的节点尽可能地集中在左侧。这样的二叉树就是完全二叉树(我们也可以这样理解:所有的叶子节点都在最后一层或者倒数第二层,且最后一层叶子节点在左边连续,倒数第二层在右边连续)

要注意:满二叉树就是一种特殊的完全二叉树


2.3 二叉树的性质

  1. 假设根节点的层数为第1层,那么一棵非空二叉树第i层上最多有 2 i − 1 2^{i-1} 2i1(i>0)个节点
  • 证明:因为二叉树的度最大为2,假设每一层节点的度都为2。那么第1层就有1个节点,第2层就有2个节点,第3层就有4个节点,根据等比数列的规律,我们可以算出第i层上最多有 2 i − 1 2^{i-1} 2i1(i>0)个节点
  1. 假设根节点的深度为1,那么深度为K的二叉树最大节点数为 2 K − 1 2^K-1 2K1(K>=0)
  • 证明:假设每一层节点的度都为2。那么第1层就有1个节点,第2层就有2个节点,第3层就有4个节点……根据等比数列的求和公式可以得到深度为K的二叉树最大节点数为 2 K − 1 2^K-1 2K1(K>=0)
  1. 对于任何一棵二叉树, n 0 n_0 n0表示叶子节点数,用 n 2 n_2 n2来表示度为2的节点数,则有 n 0 n_0 n0 = n 2 n_2 n2 + 1
  • 证明: n 1 n_1 n1表示度为1的节点数,又因为一棵N个节点的数有N-1条边,所以我们可以等出两条等式①N = n 0 n_0 n0 + n 1 n_1 n1 + n 2 n_2 n2 ②N = n 0 n_0 n0*0 + n 1 n_1 n1*1 + n 2 n_2 n2*2; 联立可得 n 0 n_0 n0 = n 2 n_2 n2 + 1
  1. 假设根节点的层数为第1层,那么有n个节点完全二叉树的深度为 log ⁡ 2 ( n + 1 ) \log_2{(n+1)} log2(n+1) 的向上取整
    证明:因为深度为K的满二叉树的节点数n一定小于等于 2 K − 1 2^K-1 2K1(用性质2可得),那么倒推可以等到 K = log ⁡ 2 ( n + 1 ) \log_2{(n+1)} log2(n+1) ,向上取整指的是
  2. 对于完全二叉树,如果我们从上到下、从左往右编号,则编号为 i 的节点,则其左孩子编号就为 2i,右孩子编号就为 2i+1;其双亲节点编号为 i/2(i = 1时为根节点,无双亲节点)

3. 二叉树的存储结构

二叉树有两种存储结构:顺序存储和链式存储

3.1 顺序存储

二叉树的顺序存储结构跟线性表十分相似,就是使用一维数组来存储二叉树中的节点,而数组的下标表示的就是该节点的存储位置:

在这里插入图片描述

该树各节点在数组中的形式:(为表示方便,此处起始点记为1)

在这里插入图片描述

我们上面展示的树为完全二叉树,它刚好可以填满整个数组,不会造成存储空间的浪费

而当二叉树不是完全二叉树时:(D、F表示不存在的节点)

在这里插入图片描述

其存储结构如下,^ 表示该位置没有节点,我们可以发现,此时浪费了两个存储空间

在这里插入图片描述

由此我们可以得到一个结论:顺序存储结构适用于完全二叉树,非完全二叉树使用顺序存储则会造成浪费存储空间。因此对于二叉树,我们更习惯于使用链式存储结构


3.2 链式存储

我们可以将节点设计成两个域:数据域和指针域,数据域用来存放具体数据,指针域则存放父节点或者子节点的地址。下面我们使用孩子表示法进行演示

在这里插入图片描述

通过一个一个的节点引用起来就是链式存储,常见的表示方式为二叉链表,如图

在这里插入图片描述


4. 二叉树的遍历

二叉树的遍历指的是从根节点出发,按照某种约定依次对树中的每个节点仅作一次访问。遍历是二叉树上最重要的操作之一,是二叉树上进行其他运算的基础。二叉树一共有四种遍历方式:

  • 前序遍历(NLR):又称为先序遍历,先访问根节点 → \rightarrow 根的左子树 → \rightarrow 根的右子树
  • 中序遍历(LNR):先访问根的左子树 → \rightarrow 根节点 → \rightarrow 根的右子树
  • 后序遍历(LRN):先访问根的左子树 → \rightarrow 根的右子树 → \rightarrow 根节点
  • 层序遍历:从根节点从上往下逐层遍历,在同一层时,按从左到右的顺序对节点逐个访问。与上面提到的顺序存储结构相类似

N:Node(根节点) L:Left(左子树) R:Right(右子树)


4.1 前序遍历

规则:首先访问根节点,然后递归地进行左子树的前序遍历,最后递归地进行右子树的前序遍历

此处要重点理解递归的含义:我们知道,二叉树是递归定义的,即每棵子树都可以看成是一棵二叉树。所以在遍历的时候,需要不断对新的子树严格按照前序遍历的规则来执行

具体的遍历步骤如下:

  1. 访问根节点:首先访问当前节点,也就是根节点
  2. 遍历左子树:然后,对根节点的左子节点进行前序遍历。如果左子节点存在,重复上述步骤,即先访问左子节点,然后递归地遍历其左子树,接着遍历其右子树
  3. 遍历右子树:最后,对根节点的右子节点进行前序遍历。同样,如果右子节点存在,重复上述步骤

在这里插入图片描述

由此我们可以得到前序遍历的结果为:

A B D H E I C F J G


4.2 中序遍历

首先递归地进行左子树的中序遍历,然后访问根节点,最后递归地进行右子树的中序遍历

具体的遍历步骤如下:

  1. 遍历左子树:首先,对根节点的左子节点进行中序遍历。如果左子树存在,那么按照同样的规则,先遍历左子树的左子树,然后访问左子树的根节点,最后遍历左子树的右子树
  2. 访问根节点:在左子树的遍历完成后,访问当前节点,也就是根节点
  3. 遍历右子树:最后,对根节点的右子节点进行中序遍历。如果右子树存在,重复上述步骤,即先遍历右子树的左子树,然后访问右子树的根节点,最后遍历右子树的右子树

在这里插入图片描述

由此我们可以得到中序遍历的结果为:

H D B I E A F J C G


4.3 后序遍历

首先递归地进行左子树的后序遍历,然后递归地进行右子树的后序遍历,最后访问根节点

具体的遍历步骤如下:

  1. 遍历左子树:首先,对根节点的左子节点进行后序遍历。如果左子树存在,那么按照同样的规则,先遍历左子树的左子树,然后遍历左子树的右子树,最后访问左子树的根节点
  2. 遍历右子树:在左子树的遍历完成后,对根节点的右子节点进行后序遍历。如果右子树存在,重复上述步骤,即先遍历右子树的左子树,然后遍历右子树的右子树,最后访问右子树的根节点
  3. 访问根节点:在左子树和右子树的遍历都完成后,访问当前节点,也就是根节点

在这里插入图片描述

由此我们可以得到后序遍历的结果为:

H D I E B J F C G A


4.4 层序遍历

从根节点从上往下逐层遍历,在同一层,按从左到右的顺序对节点逐个访问

在这里插入图片描述

层序遍历的结果为:

A B C D E F G H I J


通过上面的例子我们也知道了二叉树的遍历是怎么一回事。实际上,我们也可以根据遍历的结果来创建出一棵二叉树:

前序遍历 + 中序遍历、 后序遍历 + 中序遍历

前序遍历的第一个节点就是根节点;后序遍历的最后一个节点也是根节点。在知道根节点后,根据中序遍历的我们就能得知左子树和右子树,最后根据递归的规律我们就能够反推出一棵二叉树

但是如果只知道前序遍历和后序遍历则是无法反推出一棵二叉树


5. 遍历的代码实现

5.1 递归实现

因为二叉树是由递归定义的,所以我们最常使用递归来实现二叉树的遍历

首先,我们要先定义好节点:(此处使用孩子表示法)

	class TreeNode {public char val; //节点中存储的数据public TreeNode left; //左孩子public TreeNode right; //右孩子public TreeNode(char val) {this.val = val;}}

前序遍历:

    //前序遍历(根左右)public void preOrder(TreeNode root) {if (root == null) {return;}System.out.print(root.val + " ");//根preOrder(root.left);//左preOrder(root.right);//右}

中序遍历:

    //中序遍历(左根右)public void inOrder(TreeNode root) {if (root == null) {return;}inOrder(root.left);//左System.out.print(root.val + " ");//根inOrder(root.right);//右}

后序遍历:

    //后序遍历(左右根)public void postOrder(TreeNode root) {if (root == null) {return;}postOrder(root.left);//左postOrder(root.right);//右System.out.print(root.val + " ");//根}

层序遍历:(此处我们需要借助队列)

从根节点从上往下逐层遍历,在同一层,按从左到右的顺序对节点逐个访问

    //层序遍历public void levelOrder(TreeNode root) {Queue<TreeNode> queue = new LinkedList<>();if (root == null) {return;}queue.offer(root);while (!queue.isEmpty()) {TreeNode cur = queue.poll();System.out.print(cur.val + " ");if (cur.left != null) {queue.offer(cur.left);}if (cur.right != null) {queue.offer(cur.right);}}System.out.println();}

设计思路:队列有先进先出的特性。

  1. 首先我们得判断根节点是否为 null,为 null 就直接返回,说明是一棵空树,不为 null 就入队
  2. 接着就是以队列是否为空来作为 while 循环条件,不为空就一直循环。循环内部我们让根节点出队,创建一个 cur 来接收根节点,接下来打印 cur 的值
  3. 然后就判断 cur 的左右是否为null(一定要先左再右),不为 null 就入队。接着继续循环上面操作,cur 接收出队的节点,打印 cur 的值
  4. 最后队列为空,循环停止,层序遍历完成

5.2 非递归实现(了解)

非递归实现遍历需要借助栈,它有先进后出的特性

    //前序遍历(非递归)public void preOrderNot(TreeNode root) {Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;while (cur != null || !stack.empty()) {while (cur != null) {stack.push(cur);System.out.print(cur.val + " ");cur = cur.left;}TreeNode top = stack.pop();cur = top.right;}}//中序遍历(非递归)public void inOrderNot(TreeNode root) {Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;while (cur != null || !stack.empty()) {while (cur != null) {stack.push(cur);cur = cur.left;}TreeNode top = stack.pop();System.out.print(top.val + " ");cur = top.right;}}//后序遍历(非递归)public void postOrderNot(TreeNode root) {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;}TreeNode top = stack.peek();if (top.right == null || top.right == prev ) {stack.pop();System.out.print(top.val+" ");prev = top;} else {cur = top.right;}}}

通过对比代码数量我们也可以看出非递归实现二叉树的遍历十分麻烦,因此该方法了解即可


结语

二叉树的相关知识十分重要,关于四种遍历的递归思路一定要熟记。下一篇博客我会详细介绍二叉树的经典题型,如 ”相同的二叉树‘ “翻转二叉树”……掌握了这些经典题型能让我们更加深刻的认识二叉树
希望大家能喜欢这篇文章,有总结不到位的地方还请多多谅解,若有出现纰漏,希望大佬们看到错误之后能够在私信或评论区指正,博主会及时改正,共同进步!

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

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

相关文章

PTT票据传递攻击

一. PTT票据传递攻击原理 1.PTT介绍 PTT(Pass The Ticket)&#xff0c;中文叫票据传递攻击&#xff0c;PTT 攻击只能用于kerberos认证中,NTLM认证中没有&#xff0c; PTT是通过票据进行认证的。 进行票据传递&#xff0c;不需要提权&#xff0c;域用户或者system用户就可以 …

深入探讨Java泛型:详解Java泛型的工作原理,擦除,包括通配符以及在编程实践中的使用和注意事项

1. Java泛型简介 1. 泛型的主要目的 泛型主要有两个目的: 提高类型安全:在编译时进行严格的类型检查。有了泛型,可以确保在编译时而不是在运行时发现类型问题,从而降低了类类型转换错误的可能性。 消除代码冗余:避免在代码中使用过多的类型转换,提高代码的可读性和可…

HTTP 和高级编程

目录 一.HTTP 简介 HTTP 工作原理 HTTP 请求 HTTP 响应 HTTP 的常见应用 HTTP 的优点 HTTP 的缺点 二.使用 CHtmlView 类创建 Web 视图 创建 CHtmlView 控件 CHtmlView 类成员函数 CHtmlView 类事件 使用 CHtmlView 类创建 Web 浏览器 三.Web 浏览器应用程序示例 一…

【图书推荐】《Vue.js 3.x+Element Plus从入门到精通(视频教学版)》

本书用处 内容简介 本书通过对Vue.js&#xff08;简称Vue&#xff09;的示例和综合案例的介绍与演练&#xff0c;使读者快速掌握Vue.js 3.x框架的用法&#xff0c;提高Web前端的实战开发能力。本书配套示例源码、PPT课件、教学大纲、教案、同步教学视频、习题及答案、其他资源…

HTML+CSS 响应式导航栏

效果演示 Code <div class="navbar"><input type="checkbox"><span></span><span></span><ul><li><a href="#">点赞</a></li><li><a href="#">关注&…

模拟器玩游戏

IOS系统 【PPSSPP与Retroarch 模拟器上架苹果商店&#xff0c;教你如何玩转复古游戏模拟器】 安卓

SpringCloud+Python 混合微服务,如何打造AI分布式业务应用的技术底层?

尼恩&#xff1a;LLM大模型学习圣经PDF的起源 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;经常性的指导小伙伴们改造简历。 经过尼恩的改造之后&#xff0c;很多小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试机会&#x…

【数据结构】数据结构中的隐藏玩法——栈与队列

前言&#xff1a; 哈喽大家好&#xff0c;我是野生的编程萌新&#xff0c;首先感谢大家的观看。数据结构的学习者大多有这样的想法&#xff1a;数据结构很重要&#xff0c;一定要学好&#xff0c;但数据结构比较抽象&#xff0c;有些算法理解起来很困难&#xff0c;学的很累。我…

依赖管理与Springboot Starters

依赖管理与Spring Boot Starters 在现代软件开发过程中&#xff0c;依赖管理是一项至关重要的任务。随着应用程序规模的扩大&#xff0c;引入的依赖项数量也会增加&#xff0c;使得版本冲突和类路径问题成为常见的挑战。为了应对这些挑战&#xff0c;Spring Boot提供了一套简化…

【OpenCV 基础知识 11】计算通道像素值和

cvSplit()函数将复制src的各个通道到图像dst0&#xff0c;dst1&#xff0c;dst2和dst3中。如果源图像少于4个通道的情况下&#xff0c;那么传递给cvSplit()的不必要的目标参数可设置为NULL program cv_Sum;{$APPTYPE CONSOLE} {$R *.res}usesSystem.SysUtils,ocv.highgui_c,oc…

前端基于word模板导出word文档

项目环境 vue2 js vue-cli等 依赖包都可以在npm官网找到对应文档 npm官网(英文) 1、依赖 安装依赖 docxtemplater npm i docxtemplaterfile-saver npm i file-saverjszip-utils npm i jszip-utilsjszip npm i jszip在对应页面或模块中引入依赖 import Docxtemplater …

QQ个性网空间日志网站模板源码

QQ个性网空间日志网站模板源码自带后台登录设置&#xff0c;适用于博客、文章、资讯、其他类网站内容使用。模板自带eyoucms内核&#xff0c;原创设计、手工书写DIVCSS&#xff0c;完美兼容IE7、Firefox、Chrome、360浏览器等;主流浏览器;结构容易优化;多终端均可正常预览。由于…

力扣刷题---2206. 将数组划分成相等数对【简单】

题目描述&#x1f357; 给你一个整数数组 nums &#xff0c;它包含 2 * n 个整数。 你需要将 nums 划分成 n 个数对&#xff0c;满足&#xff1a; 每个元素 只属于一个 数对。 同一数对中的元素 相等 。 如果可以将 nums 划分成 n 个数对&#xff0c;请你返回 true &#xf…

react 低代码平台方案汇总

React作为当前最流行的前端框架之一&#xff0c;其生态系统中孕育了多种低代码平台方案&#xff0c;旨在加速应用开发过程。以下是几款基于React的低代码平台或工具&#xff0c;它们通过可视化构建、预制组件、数据绑定等功能&#xff0c;帮助开发者快速构建应用程序&#xff1…

【Tools】SpringBoot工程下的全局异常处理

Catalog SpringBoot工程下的全局异常处理一、需求二、实现 SpringBoot工程下的全局异常处理 一、需求 程序运行过程中没法保证一定不会出错&#xff0c;即程序中会报异常&#xff0c;而系统默认的异常信息的格式往往和我们自定义返回的消息格式不一致&#xff0c;还会影响返回…

力扣刷题---3146. 两个字符串的排列差

题目描述 给你两个字符串 s 和 t&#xff0c;每个字符串中的字符都不重复&#xff0c;且 t 是 s 的一个排列。 排列差 定义为 s 和 t 中每个字符在两个字符串中位置的绝对差值之和。 返回 s 和 t 之间的 排列差 。 示例 1&#xff1a; 输入&#xff1a;s “abc”, t “b…

AcWing 154. 滑动窗口——算法基础课题解

AcWing 154. 滑动窗口 题目描述 给定一个大小为 n≤10^6 的数组。 有一个大小为 &#x1d458; 的滑动窗口&#xff0c;它从数组的最左边移动到最右边。 你只能在窗口中看到 &#x1d458; 个数字。 每次滑动窗口向右移动一个位置。 以下是一个例子&#xff1a; 该数组为…

运用HTML、CSS设计Web网页——“西式甜品网”图例及代码

目录 一、效果展示图 二、设计分析 1.整体效果分析 2.头部header模块效果分析 3.导航及banner模块效果分析 4.分类classify模块效果分析 5.产品展示show模块效果分析 6.版权banquan模块效果分析 三、HTML、CSS代码分模块展示 1. 头部header模块代码 2.导航及bann…

【SpringCloud】Spring Cloud基本介绍

目录 回顾架构分类单体架构分布式架构微服务架构什么是微服务优点缺点微服务的架构特征&#xff1a;微服务架构面临的挑战技术挑战微服架构的设计原则微服务概念提供者(Provider)消费者(Consumer)RPC和Restful集群分布式 总结 服务拆分和远程调用服务拆分原则服务拆分示例 思考…

docker和docker-compose安装

1.安装docker 安装所需的软件包 sudo yum install -y yum-utils device-mapper-persistent-data lvm2#设置阿里源 sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo更新软件包索引 sudo yum makecache fast查询可安装…