树和二叉树

线性表、栈和队列都是线性的数据结构,这种数据结构之内的元素只存在一个对一个的关系,存储、处理起来相对比较简单。而树是一种更复杂的数据结构,这种结构内的元素存在一个对多个的关系,例如一个父节点可以包含多个子节点。
树也是一种非常常用的数据结构,尤其是二叉树的应用,哈夫曼树及哈夫曼编码就是二叉树的重要用途,排序二叉树、平衡二叉树、红黑树在实际编程中都有极为广泛的用途。例如,Java 集合框架的TreeMap本质上就是红黑树的实现。


树的概述

树也是一种非常常用的数据结构,树与线性表、栈、队列等线性结构不同:线性表、栈、队列都是一种线性结构,而树是一种非线性结构。树中任一节点可以有0或多个子节点,但只能有一个父节点。根节点是一个特例,根节点没有父节点,叶子节点没有子节点。树中每个节点既可以是其上一级节点的子节点,也可以是下一级节点的父节点,因此同一个节点既可以是父节点,也可以是子节点(类似于一个人——他既是他儿子的父亲,又是他父亲的儿子)。很显然,父子关系是一种非线性关系,所以树结构是非线性结构。

如果按节点是否包含子节点来分,节点可分成以下两种:
➢普通节点:包含子节点的节点。
➢叶子节点:没有子节点的节点,因此叶子节点不可作为父节点。
如果按节点是否具有唯一的父节点来分,节点又可分为如下两种:
➢根节点:没有父节点的节点,根节点不可作为子节点。
➢普通节点:具有唯一父节点的节点。

一棵树只能有一个根节点,如果一棵树有了多个根节点,那么它已经不再是一棵树了,而是多棵树的集合,有时也被称为森林。
在这里插入图片描述

名称解释
节点树的最基本组成单元,通常包括一个数据元素及若干指针用于指向其他节点。
节点的度节点拥有的子树的个数被称为节点的度(degree)。
树的度树中所有节点的度的最大值就是该树的度。
叶子节点度为0的节点被称为叶子节点或终端节点。
分支节点度不为0的节点被称分支节点或非终端节点。
子节点、父节点、兄弟节点节点的子树的根被称为该节点的子节点,而该节点称为子节点的父节点(parent)。 具有相同父节点的子节点之间互称为兄弟节点(sibling)。
节点的层次(level)节点的层次从根开始算起,根的层次值为1,其余节点的层次值为父节点层次值加1。
树的深度(depth)树中节点的最大层次值称为树的深度或高度。
有序树与无序树如果将树中节点的各棵子树看成从左到右是有序的(即不能互换),则称该树为有序树,否则称为无序树。
祖先节点(ancestor)从根到该节点所经分支上的所有节点。
后代节点(descendant)以某节点为根的子树中任一节点都称为该节点的后代节点。
森林(forest)森林是两棵或两棵以上互不相交的树的集合,删去一棵树的根,就得到一个森林。

树的基本操作

如果需要实现一棵树, 程序不仅要以合适的方式保存该树的所有节点,还要记录节点与节点之间的父子关系。
➢初始化:通常是一个构造器,用于创建一棵空树, 或者以指定节点为根来创建树。
➢为指定节点添加子节点。
➢判断树是否为空。
➢返回根节点。
➢返回指定节点(非根节点)的父节点。
➢返回指定节点(非叶子节点)的所有子节点。
➢返回指定节点(非叶子节点)的第i个子节点。
➢返回该树的深度。
➢返回指定节点的位置。
为了实现树这种数据结构,有两种记录节点与节点之间的父子关系的方法:
➢父节点表示法:每个子节点都记录它的父节点。
➢子节点链表示法:每个非叶子节点通过一个链表来记录它所有的子节点。

父节点表示法

树中除根节点之外的每个节点都有一个父节点。为了记录树中节点与节点之间的父子关系,可以为每个节点增加一个parent域,用以记录该节点的父节点。只要用一个节点数组来保存树里的每个节点,并让每个节点记录其父节点在数组中的索引即可。
在这里插入图片描述
在这里插入图片描述
父节点表示法的特点是每个节点都可以快速找到它的父节点。但如果要找某个节点的所有子节点就比较麻烦,程序要遍历整个节点数组。


子节点链表示法

