树是一种非常常用的数据结构,树与前面介绍的线性表,栈,队列等线性结构不同,树是一种非线性结构
计算机世界里的树,是从自然界中实际的树抽象而来的,它指的是N个有父子关系的节点的有限集合。对于这个有限的节点集合而言,它满足如下条件:
从上面定义可以发现树的递归特性:一棵树由根和若干棵子树组成,而每棵子树又由若干棵更小的子树组成。
树中任一节点可以有0或多个子节点,但只能有一个父节点。根节点是一个特例,根节点没有父节点,叶子节点没有子节点。树中每个节点既可以是其上一级节点的子节点,也可以是下一级节点的父节点,因此同一个节点既可以是父节点,也可以是子节点(类似于一个人—————他既是他儿子的父亲,又是他父亲的儿子)。
很显然,父子关系是一种非线性关系,所以树结构是非线性结构。
如果按节点是否包含子节点来分,节点可以分成以下两种:
如果按节点是否具有唯一的父节点来分,节点有可分为如下两种:
一棵树只能有一个根节点,如果一棵树有了多个根节点,那么它已经不再是一棵树了,而是多棵树的集合,有时也被称为森林。示意图如下:
tree.PNG
与树有关的术语有如下一些:
如果需要实现一棵树,程序不仅要以合适的方式保存该树的所有节点,还要记录节点与节点之间的父子关系。接下来,还应该为树实现如下基本操作。
通过前面的介绍可以发现,树中除根节点之外的每个节点都有一个父节点。为了记录树中节点与节点之间的父子关系,可以为每个节点增加一个parent域,用以记录该节点的父节点。用如下图和如下表来表示
tree_show.PNG
数组索引 | data | parent |
---|---|---|
0 | A | -1 |
1 | B | 0 |
2 | C | 0 |
3 | D | 0 |
4 | E | 1 |
5 | F | 3 |
6 | G | 3 |
7 | H | 4 |
8 | I | 4 |
9 | J | 4 |
10 | K | 6 |
... | ... | ... |
由此可见,只要用一个节点数组来保存树里的每个节点,并让每个节点记录其父节点在数组中的索引即可。
父节点表示法的思想是让每个节点“记住”它的父节点的索引,父节点表示法是从子节点着手的;反过来,还有另外一种方式:让父节点“记住”它的所有子节点口在这种方式下,由于每个父节点需要记住多个子节点,因此必须采用“子节点链”表示法。示意图如下:
tree_linked.PNG
二叉树指的是每个节点最多只能有两个子树的有序树。通常左边的子树被称作“左子树”(left subtree),右边的子树被称为“右子树”(right subtree).由此可见,二叉树依然是树,它是一种特殊的树。 二叉树的每个节点最多只有来两颗树(不存在度大于2的节点),二叉树的子树有左,右之分,次序不能颠倒。 树和二叉树的两个重要区别如下:
一棵深度为k的二叉树,如果它包含了
2^k-1
个节点,就把这棵二叉树称为满二叉树。满二叉树的特点是。每一层上的节点数都是最大节点数,即各层节点数分别为1,2,4,8, 16,...,满二叉树下图所示:
two_tree.PNG
一颗有n个节点的二叉树,按满二叉树的编号方式对它进行编号,若树中所有节点和满二叉树1~n编号完全一致,则称该树为完全二叉树。也就是说,如果一颗二叉树除最后一层外,其余层的所有节点都是满的,并且最后一层或者是满的,或者仅在右边缺少若干连续的节点,则此二叉树就是完全二叉树。
综上所述,二叉树大致有如下几个性质:
对于一颗具有n个节点的完全二叉树的节点按层自左向右编号,则对任一编号为i(n>=i>=1)的节点有下列性质。
二叉树记录其节点之间的父子关系更加简单,因为二叉树中的每个节点最多只能保存两个子节点。接下来,程序也需要为二叉树实现如下基本操作。
要实现二叉树这种数据结构,有以下三种选择。
顺序存储指的是充分利用满二叉树的特性:每层的节点数分别为1, 2, 4, 8,…,2的(i-1)2的i次方。一棵 深度为i的二叉树最多只能包含2的i次方一1个节点,因此只要定义一个长度为2的i次方一1的数组即可存储这棵二叉树。
对于普通二叉树(不是满二叉树),那些空出来的节点对应的数组元素留空就可以了。由此可见,二叉树采用顺序存储会造成一定的空间浪费。对于下图1所示的二叉树(完全二叉树),采用下图2所示的数组来保存即可。
图1.PNG
图2.PNG
对于左图所示的二叉树,需使用右图所示的数组来保存。
compare_tree.PNG
当使用数组来存储二又树的所有节点时可能会产生一定的空间浪费,如果该二叉树是完全二叉树,就不会有任何空间浪费了;但如果该二叉树的所有节点都只有右子节点,那么就会产生相当大的空间浪费.
二叉链表存储的思想是让每个节点都能“记住”它的左,右两个子节点。为每个节点增加left,right两个指针,分别引用改节点的左,右两个子节点,因此二叉链表存储的每个节点有如下图结构:
two_fork_tree.PNG
二叉链表存储的二叉树的节点大致有如下定义:
class Node{
Object data;
Node left;
Node right;
}
对于这种二叉链表存储的二叉树,如果程序需要,为指定节点添加子节点也非常容易,让父节点的left或right引用指向新节点即可。
三叉链表存储的思想是让每个节点不仅“记住”它的左右两个子节点,还要“记住”它的父节点,因此需要为每个节点增加left,right和parent三个指针,分别引用该节点的左,右两个子节点和父节点。因此,三叉链表存储的每个节点有如下图的结构:
three_tree.PNG
因此三叉链表存储的二叉树的节点大致如下:
class Node{
Object data;
Node left;
Node right;
Node parent;
}
对于这种三叉链表存储的二叉树,如果程序需要,为指定节点添加子节点也非常容易,除了要维护父节点的left,right引用之外,还要维护新增节点的parent引用。
遍历二叉树指的是按某种规律依次访问二叉树的每个节点,对二叉树的遍历过程就是讲非线性结构的二叉树的节点排列成线性序列的过程。
如果采用顺序结构来保存二叉树,程序遍历二叉树非常容易,无须进行任何思考,直接遍历底层数组即可。如果采用链表来保存二叉树的节点,则有以下两种遍历方式。
如果L,D,W表示左子树、根、右子树,习惯上总是必须先遍历左子树,后遍历右子树,根据遍历根节点的顺序不同,上面三种算法可表示如下。
深度遍历的先序遥历、中序遍历、后序遍历这三种遍历方式的名称都是针对根节点(D)而言的。先处理根节点(D)时就称为先序遍历。其次处理根节点(D)时就称为中序遍历;最后处理根节点(D)时就称为后序遍历。
先序遍历指先处理根节点,其处理顺序如下: (1) 访问根节点 (2) 递归遍历左子树 (3) 递归遍历右子树
中序遍历指其次处理根节点.其处理顺序如下。 (1) 递归遍历左子树 (2) 访问根节点 (3) 递归遍历右子树
后序遍历指最后处理根节点,其处理顺序如下。 (1) 递归遍历左子树 (2) 递归遍历右子树 (3) 访问根节点
广度优先遍历又称为按层遍历,整个遍历算法是先遍历几叉树的第一层(根节点),再遍历根节点的两个子’节点(第二层)……依此类推,逐层遍历二叉树的所有节点。
为了实现广度优先遍历,可以借助于具有FIFO特征的队列来实现。如下所示。
由于二叉树是一种更“确定”(它的每个节点最多只有两个子节点)的数据结构,因此不管是存储、增加、删除节点,还是遍历节点,程序都可以更简单、方便地实现口反之,由于树的每个节点具有个数不确定的节点,因此程序实现起来更复杂。
为了充分利用二义树的简单易用性,可以将普通树转换为二叉树,以二叉树的形式来保存柞通树,当程序需要树时,再将悦义树转换为普通树。
森林其实更简单,如果将一棵伶通树的根节点去掉,这棵树就变成了森林。或者可以转换一下思维,森林其实就是有多个根节点的树。
有序树,森林和二叉树之间有一一映射的关系,可以进行互相转换。
多叉树向二叉树的方法如下:
如图就是多叉图向二叉树转换的结果
forest_tree.PNG
图中的虚线就是新增的“父子”关系。这个转换结果来看,多叉树1转换为二叉树的方法的关键思想就是:所有子节点只保留子节点,其他子节点转为左子节点的右子节点链。
按照这个转换思路,森林也可转换为二叉树————只要把森林当成一颗根节点被删除的多叉树即可。下图示范了将森林转换为二叉树的结果。
forest_to_tree.PNG
反过来,二叉树也可恢复出对应的多叉树,森林,恢复方法如下:
-(1)加虚线:若某节点I是父节点的左子节点,则为该节点I的右孩子链的所有节点分别于节点I的父节点添加连线
把二叉树转换为多叉树
two_tree_more_tree.PNG
如果二叉树的根节点有右子节点————右子节点就代表根节点的兄弟节点,这种情况会转换得到森林。
把二叉树转换为森林
tree_to_forest.PNG
根据上面介绍的理论,二义树可以和多叉树之间进行自由转换,因此可以得到普通树的另外一种保存方式:以二义树的形式保存多叉树,实际需要的时候再将二叉树转换为普通树。 至于到底以哪种方式来保存二叉树,完全是自由的。通常会选择使用三叉链表存储方式来保存二叉树,这样得到的二叉树操作起来更方便,进行二叉树和多叉树之间转换时也更方便。
哈夫曼树又被称为最优二叉树,是一种带权路径最短的二叉树。哈夫曼树是二叉树的一种应用,在信息检索中很常用.
在介绍哈夫曼树之前先来介绍一些相关的概念。
对于下图所示的而二叉树,该树的路径长度为17.即0+1+2+2+3+4+5==17.
hafuman.PNG
daiquan.PNG
对于哈夫曼树,有一个很重要的定理:对于具有对n个叶子节点的哈夫曼树,一共需要2乘以n-1个节点。因为对于二叉树来说,有三种类型节点,即度数为2的节点、度数为1的节点和度数为0的叶子节点,而哈夫曼树的非叶子节点都是由两个节点合并产生的,所以不会出现度 数为1的节点。而生成的非叶子节点的个数为叶子节点个数-1因此n个叶子节点的哈夫曼树,一共需要Z乘以n-1个节点。
创建哈夫曼树,可以按如下步骤进行:
下图显示了创建哈夫曼树的过程。
hafuman_tree.PNG
根据哈夫曼树可以解决报文编码问题。假设需要对一个字符串如“a6cdabcaba”进行编码,将它转换为唯一的二进制码,但要求转换出来的二进制码的长度最小。
假设每个字符在字符串中出现的频率为W}其编码长度为L,编码字符有n个,则编码后二进制码的总长度为W1L1+W2L2+W3L3+...+WnLn,这正好符合哈夫曼树的处理原则。因此可采用哈夫曼树的原理构造二进制编码,并使电文总长最短。
对于“abcdabcaba”字符串,总共只有a,b,c,d,这四个字符,它们出现的次数是4,3,2,1次__这相当于它们的权值。于是,将a,b,c,d四个字符以出现的次数为权值构造哈夫曼树,得到如下图结构:
hanfuman1.PNG
从哈夫曼树根节点开始,对左子树分配代码“0”,对右子树分配代码“1”,一直到达叶子节点。然后.将从树根沿每条路径到达叶子节点的代码排列起来,便得到了每个叶子节点的哈夫曼编码。下图显示了对a, b, c, d四个字符编码得到的哈夫曼编码。
hanfuma2.PNG
排序二叉树是一种特殊结构的二叉树,通过它可以非常方便地对树中的所有节点进行排序和检索
排序二叉树要么是一颗空二叉树,要么是具有下列性质的二叉树
下图显示了一棵排序二叉树. 对于排序二叉树,若按中序遍历就可以得到由小到大的有序序列。中序遍历得:
{2,3,4,8,9,9,10,13,15,18)
sort_tree.PNG
创建排序二义树的步骤,就是不断地向排序二义树添加节点的过程,几体如下。
当程序从排序二叉树中删除一个节点之后,为了让它依然保持为排序哭叉树,必须对该排序二叉树进行维护。维护可分为如下几种情况。
被删除的节点只有左子树的情况
delete_only_left_tree.PNG
被删除节点只有右子树的情况
delete_only_right_tree.PNG
delete_left_right.PNG
以P节点的中序前趋或后继替代P所指节点,然后从原排序二叉树中删除中序前趋或后继节点。简单来说,就是用大于p的最小节点或小于P的最大节点代替P节点点,采 用这种方式删除节点的示意图如下图:
delete_left_right_center.PNG
排序二叉树虽然可以快速检索,但在最坏的情况下,如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二义树将变成链表:所有节点只有左节点(如果插入节点集合本身是由大到小排列的),或者所有节点只有右节点(如果 插入节点集合本身是由小到大排列的)。在这种情况下,排序二叉树就变成了普通链表,其检索效率就会很低。
为了改变排序二叉树存在的不足,对二叉树进行改进————红黑树,他将这种排序二叉树称为“对称二叉B树”。
红黑树是一个更高效的检索二叉树,因此常常用来实现关联数组。典型的,JDK提供的集合类TreeMap本身就是一颗红黑树的实现。 红黑树在原有的排序二叉树上增加如下几个要求:
java实现的红黑树结构如下图:
red_black_tree.PNG
由此可以得出结论:对于给定的黑色高度为N的红黑树,从根到叶子节点的最短路径长度为N-1,最长路径长度为2*(N-1).
红黑树通过上面这种限制来保证它大致是平衡的—因为红黑树的高度不会无限增高,这样能保证红黑树在最坏的情况下都是高效的,不会出现普通排序二叉树的情况。
由于红黑树只是一棵特殊的排序二叉树,因此对红黑树上的只读操作与普通排序二叉树上的只读操作完全相同,只是红黑树保持了大致平衡,因此检索性能更好.
但在红黑树上进行插入操作和删除操作会导致树不再符合红黑树的特征,因此插入操作和删除操作都需要进行一定的维护,以保证插入节点、删除节点后的树依然是红黑树。
插入操作按如下步骤进行:
这种颜色调换和树旋转就比较复杂了,下面将分情况进行介绍。在介绍中,把新插入的节点定义为N节点,把N节点的父节点定义为P节点,把P节点的兄弟节点定义为U节点,把P节点的父节点定义为G节点。
在这种情形下,直接将它设置为黑色以满足性质2。
在这种情形下,新插入的节点是红色的,因此依然满足性质4。而且因为新节点N有两个黑色叶子节点,但是由于新节点N是红色的,通过它的每个子节点的路径依然保持相同的黑色节点数,因此依然满足性质5
3.情形3:父节点P和父节点的兄弟节点U都是红色的
在这种情形下,程序应该将P节点、U节点都设置为黑色,并将P节点的父节点设置为红色(用来保持性质5)。现在,新节点N有了一个黑色的父节点P。由于从P节点、U节点到根节点的任何路径都必须通过G节点,这些路径上的黑色节点数目没有改变(原来有叶子和G节点两个黑色节点,现在有叶子和P节点两个黑色节点)。
经过上面处理后,红色的G节点的父节点也有可能是红色的,这就违反了性质4,因此还需要对G节点递归地进行整个过程〔把G节点当成新插入的节点进行处理)。 下图显示了处理过程:
red_black_tree_1.PNG
在这种情形下,对新节点和其父节点进行一次左旋转。接着,按情形5处理以前的父节点P(也就是把P当成新插入的节点)。这将导致某些路径通过它们以前不通过的新节点N或父节点P其中之一,但是这两个节点都是红色的,因此不会影响性质5。
red_black_tree_2.PNG
在这种情形下,需要对节点G进行一次右旋转口在旋转产生的树中,以前的父节点P现在是新节点N和节点G的父节点。由于以前的节点G是黑色的(否则父节点P就不可能是红色的),切换以前的父节点P和节点G的颜色,使之满足性质4。性质5也仍然保持满足,因为通过这三个节点中任何一个的所有路径以前都通过节点G,现在它们都通过以前的父节点P。在各自的情形下,这都是三个节点中唯一的黑色节点。
red_black_tree_3.PNG
红黑树的删除操作比插入操作要稍微复杂一些,实际上也可按如下步骤进行: