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

news2025/4/5 22:19:53


前言

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

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


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.coloradmin.cn/o/1692012.html

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

相关文章

机器学习模型可视化分析和诊断神器Yellowbrick

大家好&#xff0c;机器学习(ML)作为人工智能的核心&#xff0c;近来得到巨大应用&#xff0c;ML是使计算机能够在无需显式编程的情况下进行学习和预测或决策。ML算法通过学习历史数据模式&#xff0c;来对新的未见数据做出明智的预测或决策。然而&#xff0c;构建和训练ML模型…

ROS2学习——节点话题通信(2)

目录 一、ROS2节点 1.概念 2.实例 &#xff08;1&#xff09;ros2 run &#xff08;2&#xff09;ros2 node list &#xff08;3&#xff09;remapping重映射 &#xff08;4&#xff09;ros2 node info 二、话题 &#xff08;1&#xff09; ros2 topic list &#xf…

C语言内存函数(与上篇字符函数及字符串函数一起食用效果更佳哦~)

顾名思义&#xff0c;内存函数就是针对内存块&#xff08;即一块内存&#xff09;来处理的。 因此本篇所讲的四种内存函数&#xff1a; memcpy&#xff08;内存拷贝&#xff09;memmove&#xff08;内存移动&#xff09;memset&#xff08;内存设置&#xff09;memcmp&#x…

RocketMQ使用(3):消息重复

一、问题说明 发送时消息重复 当一条消息已被成功发送到服务端并完成持久化&#xff0c;此时出现了网络闪断或者客户端宕机&#xff0c;导致服务端对客户端应答失败。如果此时生产者意识到消息发送失败并尝试再次发送消息&#xff0c;消费者后续会收到两条内容相同并且Message…

vue项目elementui刷新页面弹窗问题

bug&#xff1a;每次刷新页面都有这个鬼弹窗。 刚开始以为是自己的代码问题&#xff0c;于是我翻遍了每一行代码&#xff0c;硬是没找出问题。 后来在网上找了些资料&#xff0c;原来是引入的问题。 解决方案&#xff1a; 改一下引入方式即可。 错误姿势 import Vue from …

Autodesk 3ds Max下载,3ds MAX 2024三维建模渲染软件安装包下载安装

3ds MAX中文版&#xff0c;其强大的功能和灵活的操作为广大用户提供了无限的创意空间&#xff0c;使得高质量动画、最新游戏、设计效果等领域的制作需求得以完美满足。 ​ 作为一款三维建模软件&#xff0c;3ds MAX中文版具备极高的建模精度和渲染质量。它支持多种建模方式&am…

Golang项目代码组织架构实践

Golang在项目结构上没有强制性规范&#xff0c;虽然这给了开发者很大的自由度&#xff0c;但也需要自己沉淀一套可行的架构。本文介绍了一种项目布局&#xff0c;可以以此为参考设计适合自己的 Golang 项目组织模式。原文: Golang Project Layout Go 有很多强制的或是约定俗成的…

Python学习---基于TCP协议的网络通信程序案例

TCP简介&#xff1a; ●TCP 面向连接、可靠的、基于字节流的传输控制协议 ●TCP的特点 ○面向连接 ○可靠传输 ■应答机制 ■超时重传 ■错误校验 ■流量管控 ●TCP通信模型 TCP严格区分客户…

2024年5月25日 十二生肖 今日运势

小运播报&#xff1a;2024年5月25日&#xff0c;星期六&#xff0c;农历四月十八 &#xff08;甲辰年己巳月己丑日&#xff09;&#xff0c;法定节假日。 红榜生肖&#xff1a;鸡、鼠、猴 需要注意&#xff1a;马、狗、羊 喜神方位&#xff1a;东北方 财神方位&#xff1a;…

篮球论坛|基于SprinBoot+vue的篮球论坛系统(源码+数据库+文档)