父节点表示法的思想是让每个节点"记住"它的父节点的索引,父节点表示法是从子节点着手的;反过来,还有另外一种方式:让父节点"记住"它的所有子节点。在这种方式下,由于每个父节点需要记住多个子节点,因此必须采用"子节点链"表示法。
在这里插入图片描述
采用子节点链表示法来记录树时,需要为每个节点维护一个子节点链,通过该子节点链来记录该节点的所有子节点。子节点链表示法的特点是:每个节点都可以快速找到它的所有子节点。但如果要找某个节点的父节点则比较麻烦,程序要遍历整个节点数组。


二叉树

如果对普通树增加一些限制,让一棵树中每个节点最多只能包含两个子节点,而且严格区分左子节点、右子节点(左、右子节点的位置不能交换),这棵树就变成了二叉树。

二叉树的定义和基本概念

二叉树指的是每个节点最多只能有两个子树的有序树。通常左边的子树被称作"左子树"(left subtree),右边的子树被称作"右子树"(right subtree)。由此可见,二叉树依然是树,它是一种特殊的树。二叉树的每个节点最多只有两棵子树(不存在度大于2的节点),二叉树的子树有左、右之分,次序不能颠倒。

树和二叉树的区别:
➢树中节点的最大度数没有限制,而二叉树节点的最大度数为2,也就是说,二叉树是
节点的最大度数为2的树。
➢无序树的节点无左、右之分,而二叉树的节点有左、右之分,也就是说,二叉树是有
序树。

一棵深度为k的二叉树,如果它包含了2k - 1个节点,就把这棵二叉树称为满二叉树。满二叉树的特点是,每一层上的节点数都是最大节点数,即各层节点数分别为1,2,4,8,16, …,2k-1
在这里插入图片描述


一棵有n个节点的二叉树,按满二叉树的编号方式对它进行编号,若树中所有节点和满二叉树1~n编号完全一致,则称该树为完全二叉树。也就是说,如果一棵二叉树除最后一层外,其余层的所有节点都是满的,并且最后一层或者是满的,或者仅在右边缺少若干连续的节点,则此二叉树就是完全二叉树。
在这里插入图片描述
满二叉树是一种特殊的完全二叉树。当完全二叉树最后一层的所有节点都是满的时,这棵完全二叉树就变成了满二叉树。

二叉树大致的性质:
➢二叉树第i层上的节点数目至多为2 i-1 (i≥1)。
➢深度为k的二叉树至多有2 k - 1个节点。满二叉树的每层节点的数量依次为1, 2, 4,8,…,因此深度为k的满二叉树包含的节点数为公比为2的等比数列的前k项总和,即2 k- 1。
➢在任何一棵二叉树中,如果其叶子节点的数量为n 0, 度为2的子节点数量为n 2,则n 0=n 2+ 1。这是因为:如果为任意叶子节点增加一一个子节点,则原有叶子节点变成非叶子节点,新增节点变成叶子节点,上述等式不变;如果为任意叶子节点增加两个子节点,则原有叶子节点变成度为2的非叶子节点,新增的两个节点变成叶子节点,上述等式依然不变。
➢具有n个节点的完全二叉树的深度为log 2 n+1
对于一棵具有n个节点的完全二叉树的节点按层自左向右编号,则对任一编号为i (n≥i≥1)的节点有下列性质:
➢当i=1时,节点i是二叉树的根;若i>1,则节点的父节点是i/2。
➢若2i≤n,则节点i有左孩子,左孩子的编号是2i;否则,节点无左孩子,并且是叶子节点。
➢若2i+1≤n,则节点i有右孩子,右孩子的编号是2i+ 1;否则,节点无右孩子。
➢1~n/2范围的节点都是有孩子节点的非叶子节点,其余的节点全部都是叶子节点。编号为n/2的节点可能只有左子节点,也可能既有左子节点,又有右子节点。

二叉树的基本操作

二叉树记录其节点之间的父子关系更加简单,因为二叉树中的每个节点最多只能保存两个子节点。
➢初始化:通常是一个构造器,用于创建一棵空树,或者以指定节点为根来创建二叉树。
➢为指定节点添加子节点。
➢判断二叉树是否为空。
➢返回根节点。
➢返回指定节点(非根节点)的父节点。
➢返回指定节点(非叶子节点)的左子节点。
➢返回指定节点(非叶子节点)的右子节点。
➢返回该二叉树的深度。
➢返回指定节点的位置。
要实现二叉树这种数据结构,有以下三种选择:
➢顺序存储:采用数组来记录二叉树的所有节点。
➢二叉链表存储:每个节点保留一个left、right域,分别指向其左、右子节点。
➢三叉链表存储:每个节点保留一个left、right、parent域,分别指向其左、右子节点和父节点。

