红黑树

可能在历史上最负盛名的二叉树:

 

红黑树:对每一个节点都附着了一个颜色,或者是红色或者是黑色。对于红黑树这种数据结构来说,相对是有些复杂的。

我们在《算法导论》中发现红黑树要满足五个条件:

首先红黑树是一颗二分搜索树,这一点与AVL树是一样的。红黑树在二分搜索树的基础上和AVL树一样,添加了一些其他的性质来保证他不会退化成为链表。也就是来保证自己是一棵平衡的二叉树。

1.每个节点或者是红色的,或者是黑色的

2.根节点是黑色的

3.每一个叶子节点(最后的空节点)是黑色的   注意:叶子节点的定义不是左右子树都为空的节点,再向下递归一层,最后的空节点才管他叫做叶子节点

4.如果一个节点是红色的,那么他的孩子节点都是黑色的

5.从任意一个节点到叶子节点,经过的黑色节点是一样的

这就是算法导论对红黑树的五个定义。

 

在此引出另一种数据结构:2-3树      事实上红黑树与2-3树是等价的。

2-3树数据结构和大多数数据结构有一些不同的地方。

大多数数据结构每一个节点相应的是一致的,而2-3树有所不同

 

1.   2-3树满足二分搜索树的基本性质    在满足这种基本性质之上,2-3树不是一种二叉树

2.   2-3树有两种节点,一种节点可以存放一个元素,还有一种节点可以存放两个元素

一种节点和我们普通的二分搜索树一样,它只存在一个元素,有左右两个孩子

它还有另一种节点,这种节点存放了两个元素,相应有3个孩子,在第一个元素左侧,两个元素中间,第二个元素的右侧

和二分搜索树一样这个节点左孩子的值小于a,右孩子大于a

左孩子小于b小于中间孩子小于c小于c

即每个节点或者有两个孩子或者有三个孩子,这也就是2-3树名称的由来

通常称这两种节点为     2节点      和        3节点       

2-3树是一棵绝对平衡的树

绝对平衡:根节点到任意一颗叶子节点所经过的节点数量是绝对相同的

二分搜索树->可能退化成链表

堆->虽然书完全二叉树,由于最后一层叶子节点没有填满,所以不能叫做绝对平衡

线段树->叶子节点分布在最后一层或者倒数第二层

trie,并查集->不是平衡的树结构

avl->平衡二叉树,是对于任意一个节点,左右子树的高度差是不超过1的,所以是比这种绝对平衡条件要宽松的

对于2-3树满足对于任意一个节点,左右子树的高度一定是相等的

 

那么2-3树是如何维持绝对平衡的呢?(其实是和红黑树等价的)

对于一颗空的2-3树添加一个元素很容易

此时这个2-3树是一棵平衡的树

如果我们这时候需要添加37

我们需要从根节点出发来添加这个节点

对于二分搜索树,插入37这个过程将是从根节点42出发,37<42 所以插入到42的左子树,因为42左子树为空,所以37直接成为了42的左孩子

但是对于2-3树来说添加节点不是这个样子,对于2-3树来说添加节点将永远不会添加到一个空的位置

由于37比42小,应该放到42的左子树上,因为42左子树为空,那么此时新节点该融合到在添加的过程中找得到的最后的一个叶子节点上,而叶子节点就是42这个节点。现在整棵树只有一个根节点,这个唯一的根节点也是叶子节点,所以就会产生节点的融合。42本来是个2节点,融合之后成为了3节点。此时2-3树亦然是平衡的。亦然只有一个节点,只不过这个节点变成了一个三节点。

如果向这棵2-3树再添加一个节点,比如说这个节点是12。

12这个节点尝试添加进2-3树,由于12比37要小。应该添加到37的左子树中去,由于37的左子树为空,只能与3节点融合,暂时形成这样一个“4节点”

但是2-3树不能有4节点,只能有3节点,可以将它分裂成子树。一个4节点,转而变成了由3个二节点组成的平衡的树,这样一来现在看上去像一颗二分搜索树,也可以理解成是2-3树。只不过每一个节点都是2节点,同时这棵树亦然保持绝对的平衡。

从一个空树,添加1,2,3各节点都能保持绝对平衡。