篮球论坛系统 目录 基于SprinBootvue的篮球论坛系统 一、前言 二、系统设计 三、系统功能设计 1系统功能模块 2管理员功能模块 3用户功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff…

抖音运营_打造高流量的抖音账号

目录 一 账号定位 行业定位 用户定位 内容定位 二 账号人设 我是谁? 我的优势 我的差异化 三 创建账号 名字 头像 简介 四 抖音养号 为什么要养号&#xff1f; 抖音快速养号 正确注册抖音账号 一机一卡一号 实名认证 正确填写账号信息 养号期间的操作 五…

OpenWrt 23.05 安装中文语言包 教程 软路由实测 系列三

1 web 登录 #更改阿里云下载源&#xff0c;可参考第一篇文章:OpenWrt U盘安装使用 详细教程 x86/64平台 软路由实测 系列一-CSDN博客

如何网页在线编辑 Office word 文档,并支域功能:创建域/插入域/替换域等

在日常在线办公场景中&#xff0c;我们经常会遇到一些复杂的文档编辑需求&#xff0c;特别是我们经常会遇到一些复杂的数学公式&#xff0c;会用到“域”功能&#xff0c;“域”功能便是一个高级且实用的工具。通过设置域&#xff0c;用户可以实现文档的自动化处理&#xff0c;…

聚观早报 | 华为畅享 70S真机图赏;vivo Y200 GT开售

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 5月25日消息 华为畅享 70S真机图赏 vivo Y200 GT开售 一加13部分细节曝光 马斯克谈AI未来 三星Galaxy Z Fold6将…

轻量级 K8S 环境 安装minikube

文章目录 操作系统DockerDocker CE 镜像源站使用官方安装脚本自动安装 &#xff08;仅适用于公网环境&#xff09;安装校验Docker代理docker permission denied while trying to connect to the Docker daemon socket minikubekubectl工具minikube dashboard参考资料 操作系统 …

[图解]产品经理创新之阿布思考法

0 00:00:00,000 --> 00:00:01,900 那刚才我们讲到了 1 00:00:02,730 --> 00:00:03,746 业务序列图 2 00:00:03,746 --> 00:00:04,560 然后怎么 3 00:00:05,530 --> 00:00:06,963 画现状&#xff0c;怎么改进 4 00:00:06,963 --> 00:00:09,012 然后改进的模式…

简洁实用视频播放器-PotPlayer

一、前言 PotPlayer 是一款简洁实用的视频播放器。 发现的确是良心软件&#xff0c;只有20M 的大小&#xff0c;占内存是同类软件最低的。不要小看它那么小巧简洁&#xff0c;但也很强大的&#xff0c;支持强劲的加速引擎&#xff0c;同时支持3D 视频。 同时支持多种编码和字…

生产物流智能优化系统

对生产调度、物流调度【车辆路径问题、配送中心拣选问题】智能优化算法研究形成系统性程序&#xff0c;逐步开发设计一个智能优化系统【包括&#xff1a;问题说明、实验界面、算法结构和算法程序应用说明】&#xff0c; 当前完成TSP和集送车辆路径的算法程序&#xff0c;程序效…

移动端仪表盘,支持更多组件

05/22 主要更新模块概览 定位函数 快捷筛选 轨迹图表 时间组件 01 表单管理 1.1 【表单组件】- 表单关联新增支持自定义按钮样式 说明&#xff1a; 表单关联-关联数据按钮&#xff0c;原仅支持默认按钮样式&#xff0c;现增加关联数据按钮自定义功能&#xff0c;满…

生活小区火灾预警新篇章:泵吸式可燃气体报警器的检定与运用

在现代化的生活小区中&#xff0c;燃气设备广泛应用于居民的日常生活之中&#xff0c;但同时也带来了潜在的火灾风险。 可燃气体报警器作为一种安全监测设备&#xff0c;能够及时检测到燃气泄漏等安全隐患&#xff0c;并在达到预设的阈值时发出警报&#xff0c;提醒居民采取相…