二叉树的顺序存储

顺序存储指的是充分利用满二叉树的特性:每层的节点数分别为1,2,4,8,… ,2i-1,一棵深度为i的二叉树最多只能包含2i -1个节点,因此只要定义一个长度为2i -1的数组即可存储这棵二叉树。对于普通二叉树(不是满二叉树),那些空出来的节点对应的数组元素留空就可以了。由此可见,二叉树采用顺序存储会造成一定的空间浪费。
在这里插入图片描述
在这里插入图片描述
当使用数组来存储二叉树的所有节点时可能会产生一定的空间浪费:如果该二叉树是完全二叉树,就不会有任何空间浪费了;但如果该二叉树的所有节点都只有右子节点,那么就会产生相当大的空间浪费。


二叉树的二叉链表存储

二叉链表存储的思想是让每个节点都能"记住"它的左、右两个子节点。为每个节点增加left、right 两个指针,分别引用该节点的左、右两个子节点。
在这里插入图片描述
二叉链表存储的二叉树的节点大致有如下定义:

class Node{
	Object data;
	Node left;
	Node right;
}

对于这种二叉链表的二叉树,因为程序采用链表来记录树中的所有节点,所以添加节点没有限制,而且不会像顺序存储那样产生大量的空间浪费。这种二叉链表的存储方式在遍历树节点时效率不高,指定节点访问其父节点时也比较困难,程序必须采用遍历二叉树的方式来搜索其父节点。为了克服二叉链表存储方式中访问父节点不方便的问题,可以将二叉链表扩展成三叉链表。


二叉树的三叉链表存储

三叉链表存储的思想是让每个节点不仅"记住"它的左、右两个子节点,还要"记住"它的父节点,因此需要为每个节点增加left、right 和parent三个指针,分别引用该节点的左、右两个子节点和父节点。
在这里插入图片描述
二叉链表存储和三叉链表存储都是根据该二叉树的节点特征来划分的。对于二叉链表存储而言,二叉树的每个节点需要两个“分叉”,分别记录该节点的左、右两个子节点;对于三叉链表存储而言,二叉树的每个节点需要三个“分叉”,分别记录该节点的左、右两个子节点和父节点。
三叉链表存储的二叉树的节点大致有如下定义:

class Node{
	Object data;
	Node left;
	Node right;
	Node parent;
}

三叉链表存储方式是对二叉链表的一种改进,通过为树节点增加一个parent引用,可以让每个节点都能非常方便地访问其父节点。三叉链表存储的二叉树既可方便地向下访问节点,也可方便地向上访问节点。


遍历二叉树

遍历二叉树指的是按某种规律依次访问二叉树的每个节点,对二叉树的遍历过程就是将非线性结构的二叉树中的节点排列成线性序列的过程。
如果采用顺序结构来保存二叉树,程序遍历二叉树将非常容易,无须进行任何思考,直接遍历底层数组即可。

如果采用链表来保存二叉树的节点,则有以下两种遍历方式:
深度优先遍历:这种遍历算法将先访问到树中最深层次的节点。
广度优先遍历:这种遍历算法将逐层访问每层的节点,先访问根(第一层)节点,然后访问第二层的节点…依此类推。因此,广度优先遍历方法又被称为按层遍历。
对于深度优先遍历算法而言,它又可分为以下三种:
➢先(前)序遍历二叉树。
➢中序遍历二叉树。
➢后序遍历二叉树。
如果L、D、R表示左子树、根、右子树,习惯上总是必须先遍历左子树,后遍历右子树,根据遍历根节点的顺序不同,上面三种算法可表示如下:
➢DLR:先序遍历。
➢LDR:中序遍历。
➢LRD:后序遍历。

深度遍历的先序遍历、中序遍历、后序遍历这三种遍历万式的名称都是针对根节点(D)而言的。先处理根节点(D)时就称为先序遍历,其次处理根节点(D)时就称为中序遍历;最后处理根节点(D)时就称为后序遍历
。因为二叉树的定义本身就有"递归性",所以深度优先遍历时能非常方便地利用递归来遍历每个节点:一棵非空二叉树由树根、左子树和右子树组成,依次遍历这三部分,就可以遍历整棵二叉树。

先序遍历

先序遍历指先处理根节点,其处理顺序如下:
(1)访问根节点。
(2)递归遍历左子树。
(3)递归遍历右子树。

中序遍历