再来添加一个18,对于18来说小于37,添加到左子树中去,和二分搜索树一样,又18比12大,我们应该把18添加到12的右子树去,此时12的右子树为空,应该和叶子节点融合。2节点融合成3节点即可。此时依然绝对平衡。

再来添加一个节点6,从根节点出发,6比37小,所以应该添加到37的左子树上,对于左子树是12,18包含2个元素的3节点。6比12要小,所以他要添加到3节点的左子树中去,不过对于3节点的左子树为空,不会添加到空节点,而是找到最后添加的叶子节点融合,如果叶子节点是3节点就会暂时形成4节点。

对4节点进行拆解。拆解成包含3个2节点的子树,此时的2-3树不是一颗绝对平衡的树。对于2-3树来说,如果叶子节点本身已经是3节点了,添加一个新的节点变成4节点。

对于新的4节点拆解成这个形式之后,这棵子树有一个新的根节点12,这个12要向上和上面的父亲节点融合。

进而原来12这个节点的孩子6和18变成新的3节点的左孩子和中间孩子。依然是一颗绝对平衡的2-3树。

如果添加元素11,则如下图即可。

如果添加元素5,如下图顺序执行

形成一个这样的4节点,这个4节点还要进行分裂。把这个四节点的3个元素化成3个二节点。亦然保持绝对平衡。

在这个过程中其实是一个很极端的情况,添加的第一个节点其实是42之后是37,18以此类推。

第一次添加的节点是整棵树中最大的元素,后续添加的元素都比这个最大的元素小,也就是我们一直在向着最大的元素的左侧添加元素。如果使用的是二分搜索树,在就非常偏斜,不过2-3树非常神奇的维护了整棵树绝对平衡。不管我们怎么添加,2-3树整体都保持绝对平衡。

 

红黑树和2-3树是等价的!

 

对于2-3树来说就是包含两种节点的数据结构

而红黑树每一个节点都只能存储一个元素。我们假设让每一个节点都只存储一个元素基于这样方式也可以实现和2-3树一样的逻辑。这样的数据结构就是红黑树。

对于2-3树的2节点非常简单,本身就存有一个元素,和二分搜索树是一致的。像红黑树也是只有一个节点。

复杂的是3节点,是2-3树特有的节点。而想要实现的数据结构每一个节点只有一个元素,我们就使用2个节点来实现这个3节点。如图所示将b,c平行的画下面节点本质上和上面的节点是一致的。

为了表示b,c在原来的节点是3节点本身是放在一起的,将b,c两个连接在一起的边绘制成红色,用特殊的边的颜色来表示。

再把它还原成二分搜索树中的样子。

在2-3树的3节点就等加成二分搜索树的这个样子,其中b是c的左孩子,因为b比c小,为了表示b和c在原来的2-3树是并列的关系,是在一起存放在一个3节点中的,这个边绘制成一个特殊的颜色红色。

普通二分搜索树对边这样的对象是没有相应的类来表示的,红黑树中也没有必要对每两个节点所连接的边实现的特殊的类来表示,可是这个边又是红色的。由于每一个节点只有一个父亲,所以每一个节点和他父亲节点相连接的边只有一根边,我们可以把这个边的信息存放在节点上,我们对这个节点做一个特殊的标识,比如让它变成是红色。

在这种情况下就表示b这个节点和他父亲节点相连接的边是红色的是特殊的边,b这个节点和c这个2-3树中是一个并列的关系,是一起存放在一个3节点的,就巧妙的把特殊的边的信息存放到节点上,也可以表示同样的属性同样的逻辑,而不需要添加特殊的代码来维护节点的边的相互信息。

而这就是红黑树和2-3树等价的原因。实际上进行了特殊的定义,在二分搜索树上用这样的两种方式来表示出了对于二分搜索树上2节点和3节点这两种节点,特殊的地方就是引入了红色的节点,红色节点的意思就是和他的父亲节点一起表示原来在2-3树中的3节点。现在这个二分搜索树就有两种节点了,一种节点是黑节点,就是普通节点。另外就是红色节点。所以这种树就叫做红黑树。 

对于上述红黑树的定义易得,所以所有的红黑树的红色节点都是向左倾斜的。这个结论是定义出来的而不是推导出来的。这是因为对于3节点来说,选择这样的方式,3节点左边的元素当做右边元素的左孩子来看待,左边元素的节点是一个红色的节点,所以红色的节点一定是向左倾斜的。

