提及红黑树就头痛,在大学时就没搞懂,看的晕晕乎乎,理解不了。直到前几天在极客时间的《数据结构与算法之美》
专栏中的《26 | 红黑树(下):掌握这些技巧,你也能够实现一个红黑树 》,再次看到讲解红黑树插入删除如何保持平衡,很惋惜,仍是没看明白。但在留言区看到小伙伴推荐的红黑树是2-3树的变形,以2-3树的角度去理解红黑树就容易多了
。因而,就跑去看了2-3树
相关的文章,发现理解起来是要简单些。算法
通常咱们接触最多的是二叉树,也就是一个父节点最多有两个子节点。2-3树的意思就是说,一个父节点能够有两个子节点,也能够有三个子节点,而且其也知足相似二叉搜索树
的定义(父节点的值大于左子树,但小于右子树),全部叶子节点都在同一层。bash
2节点:父节点存储一个值,最多有左右两个子树。假设父节点为p,子节点为l(左节点)、r(有节点),且知足:数据结构
l < p < r
复制代码
如图所示: spa
3节点:父节点存储两个值,最多有左中右三个子树。假设父节点分别为p1,p2,子节点分别为l(左节点)、m(中间节点)、r(右节点),且知足:3d
l < p1
p1 < m < p2
r > p2
复制代码
如图所示: code
跟BST
的查找相似:cdn
从根节点开始比较,若相等,则结束。若是小于根节点,则说明它应该在左边,选定左节点进行比较;若是大于根节点,则说明在右边,选定右节点进行比较,如不相等,则继续循环。如到最后访问到空节点,则说明没找到。blog
只不过对于3节点的状况,就须要判断左中右子树,原理同样。get
下面以这颗树举例说明,要查找的值存在和不存在的状况。it
查找5。
查找24。
在将插入以前,先介绍一下节点分裂与合并。
2-3树只能存在2节点和3节点,因为插入的时候会引入4节点,因此咱们须要将其分裂。
好比单个4节点,只需将中间节点往上提,左边值做为其左子树,右边值做为其右子树便可。
好比有父节点的4节点,节点分裂后,需与父节点进行合并。若合并后父节点仍是4节点,则继续分裂,直至知足定义为止。下图中6与3合并后,知足条件,无需再进行操做。
插入一个节点后,也要知足2-3树的定义。咱们须要找到一个适合的位置来插入新的值,可是和二叉树不一样的是,它不会生成新的叶子节点来存储,而是找到合适的叶子节点来进行合并。
可是注意,插入的原则是尽可能保持树的高度
,也就是尽可能不要增长
树的高度。由于树的高度越小,查找效率会更高。
下面分几种状况来讲明不一样的处理状况。其关键字是往上分裂
,从下往上生长。
生成新节点,则其为根节点。
若是不能直接放到空的子节点,则放到父节点中,此时成为3节点,仍然知足定义。好比咱们在这棵树中插入12。
首先找到待插入节点9。
节点9为2节点,可直接插入。
这种状况下,稍微会复杂一些,由于涉及到分裂,且跟待插入节点的父节点
有关。假定待插入节
点为p
,待插入节点的父节点
为pp
。
将节点强插到p中,此时p中会有三个值,咱们暂且称之为4节点
。4节点是不知足2-3树的定义的,所以须要将4节点中的某个节点往上抽离,与pp
进行合并。这时须要考虑pp的类型了。
若pp
为二节点
将分裂的节点放到pp
中,则pp
成为3节点
,知足定义。好比咱们在这棵树中插入13。
若pp
为三节点
将分裂的节点放到pp
中,则pp
成为了4节点
,不知足定义,那么4-节点须要提出一个值,并向上合并,这时须要从新设置新旧节点的关系。往上合并的过程就是继续套用这几种状况。好的状况是往上的过程当中遇到了2节点,且平衡,则结束;坏的状况是一直到根节点,而且根节点是3树,那么只好继续往上分裂出新的根节点,而后处理新节点与其余节点的关系,此时树高增长了1。
好比在这颗树中插入18。
2)向上分裂,将18插入父节点,变成4节点,需继续分裂
3)根节点成为3节点,插入结束
以上的插入,树的高度都没有变化。下面说一种树的高度会+1的状况。
好比在这棵树中插入32。
删除的状况会复杂一些,下面分几种状况来讲。
直接删除便可。以下图12可直接删除。
删除后,3节点成2节点。
这里须要区分临近兄弟节点的类型。先将节点删除。
好比在这颗树中删除7。
兄弟节点为2节点,这时须要判断父节点类型
a. 父节点为3节点 此时兄弟节点不够借,父节点降元,从3节点变成2节点,与兄弟节点合并。
好比从这棵树中删除36。
30与18合并,3节点变成2节点,删除完成
b. 父节点为2节点 将父节点和兄弟节点合并,造成新的节点,这是把新节点当作当前节点,不断套用上述几种状况进行调整,直至平衡。这种状况下,若根节点是2节点,树的高度会减1。
例1 从这颗树中删除12
[8,9]
)的父节点和兄弟节点都为2节点,还需进行合并(即节点2
,5
合并)合并完成以下图。
例2 从这棵树中删除30
节点30的父节点和兄弟节点为2节点,进行合并,此时[22,25]为新节点。
把[22,25]看做当前节点,因为其兄弟节点为2节点,父节点为3节点,套用2.a
中的状况,父节点降元,调整完成。
将该节点与其前驱
或后继
节点交换,而后删除交换后的叶子节点,此时转换成上一种状况的处理。
使用中序遍历的顺序,前驱就是指其前一个节点,后继是指其后面的一个节点。最直接的定位以下:
好比下图,5的前驱是3,后继是7。
那为何要用前驱/后继节点交换呢?
由于,用前驱/后继节点交换后,才能保持大小顺序。后继节点是右子树中最小的节点,与父节点交换后,排除待删除的叶节点,仍保持左子树<新父节点<右子树的关系。同理,前驱节点是左子树中最大的节点,交换后,仍能保持。
这里咱们使用后继节点
来进行替换。
从这颗树中删除节点5。
因为5的后继节点是7,先将值进行交换。这时候目的就是删除叶子节点5
,因而能够转换成其父节点和兄弟节点都为2节点
的状况进行调整。
红黑树也是一种二叉平衡树,它知足以下几个特性(根据算法中的定义):
这个定义可能跟咱们日常看到的不太同样,因为是以2-3树来理解红黑树,定义红链在左边,这样才能跟2-3树彻底对应上。
AVL
是一种极度平衡的二叉树,那为何不用AVL呢?由于AVL
插入删除要保持平衡,相比红黑树要慢一些,须要左旋右旋等等。但实际上它的旋转也只是几个场景的套用,哪些场景须要怎么旋转,理解就好了。
而红黑树是近似平衡的(黑平衡),也就是说它不像AVL
那样绝对的平衡,因此添加/删除节点后的平衡操做没那么多。
因此对于插入和删除操做较多的场景,用红黑树效率会高一些。
主要思想:3节点分裂成2节点。
将3节点的第一个元素,做为第二个元素的左节点,并用红色的线链接,此时红色线链接的节点就至关于红色。
将2-3树按照以上思想转换后,就获得了一颗红黑树。用这种方式理解是否是简单多了呢?
同时也有几个问题值得咱们思考:
为何红链规定在左边呢?
我以为是前人的一个约定,为了保持统一,简化处理,都放在左边。那都放右边是否是也能够呢?
没有任何一个节点同时与两个红连接相连
由于一个红链表示一个3节点,若是有2个红链相连,则表示为4节点,不符合2-3树定义。
根节点为黑色
只有3节点的左链才为红色。根节点没有父节点,不可能为红色。
根节点到叶子节点通过的黑色节点数目相同
由于2-3树是完美平衡的。红黑树中通过的黑节点数=其层数。
散列表的冲突处理
map的实现,底层通常会采用红黑树,在节点多的时候效率高。 在节点少的时候,可用链表方式。