中序遍历指其次处理根节点,其处理顺序如下:
(1)递归遍历左子树。
(2)访问根节点。
(3)递归遍历右子树。

后序遍历

后序遍历指最后处理根节点,其处理顺序如下:
(1)递归遍历左子树。
(2)递归遍历右子树。
(3)访问根节点。

广度优先遍历

广度优先遍历又称为按层遍历,整个遍历算法是先遍历二叉树的第一层(根节点),再遍历根节点的两个子节点(第二层) … 依此类推,逐层遍历二叉树的所有节点。为了实现广度优先遍历,可以借助于具有FIFO特征的队列来实现,如下所示:

  1. 建一个队列(先进先出),把树的根节点压入队列。
  2. 从队列中弹出一一个节点(第一次弹出的就是根节点),然后把该节点的左、右节点压入队列,如果没有子节点,则说明已经到达叶子节点了。
  3. 用循环重复执行第2步,直到队列为空。当队列为空时,说明所有的叶子节点(深度最深的层)都已经经过了队列,也就完成了遍历。

为了实现二叉树的广度遍历,可借助一个Queue对象,通过这个队列接口,可以非常方便地对二叉树实现广度优先遍历。


森林、树和二叉树的转换

有序树、森林和二叉树之间有一一映射的关系,可以进行相互转换。

多叉树向二叉树转换的方法如下:
(1)加虚线:同一个父节点的相邻兄弟节点之间加虚线。
(2)抹实线:每个节点只保留它与最左子节点的连线,与其他子节点的连线都被抹掉。
(3)虚改实:虚线改为实线。
在这里插入图片描述
右图中的虚线就是新增的"父子"关系。多叉树转换为二叉树的方法的关键思想就是:所有子节点只保留左子节点,其他子节点转为左子节点的右子节点链。
森林也可转换为二叉树——只要把森林当成一棵根节点被删除的多叉树即可。

排序二叉树

排序二叉树是一种特殊结构的二叉树,通过它可以非常方便地对树中的所有节点进行排序和检索。

排序二叉树要么是一棵空二叉树,要么是具有下列性质的二叉树:
➢若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值。
➢若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值。
➢它的左、右子树也分别为排序二叉树。
在这里插入图片描述

对于排序二叉树,若按中序遍历就可以得到由小到大的有序序列:
{2,3,4,8,9,9,10,13,18,21}
采用广度优先法则来遍历排序二叉树得到的不是有序序列,采用中序遍历来遍历排序二叉树时才可以得到有序序列。


红黑树

排序二叉树虽然可以快速检索,但在最坏的情况下,如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成链表:所有节点只有左节点(如果插入节点集合本身是由大到小排列的),或者所有节点只有右节点(如果插入节点集合本身是由小到大排的)。在这种情况下,排序二叉树就变成了普通链表,其检索效率就会很低。
红黑树的出现是为了改变排序二叉树存在的不足,红黑树是一个更高效的检索二叉树,因此常常用来实现关联数组。JDK提供的集合类TreeMap本身就是一棵红黑树的实现。

红黑树在原有的排序二叉树上增加了如下几个要求:
➢性质1:每个节点要么是红色,要么是黑色。
➢性质2:根节点永远是黑色的。
➢性质3:所有的叶子节点都是空节点(即null), 并且是黑色的。
➢性质4:每个红色节点的两个子节点都是黑色的。(从每个叶子到根的路径上不会有两个连续的红色节点。)
➢性质5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。

上面的性质3中指定红黑树的每个叶子节点都是空节点,而且叶子节点都是黑色,但Java实现的红黑树将使用null来代表空节点,因此遍历红黑树时将看不到黑色的叶子节点,反而看到每个叶子节点都是红色的。
红黑树通过上面这种限制来保证它大致是平衡的一因为红黑树 的高度不会无限增高,这样能保证红黑树在最坏的情况下都是高效的,不会出现普通排序二叉树的情况。红黑树并不是真正的平衡二叉树,但在实际应用中,红黑树的统计性能要高于平衡二叉树,但在极端情况下性能略差。
由于红黑树只是一棵特殊的排序二叉树,因此对红黑树上的只读操作与普通排序二叉树上的只读操作完全相同,只是红黑树保持了大致平衡,因此检索性能更好。但在红黑树上进行插入操作和删除操作会导致树不再符合红黑树的特征,因此插入操作和删除操作都需要进行一定的维护,以保证插入节点、删除节点后的树依然是红黑树。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Brrby

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值