这样一颗2-3树对于的红黑树如下图所示。

这个红黑树有3个红色的节点,因为原来的2-3树有3个3节点,3个3节点每一个3节点都会产生一个红色的节点。 

首先这棵树依然满足二分搜索树,由于对这棵树进行标记(红色的标记),所以可以定价看做是一棵2-3树。

当然我们可以把他绘制成如下图。

任意的2-3树都可以使用这样的规则转换成2-3树。

根据上图的对比我们来看下算法导论给出的五条性质。

1.每个节点或者是红色的,或者是黑色的

这个就是红黑树的定义,没有什么可解释的

2.根节点是黑色的

对于红黑树来说,本身等价于2-3树,相应的在2-3树中对应的根节点或者是一个2节点,或者是一个3节点。对于2-3树相应的2节点和3节点对于红黑树的节点表示就是两种情况。如果根节点是2节点所对应的在红黑树相应的节点就是左边的a,如果2-3树本身是一个3节点包含有2个元素的话,那么相应的对于红黑树来说,和他等价的节点就变成了右边的b,c,其中b所在的节点变成了c节点的左孩子并且标识成了红色。在这种情况下,红黑树的节点是c,他依然是一个黑色的节点。所以不管在2-3树中对应的是2节点还是3节点,对应到红黑树中这个根节点一定是黑色的根节点。如果理解了在2-3树中红黑树的表现形式,这条就是显而易见的。

3.每一个叶子节点(最后的空节点)是黑色的   注意:叶子节点的定义不是左右子树都为空的节点,再向下递归一层,最后的空节点才管他叫做叶子节点

实际上这条性质与其说是性质,不如说是定义。它相当于再说我们在红黑树中定义空这样的节点本身是黑色的。这条性质本身和上一条相吻合的。对应的也就是说根节点是存在的,在2-3树的2节点的情况或者3节点的情况,对应到红黑树中就是一个叶子节点。不过还存在一种情况,就是空树也是一颗红黑树。对于空树来说,它本身是空,相应它的根节点也是空,那么第二条性质说根节点一定是黑色的,这里空这里也是黑色的,这和第三条性质其实就叠在了一起。也就是说极端的情况下,整棵树都是空的时候这个空它既是叶子节点,又是根节点,在这种情况下我们可以认为他是黑色的节点。

4.如果一个节点是红色的,那么他的孩子节点都是黑色的

只有当在2-3树中的3节点才会出现红色节点,对应的3节点的左侧元素所在节点在红黑树中就是一个红色节点。这和个红色节点的孩子节点,就是在2-3树中的3节点对应的左孩子,红这是中间的孩子,不管是左孩子还是中间的孩子,相应在2-3树中要么是2节点要么是3节点。如果是2节点那么就是黑色的节点,符合要求。如果是3节点,和跟节点一定是黑色的是一样的。所连接的三节点的表现形式是右下角样子,先连接黑色在连接红色,所以一定先连接黑色。这个结论对黑色节点不成立。但黑色节点的右孩子一定是黑色的。这个原因和红色孩子是黑色的原因是一样的。

5.从任意一个节点到叶子节点,经过的黑色节点是一样的

这条是红黑树性质的核心。核心在于红黑树和2-3树是等价的关联。2-3树是一棵绝对平衡的树,而一颗绝对平衡的树意味着从2-3树中的任意一个节点出发,到叶子节点,到根节点的节点数是一样多的。因为所有叶子节点都在同一层上,他们的深度是一致的。而任意节点是有固定深度的,从这个节点到达可以达到的叶子节点,就意味着向下走的深度是一样的,也就是深度是一致的。在2-3树中的这样性质对应到红黑树中,就意味着它走过的黑色节点是一样多的。这是因为在2-3树中无论是2节点还是3节点,转换成红黑树中的节点表示的时候都会有一个黑色的节点。所以从红黑树的任意一个节点出发,每经过一个黑色节点,其实就等于是经过原来2-3树的某一个节点。区别只是在于经过的黑色节点,左孩子还是红色节点,相应在2-3树中原来是一个3节点。这是2-3树到红黑树的等价性质。而根节点也是叶子节点,也就会说红黑树中从根节点出发,到叶子节点的经过黑色节点是一样的。本身是红黑树的一个重要的性质。

