数组和链表是常用的数据结构,数组虽然查找快(有序数组可以通过二分法查找),但是插入和删除是比较慢的;而链表,插入和删除很快(只需要改变一些引用值),但是查找就很慢,需要从头开始遍历;
那么有没有一种数据结构能同时具备数组查找快的优点以及链表插入和删除快的优点呢,那就是接下来要介绍的——树。
1、二叉查找树
特性:
- 1、左子树上所有节点的值均小于它的根节点的值;
- 2、右子树上所有节点的值均大于它的根节点的值;
- 3、左、右子树也分别为二叉排序树。
但是一个不好会形成链表结构,比如一个有序数组形成的二叉树
2、平衡二叉查找树
特性:
- 1、在二叉树的基础上,要求两个子树的高度差不能超过1;
- 2、每次增删都会通过一次或多次旋转来平衡二叉树;
调整平衡的基本思想:
当在二叉排序树中插入一个节点时,首先检查是否因插入而破坏了平衡,若破坏,则找出其中的最小不平衡二叉树,在保持二叉排序树特性的情况下,调整最小不平衡子树中节点之间的关系,以达到新的平衡。
所谓最小不平衡子树,指离插入节点最近且以平衡因子的绝对值大于1的节点作为根的子树。
先插入指定节点,记录下当前节点的信息,LH,EH或者RH。
- 若左子树高LH,查看其左子树根节点的信息,若是LH,则一次右旋;若是RH,则一次左旋+一次右旋
- 若右子树高RH,查看右子树根节点的信息,若是RH,则一次左旋;若是LH,则一次右旋+一次左旋
- 调整改变的节点信息
虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树,且随着树的高度的增加,动态插入和删除的代价也越来越大,为了解决优化插入和删除性能,红黑树出现了。
显然,如果在那种插入、删除很频繁的场景中,平衡树需要频繁着进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是有了红黑树,红黑树具有如下特点:
3、红黑树:
特性:
- 1、根节点是黑色;
- 2、每个节点或者是黑色,或者是红色;
- 3、每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!;
- 4、如果一个节点是红色的,则它的子节点必须是黑色的,也就是说一个红节点的父节点只能是黑色
- 5、从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点,也就是说确保没有一条路径会比其他路径长出俩倍;
红黑树(Red Black Tree) 是一种自平衡二叉查找树
红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
二叉平衡树的严格平衡策略以牺牲建立查找结构(插入,删除操作)的代价,换来了稳定的O(logN) 的查找时间复杂度
它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。
红黑树的操作代价:
-
查找代价:由于红黑树的性质(最长路径长度不超过最短路径长度的2倍),可以说明红黑树虽然不像平衡二叉查找树一样是严格平衡的,但平衡性能还是要比二叉查找树要好。其查找代价基本维持在O(logN)左右,但在最差情况下(最长路径是最短路径的2倍少1),比平衡二叉查找树要略逊色一点。
-
插入代价:RBT插入结点时,需要旋转操作和变色操作。但由于只需要保证RBT基本平衡就可以了。因此插入结点最多只需要2次旋转,这一点和平衡二叉查找树的插入操作一样。虽然变色操作需要O(logN),但是变色操作十分简单,代价很小。
-
删除代价:红黑树的删除操作代价要比平衡二叉查找树要好的多,删除一个结点最多只需要3次旋转操作。
RBT 效率总结 : 查找效率最好情况下时间复杂度为O(logN),但在最坏情况下比平衡二叉查找树要差一些,但也远远好于二叉查找树。
插入和删除操作改变树的平衡性的概率要远远小于AVL(RBT不是高度平衡的)。因此需要的旋转操作的可能性要小,而且一旦需要旋转,插入一个结点最多只需要旋转2次,删除最多只需要旋转3次(小于AVL的删除操作所需要的旋转次数)。虽然变色操作的时间复杂度在O(logN),但是实际上,这种操作由于简单所需要的代价很小。
红黑树能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,任何不平衡都会在3次旋转之内解决。这一点是平衡二叉查找树所不具备的。
调整平衡的基本思想:
- 变色:为了重新符合红黑树的规则,尝试把红色节点变为黑色,或者把黑色节点变为红色。
- 左旋转:逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子。说起来很怪异,大家看下图:
- 右旋转:顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子。大家看下图:
场景:HashMap TreeSet TreeMap
4、 B树:
设计缘由:
数据库索引是存储在磁盘上的,当数据量比较大的时候,索引的大小可能有十几个G,甚至更多。当我们利用索引查询的时候,能把整个索引全部加载到内存吗?显然不可能,能做的只有逐一加载每一个磁盘页,这里的磁盘页对应着索引树的节点。
如果我们利用二叉查找树作为索引结构,那么最坏(每个节点数据在不同磁盘页上)的情况下,磁盘IO次数等于索引树的高度。 所以,为了减少磁盘IO次数,我们就需要把原本“瘦高”的树结构变得“矮胖”(节点数变少了)。这就是B树的特征之一
B树是一种多路平衡查找树,它的每一个节点最多包含m个孩子,m被称为B树的阶,m的大小取决于磁盘页的大小。——一个节点最多包含一个磁盘页的数据
也就是说,当B树变得矮胖以后,每个节点可以包含多个数据(数据量大小由磁盘页的大小决定),同样的数据,B树比二叉树/红黑树节点少。所以,B树在查询时,磁盘IO次数变少了,从而可以提升查找性能。
特征:
一个m阶的B树具有如下几个特征:
- 1.根结点至少有两个子女。
- 2.每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
- 3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
- 4.所有的叶子结点都位于同一层。
- 5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划
场景:
B树主要应用于文件系统以及部分数据库索引,比如著名的非关系型数据MongoDB。
而大部分关系型数据库,比如mysql,则使用B+树作为索引
5、 B+树
B+树是B树的一种变体,但是又有所不同,
特征:
一个m阶的B+树具有如下几个特征:
-
1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
-
2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
-
3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
这里特别要注意有两点:
其一:每个父节点的元素都出现在子节点中,是子节点的最大(或最小)元素;因此所有叶子节点包含了全量元素信息;
其二:每个叶子节点都带有指向下一个节点的指针,形成了一个有序链表;
其三:只有叶子节点带有卫星数据,其余中间节点仅仅是索引,没有任何数据关联,如下图,所谓卫星数据,指的是索引元素所指向的数据记录,比如数据库中的某一行,在B树种,无论中间节点还是叶子节点都带有卫星数据。
设计优势
B+树的好处主要体现在查询性能上,由于B+树的中间节点没有卫星数据,所以同样大小的磁盘页可以容纳更多的节点元素,这就意味着,一次性加载到内存中的节点元素更多,从而使得查询时IO次数也更少。(举个简单的例子,一个磁盘页可以加载B树的100个节点元素,但是可以加载B+树的1000个节点元素,那么对于查找999这个数来说,B数需要10次IO,B+数只需要1次IO)
B+树相对B树的优点:
-
IO一次读数据是从磁盘上读的,磁盘容量是固定的,取数据量大小是固定的,非叶子节点不存储数据,节点小,磁盘IO次数就少。
-
B+树查询性能稳定,因为B+树的查询必须最终查找到叶子节点;
而B树,只要找到匹配元素即可,无论匹配元素处于中间节点,还是叶子节点。所以B树的查询性能并不稳定,最好情况是只查根节点,最坏情况是查到叶子节点 -
B+树的所有Data域在叶子节点,一般来说都会进行一个优化,就是将所有的叶子节点用指针串联起来(可以认为是链表),遍历叶子节点就能获取全部数据,这样就能进行区间访问了。
B树做范围查询,只能繁琐的遍历,但是B+树,只需要查到查找到范围下限以后,遍历叶子节点(有序链表)就可以了。
综合起来,B+树比B树的优势有三个:1、IO次数更少;2、查询性能更佳;3、范围查询简便
场景:Mysql索引
6、红黑树 VS B+树
红黑树的深度比B+树大,当数据量小时,可以把数据完全放到内存中,红黑树的时间复杂度比B树低(不用每次都查到叶子节点),如linux中进程的调度用的是红黑树,Java中HashMap、TreeMap、TreeSet(都在内存中操作)也都是用红黑树实现;
但是,当数据量大时,不能一次性把数据放到内存时,红黑树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下;而B树因其读磁盘次数少,具有更快的速度。