日常中我们见到的二叉树应用有,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虚拟内存的管理,以及B-Tree,B+-Tree在文件系统,都是通过红黑树去实现的。虽然之前写过《再谈堆排序:堆排序算法流程步骤透解—最大堆构建原理》但是二叉树的基本性质,对我来说,从入门到放弃是搞了好几回。
树(Tree):树是一种数据结构,可以表示层次关系,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。形状像一棵倒挂的树。
树:不包含回路的连通无向图(树是一种简单的非线性结构)
树有着不包含回路这个特点,所以树就被赋予了很多特性
在对树进行讨论的时候将树中的每个点称为结点,
树(Tree)是n(n>=0)个结点的有限集。在任意一棵非空树中:
结点的子树的根称为该结点的孩子(Child)。相应的,该结点称为孩子的双亲(Parent)。同一个双亲的孩子之间互称兄弟(Sibling)。其双亲在同一层的结点互为堂兄弟。
深度|高度(Depth):是指从根结点到这个结点的层数,根结点为第一层。每个结点都有深度,比如上图左边的树的4号结点深度是3。
结点的度:结点拥有的子树的数目称为结点的度(Degree)。
层次:根结点的层次为1,其余结点的层次等于该结点的双亲结点的层次加1
树中结点的各子树看成从左至右是有次序的(即不能交换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
森林(Forest)是m(m>=0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。
二叉树(Binary Tree)是一种树形结构,它的特点是每个节点最多只有两个分支节点,一棵二叉树通常由根节点,分支节点,叶子节点组成。而每个分支节点也常常被称作为一棵子树。
二叉树是递归定义的,其结点有左右子树之分。
二叉树是一种非线性结构,二叉树通常采用链式存储结构,存储结点由数据域和指针域(指针域:左指针域和右指针域)组成,二叉树的链式存储结构也称为二叉链表,对满二叉树和完全二叉树可按层次进行顺序存储
二叉树中有两种特殊的二叉树:满二叉树、完全二叉树
第一种解释:完全二叉树是指最后一层左边是满的,右边可能满也可能不满,然后其余层都是满的(这句话不太好理解),看下面第二种解释
第二种解释:除第h层外,其他各层(1到h-1)的结点数都达到最大个数,第h层从右向左连续缺若干结点,则这个二叉树就是完全二叉树
也就是说如果一个结点有右子结点,那么它一定也有左子结点
第三种解释:除最后一层外,每一层上的节点数均达到最大值,在最后一层上只缺少右边的若干结点
深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树。
满二叉树:一颗深度为k且有2^k - 1个节点的二叉树称为满二叉树。
二叉树中每个内部结点都有存在左子树和右子树(或者说满二叉树所有的叶结点都有同样的深度)
满二叉树也是一种完全二叉树,但完全二叉树不一定是满二叉树。
二叉堆由一棵完全二叉树来表示其结构,可用数组来表示。二叉堆需要满足:
注:最大堆:父结点>=子结点,最小堆:父结点=<子结点,
堆的实现通过构造二叉堆(binary heap),实为二叉树的一种;由于其应用的普遍性,当不加限定时,均指该数据结构的这种实现。
二叉查找树(Binary Search Tree,BST),又称为有序二叉树,排序二叉树。每个结点都符合: 父结点>右子结点>左子结点。
二叉查找树中对于目标节点的查找过程类似与有序数组的二分查找,并且查找次数不会超过树的深度。
二叉查找树的性质:对二叉查找树进行中序遍历,即可得到有序的数列。 二叉查找树的高度决定了二叉查找树的查找效率。
在二叉排序树中,某结点的右孩子结点的值一定大于该结点的左孩子结点的值;在堆中却不一定,堆只是限定了某结点的值大于(或小于)其左右孩子结点的值,但没有限定左右孩子结点之间的大小关系。
即
在二叉排序树中,最小值结点是最左下结点,其左指针为空;最大值结点是最右下结点,其右指针为空。在大根堆中,最小值结点位于某个叶子结点,而最大值结点是大根堆的堆顶(即根结点)。
堆是为了实现排序而设计的一种数据结构,它不是面向查找操作的,因而在堆中查找一个结点需要进行遍历,其平均时间复杂度是O(n)。
二叉排序树是为了实现动态查找而设计的数据结构,它是面向查找操作的。对于目标节点的查找过程类似与有序数组的二分查找,在二叉排序树中查找一个结点的平均时间复杂度是O(log n);
设节点数目为n,树的深度为h,假设树的每层都被塞满(第L层有2^L个节点,层数从1开始),则根据等比数列公式可得h=log(n+1)。即最好的情况下,二叉查找树的查找效率为O(log n)。当二叉查找树退化为单链表时,比如,只有右子树的情况,如下图所示,此时查找效率为O(n)。
总之,二叉查找树越是“矮胖”,也就是每层尽可能地被“塞满”(每个父节点均有两个子节点)时,查找效率越高。
为了解决二叉查找树退化为单链表时查找效率低下的问题,引入了平衡二叉树(AVL)。
平衡二叉树定义:平衡二叉树(Balanced Binary Tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用算法有红黑树、AVL树等。在平衡二叉搜索树中,我们可以看到,其高度一般都良好地维持在O(log2n),大大降低了操作的时间复杂度。
最小二叉平衡树的节点的公式如下:F(n)=F(n-1)+F(n-2)+1
这个类似于一个递归的数列,可以参考Fibonacci数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量。
从平衡二叉树的性质可知,平衡二叉树就是避免了二叉查找树退化为单链表的极端情况。二叉查找树的查找、插入、删除较好时间复杂度是O(log n),最差是O(n)。二叉平衡树保证查找、插入、删除的时间复杂度稳定在O(log n)下。
总结:
完全二叉树是效率很高的数据结构,堆是一种完全二叉树或者近似完全二叉树,所以效率极高,像十分常用的排序算法、Dijkstra算法、Prim算法等都要用堆才能优化,二叉排序树的效率也要借助平衡性来提高,而平衡性基于完全二叉树。这里推荐阅读《讲透学烂二叉树一:树和图的概念以及二叉树的基本性质》
AVL树定义:AVL树是最先发明的自平衡二叉查找树。AVL树得名于它的发明者 G.M. Adelson-Velsky 和 E.M. Landis,他们在 1962 年的论文 "An algorithm for the organization of information" 中发表了它。在AVL中任何节点的两个儿子子树的高度最大差别为1,所以它也被称为高度平衡树,n个结点的AVL树最大深度约1.44log2n。查找、插入和删除在平均和最坏情况下都是O(logn)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。
AVL树最关键的也是最难的一步操作就是旋转。旋转主要是为了实现AVL树在实施了插入和删除操作以后,树重新回到平衡的方法。
让AVL树重新平衡的操作叫做旋转(Rotate),旋转操作是树的基本操作也是其中一个难点,对于旋转,使用结点上下移动反而会好理解一点,失衡结点的BF为2或-2,注意这个失衡结点一般取的是最小失衡结点,
AVL树在实现上需要在每个结点中保留高度信息,或者使用平衡因子(Balanced Factor),简称BF,每个结点的平衡因子等于左子树的高度减去右子树的高度,因此平衡值只有三种-1,+1和0。AVL树主要是在增加或删除结点后需要重新计算平衡因子,调整树的结构使其重新平衡。
首先要确定中心结点,即最小失衡结点A,其平衡因子的绝对值为2,主要有四种不平衡的情况:
(1)在A的左儿子B的左子树插入,又称为LL;
(2)在A的左儿子C的右子树插入P,又称为LR;
(3)在A的右儿子C的左子树插入P,又称为RL;
(4)在A的右儿子B的右子树插入,又称为RR。
要记住两个重要节点,一个是失衡结点,另一个是失衡结点的儿子,该儿子在失衡路径上,旋转操作则是依据失衡结点的儿子为中心,对失衡结点进行下移动。在这四种失衡情况中(1)和(4)是一样的,(2)和(3)是一样的,前者使用单旋转,后者使用双旋转。
在进行旋转操作时,首先要找到最小失衡结点,判断失衡的类型,然后选择旋转的类型,如何判断呢?根据上面的图片中的结点A,BF为2确定为左儿子左边L,根据左儿子的BF为-1,则确定为R,此时属于不平衡情况(2),使用双旋转,下面详细介绍单旋转和双旋转的四种旋转方式。
平衡的二叉搜索树一般分为两类:
伸展树(Splay Tree),是一种二叉搜索树(Binary Search Tree,又称二叉排序树Binary Sort Tree),由丹尼尔·斯立特(Daniel Sleator)和 罗伯特·恩卓·塔扬(Robert Endre Tarjan)在1985年发明。
AVL树在每次删除或添加结点时都需要使用旋转操作平衡二叉树,以获得最好的查找效率,伸展树是另一种二叉树,它不需要高度或平衡因子这些平衡信息。伸展树使用另一种方式实现高效率的查找,不平衡但要求每次操作的那个结点旋转到根结点上来,这样下次查找它就能达到最快效率了,这是根据计算机的局部原理,当一块数据被访问后,此后段时间内也会该数据或附近的数据也会被再次用到。这也就是说,进行增加、删除、查找等操作都需要将本次操作的结点或附近结点旋转到根结点上,可对所有操作都调整,或只针对查找进行调整。
伸展树进行M次操作,其时间复杂度为O(M logN),而普通二叉树最坏情况为O(N),连续M次操作为O(M*N)。如果一个算法M次操作的时间为O(MF(N)),则O(F(N))称为该算法的摊还时间或摊还代价,伸展树的摊还代价为O(logN)。
综上,伸展树不需要AVL树的平衡信息,高度或BF,它是一个普通二叉查找树,它的出发点是:频繁查找一个深结点X,会造成花费的时间过多,采取的办法是:将树在X处展开,将该结点旋转到根结点,自下向上单旋转,对访问路径上的每个结点和父结点进行单旋转,这样频繁访问结点即可大大减少时间,但是执行M次操作仍然至少需要M*N的时间(最坏情况单链表为N)。
伸展树在实现上可使用上面说的单旋转,根据目标结点,全部使用单旋转,但是效率并不好,例如单链表的情况,依次插入1、2、3、4、5,其效率并不好,结点4深度依然比较深,如下图:
为了解决这个问题,我们采取一种特别的实现,根据三种情况进行旋转:
(1)当前结点只有父亲结点,使用单旋转,很明显这种情况的父亲结点为根结点;
(2)当前结点有父亲结点和祖父结点,呈之字形,例如当前结点是父亲结点的右儿子,父亲结点是祖父结点的左儿子。之字形的情况进行一次AVL双旋转,如下图:
当前结点有父亲结点和祖父结点,呈一字形,也就是类似LL和RR的情况,但是并不是使用单旋转,而是进行一字形对称旋转。假设祖父结点是根结点,那么让当前结点X成为根结点,父亲结点称为X的右儿子,祖父结点成为父亲结点的右儿子,X的原右儿子成为父亲的左儿子,父亲结点的右儿子成为祖父结点的左儿子,下图是一个例子:
相对于仅仅使用单旋转,新实现方法的效率更高,使用新的旋转方式,对1、2、3、4、5的单链表情况在5处展开的过程如下图:
红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由鲁道夫·贝尔发明的,称之为"对称二叉B树",它现代的名字是在 Leo J. Guibas 和 Robert Sedgewick 于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在O(logn)时间内做查找,插入和删除,这里的n是树中元素的数目。
红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。这不只是使它们在时间敏感的应用如实时应用(real time application)中有价值,而且使它们有在提供最坏情况担保的其他数据结构中作为建造板块的价值;例如,在计算几何中使用的很多数据结构都可以基于红黑树。此外,红黑树还是2-3-4树的一种等同,它们的思想是一样的,只不过红黑树是2-3-4树用二叉树的形式表示的。
红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制的一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
设平衡二叉树的深度为N,则N%2=0结点为黑色,N%2=1结点为红色。
下面是一个具体的红黑树的图例:
这些约束确保了红黑树的关键特性: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。
要知道为什么这些性质确保了这个结果,注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。
红黑树这段内容来自maybe2030 整理自wiki百科之红黑树的内容
B-树和下面的B+树是相当有用和比较重要的树数据结构(B-树和B树的叫法是一样的),B树,概括来说是一个一般化的二叉查找树,可以拥有多于2个子节点。与自平衡二叉查找树不同,B-树为系统最优化大块数据的读和写操作。B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。这种数据结构常被应用在数据库和文件系统的实作上。
B-树也是一种平衡树,称为M路平衡查找树(并不是二叉的,M=2就是平衡二叉查找树),M称为阶数或度数或叉数或最多子树数,指的是一个结点拥有最多的儿子数。上面一直提到关键字域,关键字用于确定结点的分布规则,又称为键值,和数据库表的主键和唯一键是一样的。1个关键字最多有2个儿子,如二叉树,M阶平衡树的关键字数为M-1,在B-树的数据结构实现上,主要是使用关键字数M-1,可使用数组存储,或设计其它的容器如链式数据结构,其中3阶B树又叫2-3树,4阶B树又叫2-3-4树,如下图是一个3阶B叉树:
B-tree树即B树,B即Balanced,平衡的意思。因为B树的原英文名称为B-tree,而国内很多人喜欢把B-tree译作B-树,其实,这是个非常不好的直译,很容易让人产生误解。如人们可能会以为B-树是一种树,而B树又是另一种树。而事实上是,B-tree就是指的B树。
B树的搜索,从根结点开始,如果查询的关键字与结点的关键字相等,那么就命中;否则,如果查询关键字比结点关键字小,就进入左儿子;如果比结点关键字大,就进入当前兄弟节点的右边节点(二叉树就是右节点)
首先要指出,以上B-树只是一个参考的规范并不是绝对标准,你可以根据自己的需求自己设计,这里的M一直指的都是每个结点的最大儿子数,而在我们的代码中更直接的是使用关键字数,关键字数等于M-1,要注意是否混淆了。
B树的主要数据存储在所有结点上,和一般的二叉树是一样的,在二叉树上一个关键字对应一个数据对象,B树中H个关键字对应有H个数据对象,也就是说关键字和数据对象的数量是相同的。如果使用的是数据对象中的成员作为数据关键字,则在节点中可以直接声明一个数据对象的数组存储(或者其它类型的容器),否则自定义创建一个关键字数组,另外再创建数据对象数组,这样会相当麻烦(实际可以在数据对象中创建虚拟关键字,在结点声明)。
B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点;
B树和B+树的主要应用在于数据库开发,例如MySQL的索引引擎就是使用B+树实现的,如此看来,使用数据库的目的除了数据持久化就是索引了,如果数据库失去了索引功能,那么和一般的文件访问也就区别不大了。说到数据库,这里稍微讨论一下相关的内容,数据库文件是存储在硬盘上的,程序访问数据需要调用外设硬件去读取硬盘上的数据,一次读取操作称为一次IO操作,IO操作是比较耗时的,因此提高数据访问的速度也就是要降低IO操作的次数。
相对于物理磁盘而言,数据的最小单位为扇区,一般512字节,相对文件系统而言,一次IO操作读取的数据大小目前可达到4K,也就是8个扇区。假如一个结点为4K,N个结点,最多深度为O(log N),底数为M/2,加入一个结点存储200个关键字,一百万个结点只需读取几次即可,而关键字的查询速度也是对数时间O(log M),如此你可以发现使用B树或B+树的功能强大。
B+树和B树的定义是等价的,其中有的定义是儿子数比关键字数小1,这个不是很重要,完全可以自定义。
B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;
1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
2.不可能在非叶子结点命中;
3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
4.更适合文件索引系统。
注意,只有叶子结点才存储实际数据,MySQL的InnoDB引擎直接在叶子结点存储数据本身,而MyISAM引擎则是在叶子结点存储数据的逻辑地址,前者的方式称为聚簇索引,后者称为非聚簇索引,下面是这两种索引结构的粗略图:
B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;
B*树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针,将结点的最低利用率从1/2提高到2/3。
B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2);
B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;
所以,B*树分配新结点的概率比B+树要低,空间使用率更高。
Tire树称为字典树,又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
前缀树里面可以存一堆字符串,也可以说是一堆单词,存完之后我们可以轻松判断一个指定的字符串是否出现过。”比如说对于某一个单词,我们要询问它的前缀是否出现过。这样hash就不好搞了,而用trie还是很简单“。例如:给你100000个长度不超过10的单词。对于每一个单词,我们要判断他出没出现过,如果出现了,求第一次出现在第几个位置。
给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
在这道题中,我们可以用数组枚举,用哈希,用字典树,先把熟词建一棵树,然后读入文章进行比较,这种方法效率是比较高的。
给定N个互不相同的仅由一个单词构成的英文名,让你将他们按字典序从小到大输出。用字典树进行排序,采用数组的方式创建字典树,这棵树的每个结点的所有儿子很显然地按照其字母大小排序。对这棵树进行先序遍历即可。
对所有串建立字典树,对于两个串的最长公共前缀的长度即他们所在的结点的公共祖先个数,于是,问题就转化为求公共祖先的问题。
关于算法相关的详细代码,查看https://github.com/zhoulujun/algorithm
参考文章:
[Data Structure] 数据结构中各种树 https://www.cnblogs.com/maybe2030/p/4732377.html#_label3
你真的懂树吗?二叉树、AVL平衡二叉树、伸展树、B-树和B+树原理和实现代码详解 www.srcmini.com/1315.html
伸展树(Splay Tree)进阶 - 从原理到实现 https://www.cnblogs.com/dilthey/p/9379652.html#splay-2.1
二叉树的遍历(前序、中序、后序、已知前中序求后序、已知中后序求前序) https://www.cnblogs.com/lanhaicode/p/10390147.html
js数据结构-二叉树(二叉堆)https://segmentfault.com/a/1190000017761929
图的基本概念,图的遍历、拯救007 https://www.cnblogs.com/hi3254014978/p/9535276.html
小白学数据结构——二、树与堆(基本概念及二叉树、二叉堆的python实现) https://blog.csdn.net/qq_33414271/article/details/78506632
如果子结果编号为i,求其父节点编号 https://blog.csdn.net/qingmengwuhen1/article/details/51926409?utm_source=blogxgwz5
常见数据结构(二)-树(二叉树,红黑树,B树) https://segmentfault.com/a/1190000007173881
js 中二叉树的深度遍历与广度遍历(递归实现与非递归实现) https://www.jianshu.com/p/5e9ea25a1aae
平衡二叉树-AVL树(LL、RR、LR、RL旋转) https://www.cnblogs.com/ybf-yyj/p/9513706.html
数据结构----树及二叉树的遍历JS https://blog.csdn.net/qq_43043859/article/details/101347877
https://www.cnblogs.com/guxuanqing/p/10540551.html
转载本站文章《讲透学烂二叉树(二):图中树的定义&各类型树的特征分析》,
请注明出处:https://www.zhoulujun.cn/html/theory/algorithm/TreeGraph/8282.html