所以我们通常说红黑树是保持"黑平衡"的二叉树。而这种黑平衡的二叉树严格意义上来讲不是平衡二叉树。也就是说红黑树的当中左右子树的高度差有可能大于1的。但是红黑树的左右节点的黑色高度差保持绝对平衡。

于是,红黑树的节点个数是n的话,他的最大高度不是logn而是2logn,这是因为在最次的情况下,从根节点触发一直到最深的节点,可能经过logn级别的黑色节点,同时每一个节点的左子树都是红色节点。换句话说这个2-3树所对应的都是3节点。但是2这个数是常数,所以在红黑树复杂度分析来讲依然是O(logn)。使用二分搜索树方式查找元素是O(logn)虽然经历的节点个数可能是2logn。

修改这个元素首先要查找再修改它,时间复杂度也是O(logn)级别的。添加一个元素和删除一个元素也是在这个红黑树上从根节点出发,向下在一条路径上进行遍历,它的时间复杂度都是O(logn)级别的。这就是首先红黑树不会像二分搜索树一样退化成链表的具体原因。对于红黑树增删改查的操作相应的时间复杂度都是O(logn)级别的。

另外红黑树的最高高度是2logn比AVL树要高,所以其实在红黑树上的元素的查找相比AVL树要慢一点,虽然二者都是Ologn级别的,但是这不影响红黑树成为一个非常重要的数据结构,甚至比AVL树还要重要,还要常用。这背后的原因其实就在于红黑树添加操作和删除操作比AVL树要快速一些。对于我们的数据结构,如果要经常进行添加或者删除的变动的话,相应的使用红黑树就是更好的选择。但是如果存储的数据结构几乎是不会动的话,创建好这个数据结构之后的主要操作主要是查询的话,其实AVL树的性能会高一点。

红黑树性能:对于完全随机的数据,普通的二分搜索树很好用。因为完全随机的数据二分搜索树并不会退化成链表,他的高度相对保持的比较好,同时没有很多维持平衡的操作降低了它的开销。但是缺点非常明显,如果应用的场景可能出现极度不平衡的数据(比如说数据按顺序进入二分搜索树)就会退化成链表。这样二分搜索树会高度不平衡,会影响性能。

对于查询较多(包括get,contains,set(修改就是先查找,查找直接修改))的使用情况,AVL树很好用。它采用平衡二叉树这样一个策略。

红黑树牺牲了平衡性(2logn的高度) 红黑树并不完全满足平衡二叉树的定义,最高的高度达到了2logn的级别,这个高度比AVL树要高的。但是相对的,红黑树统计性能更优(综合增删改查所有的操作)对于时间复杂度分析来说,整体是和AVL树一个级别的。但是整体来看增删改查操作的话,整体综合起来,平均性能是比AVL树要好的。尤其是使用的场景要经常使用添加操作和删除操作这种情况下红黑树就是很好的选择了。也因为整体统计性能更优,也就是平均情况更好一些,所以对于很多语言内部的容器类中所实现的有序的映射,比如说Java的treeMap和treeSet底层都是红黑树。

 

在上述案例中,我们使用左倾红节点来表示2-3树中的3节点。这样的实现方式可以叫做左倾红黑树。左倾是相对比较标准的红黑树实现方式,但不是唯一的实现方式。实际上也完全可以使用右倾。如下图所示相应的也可以实现红黑树的逻辑。

 

红黑树的统计性能更优

    对于树来说,有另一种统计性能优秀的树结构Splay Tree(伸展树)

    局部性原理:刚被访问的内容下次高概率被再次访问

伸展树也是一种二叉树,它也可以维持自平衡。不过在这里Splay Tree对于平衡的定义也没有像AVL树那么严格。其更重要的是运用了局部性原理。基于这样的假设,在创建的过程,包括查询修改这些过程中都对这棵树的结构进行一定的变化。 伸展树的本身实现是比红黑树简单的。

 

对于红黑树和AVL树,从用户的角度看就可以抽象的看成是键值这样的数据对的数据结构。基于红黑树可以实现底层是红黑树的Set和Map数据结构。

java.util中的TreeMap和TreeSet基于红黑树基于实现的

 

 

 

 

阅读更多
换一批

没有更多推荐了,返回首页