数据结构之红黑树
1、什么是红黑树?
红黑树和红色和黑色这两种颜色有关,事实上,在红黑树中,对每一个节点都附着一个颜色,或者是红色或者是黑色。红黑树首先是一棵二分搜索树,这一点和AVL树是一样的,红黑树也是一种平衡二叉树,红黑树在二分搜索树中添加了一些其它的性质,来保证红黑树不会退化成链表,来保证自己在某种情况下是一种平衡二叉树。
如果红黑树的节点个数是n的话,相应的最大的高度是2logn。因为在最次的情况下,从根节点出发,一直到最深的那个叶子节点,我们可能经过了logn这个级别的黑色节点,同时,每一个黑色节点它的左子树都是红色的节点,换句话说在,这条路径上所对应的2-3树,都是2-3树的3节点,又有了logn的红色节点。所以最大高度是2logn,又因为2是常数,放在复杂度分析中来说,所以高度依然是O(logn)级别的。
所以查找元素的时间复杂度是O(logn)级别的、修改元素的时间复杂度是O(logn)级别的、删除元素的时间复杂度是O(logn)级别的、新增元素的时间复杂度是O(logn)级别的,因此红黑树不会像二分搜索树一样退化成一个链表。
红黑树的高度比AVL的高,所以红黑树的查询操作比AVL的慢一点,虽然他们的时间复杂度都是O(logn)级别的。但是红黑树的添加元素和删除元素比AVL快。如果存储的数据经常发生添加和删除的变动的话,相应的使用红黑树就是更好的选择,但是如果存储的数据近乎是不会动的话,创建好这个数据结构之后,以后的操作只在于查询的话,AVL的性能会高一点,虽然红黑树和AVL的时间复杂度都是O(logn)级别的。
2、《算法导论》中的红黑树定义,如下所示:
1)、每个节点或者红色的,或者是黑色的。
2)、根节点一定是黑色的。红黑树等价于2-3树,对应于2-3树中,根节点要么是2节点,要么是3节点,在2-3树中,如果根节点是2节点,在红黑树中,就使用黑色节点表示,如果2-3的根节点是3节点,那么对应于红黑树中,就变成了红色节点是黑色根节点的左孩子的情况。所以红黑树中根节点是黑色的。
3)、每一个叶子节点(最后的空节点,不是之前定义的叶子节点,之前定义的左右孩子为空即为叶子节点)是黑色的。如果红黑树是空的,它也是一棵红黑树,红黑树本身是空,那么它的根节点也是空的,那么空的根节点也是黑色的。
4)、如果一个节点是红色的,那么他的孩子节点一定都是黑色的。红黑树中出现红色节点的情况,是在2-3树种是3节点的情况,对应的3节点的左侧的元素所在的节点在红黑树中就是一个红色的节点,那么这个红色的节点的孩子的节点,就是在2-3树中3节点中对应的左孩子或者中间的孩子,不管是左孩子还是中间的孩子,它在2-3树中,要么是2节点,要么是3节点,如果连接的是2节点,很显然2-3树中的2节点就是对应红黑树中的黑色的节点,此时红色的节点的孩子节点一定是黑色的节点。如果连接的孩子节点是一个3节点的话,对应红黑树的表现形式是红色节点是黑色节点的左孩子,所以要先连接上黑色的节点,再连接上黑色节点的红色孩子节点。注意,黑色的节点右孩子一定是黑色的。
5)、从任意一个节点出发到叶子节点(最后的空节点),经过的黑色节点是一样多的。2-3树是一棵绝对平衡的树,从任意一个节点出发到叶子节点,所经过的节点数是一样多的,经过的深度都是一样的,2-3树所有的叶子节点都在同一层上,他们的深度是一致的。对应到红黑树中,其实就对应的走过的黑色节点都是一样多的,这是因为2-3树,不论是2节点还是3节点,转换成红黑树中节点表示的时候,都会有一个黑色的节点。在红黑树中任意一个节点出发,每经过了一个黑色的节点,其实就等于是一定经过了原来的2-3树中某一个节点。区别只是在于经过的这个黑色的节点,如果左孩子还是红色的节点的话,那么相应的其实就经过了原来2-3树中的一个3节点。
注意:红黑树是保持黑平衡的二叉树,是在红黑树中,从根节点搜索,一直搜索到叶子节点,经过的黑色节点的个数是一样的,是黑色的一种绝对的平衡,严格意义上,不是平衡二叉树。平衡二叉树是左右子树的高度差不能超过一。红黑树左右子树黑色节点的高度差保持着绝对的平衡。
3、《算法4》中介绍的红黑树。
1)、红黑树与2-3树是等价的,理解了2-3树和红黑树之间的等价关系,红黑树很好理解。
4、什么是2-3树?
1)、学习2-3树,不仅对于理解红黑树有帮助,对于理解B类树,也是有巨大帮助的。
2)、2-3树依然满足二分搜索树的基本性质,但是在满足这个基础性质的基础上,2-3树不是一种二叉树。
3)、2-3树有两种节点,节点可以存放一个元素或者两个元素。存放一个元素有两个孩子,存放两个元素有三个孩子,三个孩子分别在第一个元素的左侧,两个元素的中间,第二个元素的右侧。
4)、2-3树,每个节点有2个或者3个孩子,这也是2-3树名称的由来。
5)、通常,在2-3树中,存放一个元素有两个孩子的节点叫做2节点,存放两个元素有三个孩子的节点叫做3节点。
6)、2-3树有一个很重要的性质,和2-3树本身插入元素的时候构建的方法是相关的,2-3树是一棵绝对平衡的树,绝对平衡,就是从根节点到叶子节点所经过的节点数量一定是相同的,对于任意一个节点来说,左右子树的高度一定是相等的。
5、2-3树如何维持绝对的平衡,通过向2-3树中添加节点操作来看2-3树如何维持绝对的平衡。通过通过向2-3树中添加节点操作来看2-3树维持绝对的平衡来看红黑树,其实是等价的。
如果此时,新增了第二个节点,比如添加37的话,对于2-3树来说,依然是从根节点出发来添加37这个节点,但是如果对于二分搜索树的话,从根节点42出发,发现节点37小于42,所以插入到42的左子树中,42的左子树又为空,那么37直接成为42的左孩子。但是对于2-3树来说,添加节点不是这个样子的,2-3树添加节点将永远不会添加到一个空的位置,对于42这个根节点来说,其实依然按照二分搜索树的性质来看,由于37比42小,所以应该放到42的左子树中,但是42的左子树为空,那么此时,新节点37将融合到我们之前添加的过程中所找到的最后的一个叶子节点,此时,这个叶子节点就是42这个节点,现在这棵树只有一个根节点,唯一的根几点也是一个叶子节点,所以就会产生节点的融合,由于42本来是一个二节点,通过融合之后,融合的成为了一个三节点。此时,这棵2-3树依然的平衡的,依然只有一个节点,只不过这个节点变成了3节点。
如果此时,再添加一个新的节点12,在这种情况下,12这个元素尝试添加进2-3树中,由于12比37要小,按照二分搜索树应该添加到37的左子树中,不过37的左子树为空,因为在2-3树中添加节点,新的节点永远不会去空的位置,只会和最后找到的叶子节点做融合,最后找的叶子节点是37和43组合的3节点,虽然是一个3节点,此时,也依然是先进行了融合,暂时形成了一个四节点,此时一个节点容纳了三个元素,相应的这个节点可以有四个孩子,但是,对于2-3树来说,最多只能有3节点,也就是一个节点中容纳两个元素,有3个孩子,对于这种四节点来说,我们可以非常容易的将他们分裂成一个子树。
我们可以非常容易的将他们分裂成一个子树,分裂成一个子树,此时由一个四节点转而变成了有三个2节点组成的一棵平衡的树。此时看起来,像一棵二分搜索树,也可以理解成2-3树,只不过每个节点都是2节点,同时,这棵树保持着绝对的平衡。
此时,在这棵树的基础上再添加一个新的节点,这个新的节点是18,由于根节点是37,所以要将18这个节点添加到根节点的左子树中,对于左子树是12,18要比12要大的,应该把18添加到12的右子树中去,此时12的右子树已经是空的,对于2-3树的添加,不会向空的节点位置添加元素,而是和它最后找到的叶子节点进行融合,此时叶子节点是12,12这个节点是2节点,还有空间可以和18融合成3节点,而不会破坏2-3树的性质,此时的2-3树变成了下面这个样子,此时的2-3树依然保持着绝对的平衡。
此时,再添加一个节点6,从根节点出发,6这个节点比根节点37小,所以添加到根节点37的左子树中,此时根节点的左子树是包含12和18元素的三节点,6比12还要小,所以6这个节点要添加到12和18组合成的3节点的左子树中去,由于12和18组合成的3节点的左子树为空,2-3树添加节点来说,不会把一个新的节点添加到一个空的节点上去,而是找到最后添加位置的叶子节点,和这个叶子节点做融合,如果这个叶子节点是3节点的话,那么会暂时形成一个4节点,之后对这个4节点再进行拆解。
那么,这个拆解是怎么做的呢,之前对根节点是4节点的时候进行拆解,是拆解成包含有3个2节点这样的一个子树,但是如果包含6、12、18这三个元素的4节点的这也叶子节点拆分成一个子树的话,此时2-3树不是一棵绝对平衡的树,对于2-3树来说,如果一个叶子节点本身已经是一个3节点了,添加了一个新的节点变成4节点的话,那么对于这个新的4节点,我们拆解成一个子树之后,这棵子树有一个新的根节点12,这个12要向上和上面的父节点进行融合,那么对于12这个新的节点它的父节点是37,37是一个二节点,此时就非常容易了。
此时,将12和37直接进行融合成一个3节点,进而原来12的左右节点6和18就可以变成由12和37组成的3节点的左孩子和中间的孩子,这个2-3树经过刚才的操作,变成了这个样子,这个样子的2-3树依然是绝对平衡的2-3树。
此时,如果再新增一个节点,这个节点是11,对于根节点由12和37组成的根节点来说,11比12还要小,要插入到12的左子树中去,12的左子树是6,6是一个2节点,11比6大,所以要插入到6的右子树中去,但是此时6的右子树为空,所以11要和6进行融合,变成了由6和11组合成的3节点。
此时,如果再新增一个节点,这个节点是5,对于根节点由12和37组成的根节点来说,5比12还要小,要插入到12的左子树中去,12的左子树是6和11组成的3节点,5比6小,所5要插入到6和11组成的左子树中,但是6和11组成的3节点的左子树为空,所以5这个新的元素要和最后找的叶子节点做融合,不过最后找的叶子节点是由6和11组成的3节点了,所以暂时形成了一个4节点,这个4节点暂时变成这个样子。
此时,将这个4节点暂时变成这个样子,将原来4节点的三个元素变成三个相应的2节点,就是一个子树,对于这样的一棵子树,新的根节点是6,节点6就要相应的融合到它的父节点中去,但是节点6的父亲节点又是一个3节点。
但是,直接将节点6和它的父节点进行融合,形成一个暂时的4节点。
此时,对于6这个节点融合进它的父节点3节点之后,变成了一个新的4节点,此时节点6的左右子树就可以挂接在这个新的4节点上面了,成为这个4节点相应的子树,此时,依然没有打破2-3树的性质,但是对于这个4节点,由于在我们的2-3树中,最多只能是3节点,所以4节点还要进行分裂,分裂方式就是将这个4节点划分成三个2节点,形成这样的2-3树。由于此时这个4节点是根节点,划分成这样的根节点之后,也就不需要继续向上去融合新的父亲节点了,因为根节点已经到头了。至此,这次添加操作也就完成了。
此时,这个2-3树所有节点都是2节点,但是它依然满足2-3树的性质,依然保持着绝对的平衡。
6、红黑树和2-3树的等价性,对于2-3树来说,它是包含两种节点的树结构,一种是2节点,另外一种是3节点,2节点中存储的是一个元素,3节点中存储的是两个元素,相应的2节点中就有两个孩子,3节点中就有三个孩子。红黑树每个节点只能存储一个元素,基于这样的一种方式,也可以实现出和2-3树一样的逻辑,这样的一种树结构就是红黑树。
具体的,对于2-3树种的2节点来说,非常简单,因为2节点本身这个节点种只存储一个元素,这和二分搜索树中的节点是一致的,在红黑树中,相应的依然也是这样的一个节点,这个节点只存储一个元素,它有左右两个孩子,这就表示一个2节点,这个非常简单。但是复杂的是3节点,3节点是2-3树中特有的一种节点,对于3节点来说,相应的它包含有两个元素,但是现在想实现的线段树中,每一个节点只能存储一个元素,由于3节点中有两个元素,我们只好使用两个节点来表示这种3节点,相应的表示方式,如下所示:
此时,将b和c这两个节点平行的这样连着来表示,其实对于下面的这个结构,这两个节点来说,它本质上和2-3树的3节点是一致的,相应的b这个元素存在一个节点,c这个元素存储在另外的一个节点中,与此同时,在2-3树中,对于这个3节点中b和c两个元素是有大小关系的,由于b在c的左边,所以b元素小于c元素,相应的,在我们的二分搜索树中,b就应该作为c的左孩子。上面的图示是为了模拟和2-3树的3节点是等价的关系,将b节点和c节点暂时并列画在一起,这样画出来之后,为了表示b和c这两个元素所在的节点在原来的2-3树中是一个3节点,也就是本身在2-3树中是放在一起的,这样的一种并列的关系,所以我将二分搜索树中的b和c这两个节点之间相连的边绘制成红色,用一个特殊的边的颜色来表示,相应的,我们再把它还原成是在二分搜索树中的样子,相应的就变成这个样子。在2-3树种的3节点等价于在二分搜索树中的由红线连接两个节点的样子。
此时,b是c的左孩子,因为b比c小,在这里为了表示b和c这两个元素在原来的2-3树中是一个并列的关系,是在一起存放在一个3节点中的,所以在二分搜索树中b和c元素之间的边绘制成了一个特殊的颜色,是红颜色来表示。但是,我们实现的二分搜索树,其实对边这样的一个对象是并没有相应的类来表示的,那么同样在红黑树中,我们也没有必要对于每两个节点,它们之间所连接的这个边实现一个特殊的类来表示,可是这个边又是红色的,如何来表示特殊的颜色的边呢.由于每一个节点只有一个父亲,换句说话,每一个节点和它父亲节点所相连接的那个边只有一个边,那么,可以将这个边的信息存放在节点上,换句话说,我们把b这个节点做一个特殊的标识,比如让它变成是红颜色,在这种情况下,就表示,b这个节点它和父亲节点相连接的那个边是红色的,它是一个特殊的边,实际上,它的意思就是b这个节点和它的父亲节点c这个节点在原来的2-3树中是一个并列的关系,是一起存放在3节点中的,这样一来,巧妙的将特殊的边的信息存放在了节点上,我们也可以表示同样的属性或者逻辑,而不需要添加特殊的代码来维护节点之间的这个边相应的信息。
其实,我们进行了特殊的定义,在二分搜索树上,用这样的两种方式来表示出来了对于2-3树来说2节点和3节点,这两种节点,在这里特殊的地方就是我们引入了一种红色的节点这样的节点,对于红色的节点它的意思就是和它的父亲节点一起,表示原来在2-3树中的3节点。此时,二分搜索树中就有两种节点了,一种是黑色节点,就是普通的节点,一种是红色节点,就是这里面定义的特殊的节点,所以这种树就要做红黑树。在这里需要注意,在红黑树中,所有的红色几点一定都是左倾斜的,这个结论是定义出来的,因为对于3节点来说,我们选择使用这样的一种方式来进行表示,其中,我们会将3节点它左边的元素来当作右边这个元素的左孩子来看待,与此同时,左边的这个元素所在的节点是一个红色的节点,所以红色的节点就一定是向左倾斜的。
7、红黑树和2-3树的结构对比。
2-3的形式结构,如下所示:红黑树的形式结构,如下所示:
此时,红黑树有三个红色的节点,因为在原来的2-3树中三个3节点,每一个3节点都会相应的产生一个红色的节点,其中在2-3树种17和33是一个3节点,那么在红黑树中17和33就使用红色的线连接起来,其中17是33的左孩子,同时17这个节点是红色的,它代表的是和它的父亲节点相连接的这个边是一个红色的边,是一个特殊的边,这是因为它和它的父亲节点33本身应该是在原来的2-3树中合在一起是一个3节点的,我们就这样来表示。同理,6节点也是红色的节点、66也是红色的节点。同时,这样的一棵树依然满足是一棵二分搜索树,与此同时,我们对一些节点进行了标记为红色,所以可以把它等价的看作是一棵2-3树。
可以将红黑树的结构进行变形,将红色的节点和其父节点横过来连线,横过来连线的两个节点就是2-3树中的3节点。 任意一棵2-3树都可以转换成为红黑树。
8、为什么新初始化一个Node默认的颜色是红色的呢?
2-3树中添加一个节点,添加的那个节点永远是和一个叶子节点进行融合的,与此同时,在红黑树中,红色的节点其实就是代表的它和它的父亲节点本身在2-3树中是在一起的,是融合在一块的,所以,在新创建一个节点的时候,也就是新添加了一个节点的时候,由于我们添加的这个节点总是要和某一个节点进行融合,只不过融合之后可能还要进行拆分,不管怎么样,都是先进行融合,融合之后或者形成一个3节点或者临时的4节点,所以对应的,在我们的红黑树中,我们新创建一个节点,这个节点的颜色总是先将它设置成红颜色的,代表它要在这个红黑树中,和它对应的那个等价的2-3树中对应的某个节点进行融合。
9、红黑树添加新元素。左旋转:
1)、2-3树中添加一个新元素,首先按照二分搜索树得策略去查找新添加元素的这个位置,只不过和二分搜索树的区别在于,在2-3中添加一个元素,添加的这个新的元素永远不会是在空的位置,而会是和我们找到的最后一个叶子节点去融合,如果我们找到的而最后一个叶子节点是2节点,就将新元素直接添加到2节点中,形成一个3节点,这种情况非常容易。如果最后一个叶子节点是3节点的话,那么新添加的这个元素添加到3节点中,暂时形成一个4节点,然后在对这个四节点进行变形处理。
2)、向2-3树中添加元素,都是将新添加的元素融合到2-3树的已有的节点中。在红黑树中,红色的节点其实就是表示的是在2-3树中那个3节点,3节点中有两个元素,这两个元素中,最左侧的那个元素,我们把它设立成红色,它代表的是这个节点和它的父亲节点,这两个节点本身应该合在一起,等价于2-3树中的一个3节点,正是因为这个原因,红黑树中添加新元素,这个新的元素所在的节点永远都是一个红色节点,这就等价于在2-3树中添加一个新的元素的时候,这个新的元素永远是首先要融合进一个已有的节点中。
3)、当然了,在红黑树中添加一个红色的节点之后,有可能会破坏红黑树的基本性质,之后我们再做相应的调整工作,让它继续维持红黑树的基本性质就行了。
但是,红黑树有一个很重要的性质,根节点必须是黑色的。初始化的时候,红黑树为空,添加第一个节点,添加第一个节点为红色,所以此时我们的根节点就是红色的,之后,就要将这个根节点由红色变成黑色的,实际上,不仅仅是在当红黑树为空,添加第一个节点,将这个红色节点变成黑色的,这种情况下有用,在更一般的情况是也是有它的作用的。
如果此时,想向红黑树中添加一个新的元素37,根据二分搜索树插入的方式,37插入到42的左孩子的位置上,相应的,37这个节点是一个红色的节点,这种情况非常简单,就这样插入就行了,此时,它依然是一棵红黑树。这样就对应了在2-3树中的一个3节点,这个3节点中有两个元素37和42。以此类推,在红黑树中添加一个新的元素,添加的这个新的元素是在黑的节点的左子树上的话,直接添加完就好了,因为默认是红色的节点。
但是,如果新添加的元素是在根节点这个黑色节点的右侧,假设根节点元素是37,新添加的这个元素是42,新插入的这个元素是红色的节点,根据二分搜索树添加的原则,这个42插入进来是在根节点37的右侧,此时新添加的元素42是在根节点这个黑色节点的右侧,此时不满足红黑树的基本性质的,因为在红黑树中所有的红色节点都是向左倾斜的,此时,做法和AVL处理一样,需要进行左旋转,将这样一种形状的树通过左旋转变成这个样子的。
此时,做法和AVL处理一样,需要进行左旋转,将这样一种形状的树通过左旋转变成这个样子的。左旋转之后,仍然是37和42这两个元素,现在变成了以42为根节点,37为42的左孩子,同时,37这个节点变成了红色的节点。
左旋转的过程是怎么样的,假设37这个节点称为node节点,此时将node的右孩子称为x,此时node的右孩子x是红色的节点,我们要进行左旋转,左旋转是在node上面进行,这个过程是怎么样得呢。
此时,将node的左子树和x的左右子树都进行了标记,这个左旋转的过程。
首先,让node的右孩子等于x的左子树,就是node.right = x.left。让node的右孩子和x断开这个连接。
然后让x的左子树t2变成新的node的右子树,此时,以x为根的整棵树是node的右子树,所以在这棵树中,所有的节点元素都比node要大,让t2连接上是node的右子树并没有失去二分搜索树的基本性质。
此时,再让x的左子树连接上node,也就是x的左子树变成是以node为根的这棵二分搜索树,也即是x.left = node。此时整棵树变成了这个样子。
此时,从二分搜索树的角度上,就已经完成了左旋转这个过程,但是此时是对红黑树进行操作,还要对节点的颜色进行维护。先来看x的颜色,x的颜色应该等于node的颜色,也就是x.color = node.color,这是因为在原来的左旋转之前的那棵树中,node是根节点,现在x变成了根节点,所以根节点的颜色应该是保持一致的,原来node是什么颜色,现在x就是什么颜色,原来node是黑色,现在x就应该是黑色,有可能原来node的颜色是红色,那么x就变成了红色,之后非常重要的一点,对于node它的颜色应该给它变成红色node.color = RED。这是因为我们新加入的这个节点,在这个例子中,其实新加入的节点是42,它和37这个节点形成一个新的3节点,我们通过左旋转之后,并没有改变这个3节点的两个元素,依然是37和42,这样的两个元素,此时,根据我们红黑树的性质,我们为了表示37和42,它是2-3树中的一个3节点, 现在37这个节点就要标成是红色,也就是原来node这个节点,就要标成是红色,那么,这整个过程就是红黑树的左旋转过程。
上面的x.color = node.color,如果原来node就是红色,那么此时在这x也变成了红色,又使用了node.color = RED,那么此时红黑树就变成了有两个连续的红色节点了,这里需要注意的是,左旋转其实只是一个子过程,在左旋转之后,有可能产生连个连续的红色节点,然后我们会将左旋转之后形成的子树的新的根节点,也就是x这个节点传回去,在传回去之后,在我们的添加逻辑中,还需要进行更多的后续处理。注意,在左旋转的过程中并不维持红黑树的性质,主要做的事情是通过旋转这个过程让37和42,这两个元素对应是2-3树中的一个3节点就行了。
总结,向红黑树中添加新的节点,这个过程中需要使用的辅助过程一个是,保持根节点是黑色的,左旋转,颜色翻转和右旋转。以上的过程,向红黑树中添加新的节点,第一种情况首先红黑树本身是空的时候,我们直接把这个节点添加进来,默认是红色的,然后让它保持是黑色的,保证根节点是黑色的。第二种情况,我们添加的这个新的节点是添加在左右子树都为空的这样的一个黑色的节点上,在这种情况下,如果我们要添加的节点是这个黑色节点的左孩子的话,我们直接添加上去就好了,并没有违背红黑树的性质,但是我们要添加的这个节点要添加到这个黑色节点的右孩子的位置,相应的我们就要进行左旋转,使得它能够继续保持红黑树的性质。这两种情况,对应到2-3树中,其实相当于是我们将一个新的元素放进一个2节点中,把这个2节点转换成了一个3节点,对应在红黑树中的操作。
10、接下来,我们看,如何向红黑树中的3节点添加元素, 类比2-3树,就是向2-3树中的3节点中添加元素,对应在红黑树中会发生什么样的情况呢。
颜色翻转filpColors:此时,红黑树中有42黑色节点和37红色节点,其实对应了2-3树中的一个3节点,此时,向这棵树中添加一个新的节点66,新的节点66默认是红色的,根据二分搜索树添加节点的规则,66就应该添加到42的右孩子中。
此时,对应在2-3树中是一个3节点,这个3节点包含37和42两个元素,现在在这个3节点上添加了一个元素66,在2-3树种会形成一个临时的4节点。实际上,现在在红黑树中的形状就对应了2-3树中的一个临时的4节点的样子,可以想象一下,在红黑树中,所谓的红色节点的意思,它和它的父亲是融合在一起的,现在42这个父亲节点它的两个孩子节点37和66都是红色节点,说明这两个节点都和它自己融合到一起的,所以可以把它理解成2-3树的4节点。在2-3树中,临时的4节点,处理方式是将这个临时的4节点拆分成这样一个由三个2节点组成的一个子树。对应在红黑树中,三个2节点,在红黑树中代表的就是都是黑色的节点,每一个黑色的节点,如果它的左侧没有红色的节点的话,它本身就代表一个单独的2节点,所以在这种情况下,对于红黑树中,我们要做的非常简单,节点不需要进行旋转,只需要改变颜色就行了,改变颜色就是让42这个父亲节点它的两个孩子节点都变成黑色的节点。这个样子就相当于是2-3树中最右侧这种临时的4节点变成的由三个2节点组成的子树。
此时,还需要注意,在2-3树中添加元素的时候,一个临时的4节点变成了三个2节点组成的子树之后,根节点要继续向上去和他的父亲节点做融合,那么这个融合意味着什么,意味着我们新的根节点在红黑树中,要变成一个红色的节点,红色的节点才表示它要和它的父亲节点去融合,至此,我们就处理完了这种情况。我们在2-3树中,37和42添加了一个元素66,相应的,在红黑树中就是这样一个过程。原来,在红黑树中,42是黑色的,37和66是红色的,经过一系列操作之后,所有节点的颜色正好反过来了,42变成了红色,37和66变成了黑色,这个过程就称为颜色翻转filpColors。在红黑树中添加一个新元素,实际上是等价的在2-3树中的3节点中添加一个新元素,对应的一个辅助的过程。
右旋转:我们向红黑树中的,等价于2-3树中的3节点中添加元素的另外一种情况。此时在红黑树中,依然是两个节点42和37,对应2-3树中的一个3节点,现在要向这棵红黑树中添加一个12这个节点元素,根据二分搜索树的规则,12会添加到37的左孩子中。
此时,这样的形状等同于2-3树中本身一个3节点由37和42组成的节点,新增了一个元素12,只不过它的形状和第一种情况不一样而已,现在的这个形状,放在红黑树中表示相当于是42的左孩子和它左孩子的左孩子都是红色的节点,这样的一种形状,也可以理解成是一个临时的4节点,这是因为依然是红色的节点表示它和它的父亲节点是融合在一起的,所以12和37是融合在一起的,与此同时,37也是红色的,它和42也是融合在一起的,这种形状也表示对应于2-3树中的一个临时4节点,那么对于这种4节点,在2-3树中,我们的处理方式都是要把它变成由三个2节点组成的一个子树,那么放在红黑树中,现在这样的一个形状,我们怎么把它变成这样的一种由三个2节点所组成的子树呢,此时需要引入右旋转。
右旋转和左旋转类似,此时,只需要将42这个节点进行一下右旋转的过程,此时将42和37的右子树画上去,对于12也有可能有左右子树,不过对于12它的左右子树根本不会动,所以这里忽略即可。此时将42这个根节点假设是node的话,node的左孩子叫做x。
首先,让node的左孩子等于x的右子树,就是node.left = T1,T1也就是node左孩子x的右子树x.right,此时,node就和x断开了连接,与此同时,T1接到了node的左子树的位置,在这里,依然是37、12和T1都在node的左子树中,由于二分搜索树的性质,它们的值都应该是小于42的,此时,将T1连接到42的左子树中,依然保持着二分搜索树的性质。
之后,要做的是x的右子树等于node节点,就是x.right = node,此时就完成了右旋转的过程,因为这个42原来是37的父亲,现在是37的右孩子,相当于是顺时针旋转了一下,在x这个节点的角度看,是向右旋转了一下。但是对于红黑树来说,还要维护一下颜色的信息,这个维护的过程和左旋转维护的过程是一样的。
首先,现在对于这棵树来说,新的根节点是x这个节点,那么x这个节点的颜色应该和以前的根节点node这个节点的颜色是一样的,也就是x.color = node.color,这个例子中,原来node的颜色是黑色的,现在x的颜色也要变成黑色。另外一方面,现在node变成了x的右孩子了,在这里需要注意,经过这样的处理之后,其实本质上,12、37、42对应在2-3树中还是一个临时的4节点,所以在这里,我们还要让42也就是node的颜色变成是红色,就是node.color = RED,表示它和它的父亲节点这个x是融合在一起的。此时就完成了整个右旋转过程,在完成这个右旋转的过程中,现在这棵子树根节点变成了x,并且它临时的4节点的样子变成了这个x的左右孩子都是红色的节点这个样子,这种情况就是颜色翻转的开始情况。
这个形状就是上面进行颜色翻转的这种情况,处理这种情况,把它变成对应在2-3树中由三个2节点组成的子树,方法就是再运行一下颜色翻转的过程,就好了,所以对于这种情况,我们处理的过程分成了两步,第一步是进行右旋转,第二步是颜色翻转。 注意,上面的步骤就是颜色翻转的开始步骤,此时再按照颜色翻转进行走一遍,就变成了下面的过程。
11、红黑树三种方式新增元素总结。向红黑树中添加新节点,等价于向2-3树的3节点上融合一个新的元素,对应这个过程在红黑树中是如何操作的呢,有三种形式。向红黑树添加新元素,等价于是在2-3树中向一个3节点中融入一个新的元素,对应可能的三种情况。远远比在2-3树中向2节点中融入一个元素复杂的多,在2节点融入元素,直接放入进去就行了。对于红黑树就是新增加一个红色的节点,如果这个红色节点右倾的话,左旋转一下就好了。
1)、第一种方式,红黑树添加新元素,此时红黑树中有两个节点元素37和42,对应于2-3树中的三节点,相应的,此时如果添加一个元素66的话,66会放到42的右孩子的位置,此时红黑树是如何操作的呢?
在这样的情况下,红黑树应该如何做操作呢,这样的添加操作,其实相当于是在2-3树中对一个3节点添加成一个临时的4节点,我们新添加的这个元素其实相当于是在原来的3节点中本来有两个元素,比这两个元素中最大的那个元素还要大,这样的一种情况。
2)、第二种方式,红黑树添加新元素,此时红黑树中有两个节点元素37和42,对应于2-3树中的三节点,相应的,此时如果添加一个元素12的话,12会放到37的左孩子的位置,此时红黑树是如何操作的呢?
这种情况,其实就相当于在2-3树中37和42这个3节点中,融合一个新的元素12,融合的这个元素比3节点中的两个元素中最小的元素37还要小,这样的一个元素融合进去,向2-3树中的3节点融合一个新的元素。
3)、第三种方式,红黑树添加新元素,此时红黑树中有两个节点元素37和42,对应于2-3树中的三节点,相应的,根据二分搜索树的添加策略,此时如果添加一个元素40的话,40会放到37的右孩子的位置,此时红黑树是如何操作的呢?
此时,相当于是向2-3树的3节点添加一个40这个节点元素,相当于是在37和42组成的3节点的中间位置融入了一个40这个节点元素,对于这种情况,后期如何处理呢。
这种过程使用左旋转、右旋转和颜色翻转就可以实现了,首先基于37这个节点进行一次左旋转。
此时,已经变成了之前处理过的情况,其实相当于是把37这样一个新的元素添加进了40和42等价的3节点中,此时针对42这个节点进行一次右旋转,旋转的结果是以40为根。
此时,在红黑树中添加要改变一下,因为红黑树的规则根节点必须是黑色的,变成了这样的。
这样的一个红黑树,为了维持红黑树应有的性质,我们还要进行一次颜色翻转,最终变成这个样子的。
4)、总结,向红黑树中添加一个元素的三种情况,尤其是等价的是在2-3树中的3节点添加一个元素。从最复杂的开始,新添加的节点是在对应于2-3树的3节点这两个元素中间。
第二步,对于这个形状,我们要做的事情,其实就是37这个红色节点,右孩子40还是红色节点,对这两个红色节点我们需要进行一个以37为节点的左旋转。
第三步,此时,需要对以黑色节点42进行右旋转。
如果我们是在等价于2-3树中的3节点添加一个元素,它比3节点中两个元素都要小的元素的时候,此时,这个过程就是直接从开始出发,到进行右旋转的过程,再进行颜色翻转。
如果我们是在等价于2-3树中的3节点添加一个元素,它比3节点中两个元素都要大的元素的时候,此时,这个过程就是直接从开始出发,再进行颜色翻转。
这三种方式可以使用一个逻辑链条将他们穿起来,区别只是在于我们添加了这个元素,形状如果不符合前面这个样子的话,我们直接跳过去就好了,直接跳到后面的过程中,所以我们在红黑树中添加一个新的元素,添加完这个新的元素之后,我们需要维持红黑树的性质,维持的过程其实就是按照这样的一个形状来看一下当前对于这个元素w我们需不需要左旋转,如果需要左旋转就进行左旋转,在左旋转之后或者就不需要进行左旋转的话,我们再来看一下是否需要进行右旋转,如果需要就进行旋转,如果不需要右旋转或者在这次右旋转之后,再看一下是否需要进行颜色翻转,就行了。同样的这样的逻辑链条也适合于等价于2-3树的2节点中融入一个新的元素,因为在2节点中融入一个新的元素,如果融入的这个新的元素红节点是左倾的,这条链条中,一个步骤都不需要走,他不满足任意一个形状,直接结束即可,但是如果是右倾的,其实是满足第一步之后的情况的,在这里我们判断是否左旋转,其实只要判断一下对于我们当前要处理的节点,它的右孩子是否是红节点,如果右孩子是红节点都进行一下左旋转,这样的逻辑链条可以帮助我们在添加完一个新元素后维持红黑树的性质。维护的时机和AVL树是一样的,都是先使用二分搜索树的基本逻辑,把我们这个新的节点添加进红黑树中,之后,添加节点后回溯向上维护,再来进行红黑树性质的维护,在维护完成以后,我们会将维护后新的根的节点返回给我们递归调用的上一层,从上一层的角度再来看我们是否需要来维护新的节点,依次类推。
红黑树的代码,如下所示:
1 package com.redBlackTree; 2 3 4 import com.tree.BinarySearchTree; 5 6 import java.util.ArrayList; 7 import java.util.Random; 8 9 /** 10 * 红黑树的实现 11 * 1、红黑树的性能总结。 12 * 如果顺序的添加数据,对于二分搜索树就退化成链表了,对于完全随机的数据,普通的二分搜索树很好用, 13 * 此时二分搜索树也不会退化成一个链表,它的高度相对可以保持的比较好,同时, 14 * 二分搜索树内部没有很多维持平衡的复杂操作,降低了开销。缺点就是极端情况退化成链表或者高度不平衡。 15 * <p> 16 * <p> 17 * 2、AVL树,对于查询较多set、get、contains等等包含查询的的使用情况,AVL树很好用,采用了平衡二叉树的策略。 18 * <p> 19 * <p> 20 * 3、红黑树一定程度上牺牲了平衡性,红黑树最高高度是2logn的高度,比AVL树的高度高,红黑树并不完全满足平衡二叉树的定义, 21 * 注意:红黑树的统计性能更优,(综合增删改查所有的操作,平均性能比AVL树好)。 22 * 如果经常需要进行新增和删除操作,就可以使用红黑树,比AVL树好。 23 * <p> 24 * <p> 25 * Java语言内部的容器类中,所实现的有序的映射,比如TreeMap、有序的集合TreeSet底层都是使用的红黑树。 26 * 这里强调有序,因为红黑树本身还是一个二分搜索树。二分搜索树一个重要的性质就是有序性。 27 * 可以方便的在二分搜索树中找到最小值,最大值,一个值的前驱和后继,等等这些操作,这些操作 28 * 对于红黑树来说也是支持的。 29 */ 30 public class RedBlackTree<K extends Comparable<K>, V> { 31 32 // 提前声明好红色或者黑色的值 33 private static final boolean RED = true;// true代表是红色。 34 private static final boolean BLACK = false;// false代表是黑色。 35 // 每一个叶子节点(最后的空节点)是黑色的。 36 37 // 红黑树的节点类,私有内部类。红黑树的节点的变化,需要有红色节点和黑色节点。 38 private class Node { 39 public K key;// 存储的键值对的键 40 public V value;// 存储的键值对的值 41 public Node left;// 指向左子树,指向左孩子。 42 public Node right;// 指向右子树,指向右孩子。 43 44 // 红黑树区分与二分搜搜书的是节点的变化 45 public boolean color;//或者是红色或者是黑色的,所以只需要一个布尔类型的变量即可。 46 // 判断节点是否为空,如果为空,那么就返回是黑色的。 47 48 /** 49 * 含参构造函数 50 * 51 * @param key 存储的键值对的键 52 * @param value 存储的键值对的值 53 */ 54 public Node(K key, V value) { 55 this.key = key;// 存储的键值对的键 56 this.value = value;// 存储的键值对的值 57 left = null;// 左孩子初始化为空。 58 right = null;// 右孩子初始化为空。 59 // 构造函数中,默认是使用红色 60 color = RED;// 红黑树的节点颜色默认是红色的。 61 } 62 } 63 64 private Node root;// 创建一个根节点 65 private int size;// 声明红黑树的大小 66 67 /** 68 * 无参构造函数 69 */ 70 public RedBlackTree() { 71 root = null;// 初始化根节点为空 72 size = 0;// 初始化红黑树的大小为0 73 } 74 75 76 /** 77 * 返回红黑树的大小 78 * 79 * @return 80 */ 81 public int getSize() { 82 return size; 83 } 84 85 /** 86 * 判断红黑树是否为空 87 * 88 * @return 89 */ 90 public boolean isEmpty() { 91 return size == 0; 92 } 93 94 /** 95 * 判断节点node的颜色 96 * <p> 97 * 每一个叶子节点(最后的空节点,不是之前定义的叶子节点,之前定义的左右孩子为空即为叶子节点)是黑色的。 98 * 99 * @param node 100 * @return 101 */ 102 private boolean isRed(Node node) { 103 if (node == null) { 104 return BLACK; 105 } 106 return node.color; 107 } 108 109 110 /** 111 * 左旋转 112 * <p> 113 * // node x 114 * // / \ 左旋转 / \ 115 * // T1 x ---------> node T3 116 * // / \ / \ 117 * // T2 T3 T1 T2 118 * 119 * @param node 120 * @return 121 */ 122 private Node leftRotate(Node node) { 123 // 对传入的node进行左旋转的过程,将旋转之后新的子树的根给返回回去。 124 125 // 记录x节点是node节点的右孩子。 126 Node x = node.right; 127 128 // 左旋转,让x的左孩子变成了node的右孩子。 129 node.right = x.left; 130 // 让node变成x的左孩子。 131 x.left = node; 132 // 此时,按照是二分搜索树就已经左旋转完毕了,现在要处理红黑树的颜色问题。 133 134 // 维持节点的颜色,x是左旋转之后新的根节点,让x它的颜色等于原来的根节点node的颜色。 135 x.color = node.color; 136 // 然后将node这个节点的颜色变成红色。这个node和父节点x形成了2-3树种的3节点。 137 node.color = RED; 138 139 // 最后返回左旋转之后形成的这个新的根节点。 140 return x; 141 } 142 143 144 /** 145 * 颜色翻转 146 * <p> 147 * <p> 148 * 由于节点没有发生变化,原来的根节点还是原来的根节点,只是颜色发生了改变。 149 * <p> 150 * <p> 151 * 针对以node为根的,包括node的左右两个孩子进行颜色的反转。 152 * <p> 153 * <p> 154 * 我们调用这个函数的时候要保证我们以node为根的这棵子树,包括它的左右孩子是满足讲解的形状的。 155 * 调用的时候,需要进行形状的判断。 156 * 157 * @param node 158 */ 159 private void flipColors(Node node) { 160 // node节点的颜色变成红色。 161 node.color = RED; 162 163 // node左右孩子的颜色都变成是黑色 164 node.left.color = BLACK; 165 node.right.color = BLACK; 166 } 167 168 169 /** 170 * 右旋转,对以node为根的子树进行一下右旋转,返回的是右旋转之后新的那个根节点。 171 * <p> 172 * <p> 173 * // node x 174 * // / \ 右旋转 / \ 175 * // x T2 -------> y node 176 * // / \ / \ 177 * // y T1 T1 T2 178 * 179 * @param node 180 * @return 181 */ 182 private Node rightRotate(Node node) { 183 // 首先记录node的左孩子 184 Node x = node.left; 185 186 // 右旋转 187 // 首先将node节点的左孩子执行x节点的右孩子。此时node和x脱离了关系。 188 node.left = x.right; 189 // 此时将x的右孩子执行node节点。 190 x.right = node; 191 192 // 此时已经完成了右旋转的过程,现在开始对红黑树的颜色进行变化 193 // 维持节点的颜色,生成的新的子树的根节点的颜色应该是原来的根节点的颜色。 194 x.color = node.color; 195 // 对于node的颜色变成是红色,表示它也要和父亲节点融合到一起, 196 node.color = RED; 197 198 // 此时,就维护好了对于右旋转过程中,改变的node和x所对应的颜色。 199 // 最后,返回旋转过程新的根节点 200 return x; 201 } 202 203 204 /** 205 * 向红黑树中添加新的元素(key,value) 206 * 207 * @param key 208 * @param value 209 */ 210 public void add(K key, V value) { 211 // 此时,不需要对root为空进行特殊判断。 212 // 向root中插入元素e。如果root为空的话,直接返回一个新的节点,将元素存储到该新的节点里面。 213 root = add(root, key, value); 214 215 // 最终根节点为黑色节点。切记,红黑树的根节点一定为黑色的。 216 root.color = BLACK; 217 } 218 219 220 /** 221 * 向以node为根的红黑树中插入元素(key,value),递归算法 222 * 返回插入新节点后红黑树的根。 223 * 224 * @param node 225 * @param key 226 * @param value 227 * @return 228 */ 229 private Node add(Node node, K key, V value) { 230 if (node == null) { 231 // 维护size的大小。 232 size++; 233 // 如果此时,直接创建一个Node的话,没有和二叉树挂接起来。 234 // 如何让此节点挂接到二叉树上呢,直接将创建的节点return返回回去即可,返回给调用的上层。 235 return new Node(key, value); // 默认插入的是红色的节点 236 } 237 238 239 // 基于要新插入的这个节点对应的这个键,根据key的值来确定插入的位置, 240 // 是在左子树中还是在右子树中,还是修改当前的node的值 241 if (key.compareTo(node.key) < 0) { 242 node.left = add(node.left, key, value); 243 } else if (key.compareTo(node.key) > 0) { 244 node.right = add(node.right, key, value); 245 } else { 246 node.value = value; 247 } 248 249 // 上面的性质都做完之后,就要对红黑树的性质进行维护了,这个维护的过程就是链条的过程 250 // 对于当前的节点,是否需要进行左旋转操作。 251 // 如果当前的节点的右孩子是红色的,并且当前节点的左孩子不能是红色 252 if (isRed(node.right) && !isRed(node.left)) { 253 // 返回的结果就是新的根节点,翻转之后的根节点 254 node = leftRotate(node); 255 } 256 257 // 对于当前的节点,是否需要进行右旋转操作。 258 // 如果当前的节点左孩子是红色的节点,并且,当前节点的左孩子的左孩子也是红色节点 259 if (isRed(node.left) && isRed(node.left.left)) { 260 // 返回的结果就是新的根节点,翻转之后的根节点 261 node = rightRotate(node); 262 } 263 264 // 对于当前的节点,是否需要进行颜色翻转操作。 265 // 当前的左孩子是红色的节点,并且当前的右孩子也是红色的节点 266 if (isRed(node.left) && isRed(node.right)) { 267 // 返回的结果就是新的根节点,翻转之后的根节点 268 flipColors(node); 269 } 270 271 return node; 272 } 273 274 275 public static void main(String[] args) { 276 int n = 20000000;//两千万 277 Random random = new Random(); 278 ArrayList<Integer> data = new ArrayList<>(); 279 for (int i = 0; i < n; i++) { 280 data.add(random.nextInt(Integer.MAX_VALUE)); 281 } 282 283 // 开始测试二分搜索树 284 long startTime = System.nanoTime(); 285 BinarySearchTree<Integer> binarySearchTree = new BinarySearchTree<>(); 286 for (Integer x : data) { 287 binarySearchTree.add(x); 288 } 289 290 long endTime = System.nanoTime(); 291 double time = (endTime - startTime) / 1000000000.0; 292 System.out.println("BinarySearchTree : " + time + " s "); 293 294 295 // 开始测试红黑树 296 startTime = System.nanoTime(); 297 RedBlackTree<Integer, Integer> redBlackTree = new RedBlackTree<>(); 298 for (Integer x : data) { 299 redBlackTree.add(x, null); 300 } 301 endTime = System.nanoTime(); 302 time = (endTime - startTime) / 1000000000.0; 303 System.out.println("RedBlackTree : " + time + " s "); 304 305 } 306 307 }