一、简介
前面的文章我们循序渐进的讲解了《二叉树》《二分搜索树》《AVL-平衡二叉树》,从左至右互为基础。尤其是二分搜索树给了我们如何将数据组织成为搜索树的思想,当然二分搜索树存在的天然问题--在极端情况下回退化为链表。所以引出了AVL-平衡二叉树,通过再平衡即LL,LR,RR,RL四个旋转操作维护了一棵平衡的二分搜索树。本章节我们继续梳理一个高阶的树结构即:红黑树。想必大家都知道,红黑树如何维持平衡,如何进行颜色反转让人很难理解,虽然很多博文很多书对红黑树都有讲解,但是想要掌握或者精通红黑树依然让大家望而生畏。本文,我们借鉴《算法-4》对红黑树的分析,从2-3树入手来理解红黑树。至于为什么从2-3树入手去理解红黑树是有原因的,就像达尔文的进化论,任何一个物种都不会是从石头中蹦出来的一样。数据结构的发展同样遵循着生物进化的理论。红黑树正是从2-3树进化来的一种树结构。
二、2-3树
2.1、2-3树的性质
2-3树类似于一棵完美二叉树(满二叉树),不过就是2-3树允许一个节点有三个孩子,正如其名字一样。2-3树为了维持这种完美的平衡性的愿景。具备如下要求:
1 2节点有且只能有两个孩子节点,并只能包含一个数据项。 2 3节点必有三个孩子,并只能包含两个数据项,从左至右依次递增 3 插入节点时不能将该节点插入到一个空节点上,新的节点只能通过分裂或者融合产生4 当2-3树只有2节点的时候,其只能是一棵满二叉树(完美二叉树)
我们通过图解详细理解一下以上三个性质。
2节点必有两个孩子节点,并只能包含一个数据项:如图1所示,2节点只能有且只能有2个孩子节点,5节点和8节点。并且2直接只能包含一个数据项即6;
3节点必有三个孩子,并只能包含两个数据项,从左至右依次递增:如图2所示,5 < 6< 7< 8< 9;
插入节点时不能将该节点插入到一个空节点上,新的节点只能通过分裂或者融合产生:关于这一条的解释,我们下面通过2-3树的插入操作好好体会。
2.2、2-3树的插入
本章节从2-3树的插入理解“插入节点时不能将该节点插入到一个空节点上,新的节点只能通过分裂或者融合产生”这句话。
在前文《二分搜索树》的分析中我们可知,当我们依次插入(1, 2, 3, 4, 5, 6, 7, 8 ...)连续节点时,二分搜索树将退化为链表。本章节我们看看2-3树是如何保持平衡的。
如上图3所示,我们一次插入(1,2,3,4,5)五个元素。
插入元素1,创建一个2节点(元素为1)。
插入元素2,1,2元素融合暂时形成一个3节点。为什么2元素不能生成一个节点作为1元素所在节点的右孩子?因为“插入节点时不能将该节点插入到一个空节点上,新的节点只能通过分裂或者融合产生”
插入元素3,1,2,3元素暂时融合形成一个4节点。
分裂,因为这是一棵2-3树,不能存在4节点,所以暂时形成的4节点要进行分裂,将中间的元素作为根节点,左右两个元素各为其左右孩子节点。这时可见形成了一棵满二叉树。
插入元素4,根据元素的大小关系其将会找到元素3所在的节点。因为新插入的节点不能插入到一个空节点上,所以4元素将根据搜索树的性质找到最后一个节点与其融合。即3,4元素将融合为一个三节点。并且4元素要位于3元素的右侧。
插入元素5,同插入元素4,元素5一路查找到3,4元素所在的三节点,与其融合,暂时形成一个4节点。
分裂,3,4,5元素所在的4节点同上面1,2,3元素形成的4节点一样,进行分裂操作。根据大小关系,4元素将会作为根节点,3,5元素各为其左右孩子节点。
融合,前面的分裂操作已经导致该2-3树不满足其第四条性质“当2-3树只有2节点的时候,其只能是一棵满二叉树(完美二叉树)”,所以该2-3树将要向上融合以满足2-3树的性质。我们只需要将4元素所在节点与其父节点即元素2所在的节点进行融合即可。这时,2,4元素就形成了一个3节点。
继续插入,6,7元素,最终形成的2-3树如上图4所示。可见,2-3树维护平衡的机制是如此的神奇。整个过程我们是一次插入了(1,2,3,4,5,6,7)7个元素,如果是二分搜索树,该二分搜索树将会退化为一个链表,但是2-3树却神奇的生成了一个满二叉树。
三,红黑树
前面恶补了2-3树的知识,接下来我们就进入正题。
红黑树的定义:
1 每个节点或者是红色的,或者是黑色的; 2 根节点是黑色的; 3 每个叶子结点(红黑树中叶子节点为最后的空节点)是黑色的; 4 如果一个节点是红色的,那么他的孩子都是黑色的; 5 从任意一个节点到叶子节点经过的黑色节点是一样的。
上面的5点定义是建立在红黑树是一个二分搜索树的基础上的。所以红黑树具备二分搜索树的所有性质。
看着定义是不是感觉无法理解红黑树?大部分的教材或者博文中对红黑树的讲解都是生硬的根据这5条定义开始。真的是一头雾水,不知所以然。接下来我们根据2-3树的插入过程结合红黑树的性质,看看红黑树的旋转和变色过程。
3.1、2-3树到红黑树的转换规则
前面详细介绍了2-3树的分裂和融合过程,本小节,我们来看看2-3树向红黑树转换的过程。2-3树有两类节点,1节点和2节点。还有一个临时的节点3节点。下面看看2-3树的这三种节点对应于红黑树的节点情况。
如上所示:
1节点:对应于红黑树的黑色节点。
2节点:对应于红黑树黑色的父节点和红色的左孩子节点。
3节点:对应于红色的父节点和黑色的左右孩子节点。这里需要说一下,为什么是红色的父节点而不是黑色的呢?主要是因为2-3树的3节点需要将分裂后的父节点进行向上融合,红色的符合我们向红黑树中插入任何一个节点默认都是红色的实现方式(后面会介绍)。如果该父节点是红黑树的根节点的话,那他肯定需要变色,这一点就不属于2-3树向红黑树的变换规则了,而属于红黑树的性质。
3.2、红黑树的旋转,变色和颜色反转
本节是本文的重点,前面介绍了这么多关于2-3树的知识,都是为本节做铺垫,如果屏幕前的你看到了这里请继续保持耐心。
下面我们向2-3树和红黑树依次插入(1,2,3)三个元素来看看旋转,变色和颜色反转的过程。
在此之前我们需要清楚红黑树的一个性质:根节点必须为黑色的。一个实现红黑树的规则:新插入的节点永远为红色。
插入1:
如上所示,插入元素1。2-3树就是一个1节点不需要做任何改变。根据红黑树添加的规则:新插入的节点为红色,所以1元素的节点为红色。根据红黑树的性质:根节点必须为黑色。1元素的节点需要进行变色。
插入2:
如上所示,插入元素2.2-3树会形成一个2节点。根据2-3树向红黑树变换的规则,需要变为2元素所在的节点为黑色的父节点,1元素所在的节点为红色,并为2元素的左孩子节点。
左旋:但是根据二分搜索树的性质,插入的2元素会成为1元素的右孩子,这时我们需要对1元素进行左旋转,然后得到如上左旋后的结果。
变色:这时再将2元素换成1元素的颜色,然后将1元素变为红色。这样的变色是有原因的,首先为了向上兼容,该子树的根节点需要始终保持原来的颜色,即将新的根节点2换成原来的根节点的颜色。其次根据2-3树的2节点向红黑树转换的规则,我们需要将1节点的颜色变为红色。
关于左旋和变色代码实现如下
1 ////////////////////////////////////////// 2 // y x // 3 // / \ 左旋转 / \ // 4 // T1 x ---------> y T3 // 5 // / \ / \ // 6 // T2 T3 T1 T2 // 7 ////////////////////////////////////////// 8 private Node leftRotate(Node y){ 9 Node x = y.right; 10 Node t2 = x.left; 11 12 y.right = t2; 13 x.left = y; 14 15 x.color = y.color;// 为了向上兼容,将新的根节点变成老根节点的颜色 16 y.color = RED; // 将被旋转的节点颜色置为红色。 17 18 return x; 19 }
对于上面变色的过程大家可能会想为什么变色过程是这样的而不是根据红黑树的性质,将2元素变为黑色?如果是这样,1,2都是黑色的。从根节点2触发,到所有叶子节点锁经过的黑色节点数就不对了。所以说,我们需要颜色交换而不是简单地将父节点变黑。
插入3:
插入3:
如上所示,插入3元素,对于2-3树会形成一个临时的3节点,然后进行分裂。红黑树中新插入的节点都是红色的,所以,这时会形成一个根节点为2。左右孩子都是红色的节点。
颜色反转:根据2-3树向红黑树变化的规则,并不满足。需要进行颜色反转,即将1,3元素变为黑色,2元素变为红色。至于为什么这样进行颜色反转,原因很简单,因为在2-3树中,三节点分裂后需要向上融合。
变色:这时2元素为根节点,根据红黑树的性质,需要进行变色即可。假如2元素不是根节点,需要向上融合,如2.2章节描述。
颜色翻转相关代码
1 /** 2 * 颜色翻转 3 * @param node 4 */ 5 ///////////////////////////////////////// 6 // 黑 红 // 7 // / \ ------> / \ // 8 // 红 红 黑 黑 // 9 //////////////////////////////////////// 10 private void flipColors(Node node){ 11 node.color = RED; // 置为红色,为了向上融合,在2-3树中,3节点分裂后的根节点要向上融合 12 node.left.color = BLACK; 13 node.right.color = BLACK; 14 }
上面我们详细介绍了依次插入(1,2,3)三个元素的过程,并且这个过程详细覆盖了红黑树的旋左转,变色,颜色反转的过程。这时就差一种右旋的操作,是因为我们插入的数据的问题没遇到右旋的情况,这时如果我们依次插入(1,2,3)就会遇到右旋的情况了。
先插入3,2元素,会形成上面的红黑树。
继续插入1元素,这个变化的过程如上所示,详细过程我们就不详细介绍了。我们简单说一下右旋的变色过程,旋转之前,该子树的根节点颜色为黑色,为了向上兼容需要将新的根节点即2元素的颜色换成原来的根节点3元素的颜色,然后将3元素的颜色变成红色。
右旋的过程如下代码所示
1 ////////////////////////////////////////// 2 // y x // 3 // / \ 右旋转 / \ // 4 // x T2 -------> z y // 5 // / \ / \ // 6 // z T1 T1 T2 // 7 ////////////////////////////////////////// 8 private Node rightRotate(Node y){ 9 Node x = y.left; 10 Node t1 = x.right; 11 12 y.left = t1; 13 x.right = y; 14 15 x.color = y.color;// 为了向上兼容,将新的根节点变成老根节点的颜色 16 y.color = RED;// 将被旋转的节点颜色置为红色。 17 18 return x; 19 }
总结一下上面依次插入(1,2,3)和(3,2,1)的过程,我们总结一个规律。
右旋:当一个节点的左孩子节点和左孩子的左孩子节点都是红色的时候需要进行右旋。
左旋:当一个节点的右孩子是红色节点并且左孩子不是红色,进行左旋。
颜色反转:当一个节点的左右孩子节点都是红色的时候需要进行颜色反转。
随之延伸出来的一个性质即,在我们的2-3树向红黑树变换的规则下,红色的节点只能出现在一个黑色节点的左孩子处。
注意:以上我们总结的规律,只是建立在我们在2-3树向红黑树变换的过程中,为什么这么说呢?因为当你查阅的资料多了你会发现,红黑树的实现是多种多样的,只要能满足红黑树的5点性质即可是一棵符合要求的红黑树。当然本文我们介绍的变换规则是主流的规则。也是维持红黑树的平衡性最好的一种变换规则。
如果你对左旋或者右旋具体是怎么旋转的请参阅我的另一篇博文《平衡二叉树》中关于旋转的过程,严格来说红黑树的旋转较于平衡二叉树只是多了一个颜色变化的过程,上面我们也有详细的描述。
大家看代码一目了然。
感兴趣的话,依次插入(1,2,3,4,5,6,7),手动画一下如果能得到最终结果如下,说明你对红黑树的理解就没什么问题了。
四、红黑树的实现
关于代码实现,和平衡二叉树的实现思想是一样的,我们就不具体描述了。当然了如果你只是为了面试而阅读本文,恭喜你你可以跳过本章节,基本没有哪个公司会让你手写红黑树。但是手写平衡二叉树或者二分搜索树是有可能的。所以大家可以参阅笔者有关《二分搜索树》和《平衡二叉树》的文章。
如果出于研究的目的你想具体实现,我相信你肯定是具备了平衡二叉树的知识才来手撕红黑树的,所以当你具备了平衡二叉树的知识,以下代码应该是没啥难度的。
1 /** 2 * 描述:红黑树的实现 3 * 4 * @Author shf 5 * @Date 2019/8/1 9:42 6 * @Version V1.0 7 **/ 8 public class RBTree<K extends Comparable<K> , V>{ 9 private static final boolean RED = true; 10 private static final boolean BLACK = false; 11 private class Node{ 12 public K key; 13 public V value; 14 public Node left, right; 15 public boolean color; 16 public Node(K key, V value){ 17 this.key = key; 18 this.value = value; 19 left = null; 20 right = null; 21 color = RED; 22 } 23 @Override 24 public String toString(){ 25 return "key-->" + key + "== value-->" + value + "== color-->" + color; 26 } 27 } 28 private Node root; 29 private int size; 30 31 public RBTree(){ 32 root = null; 33 size = 0; 34 } 35 public int size(){ 36 return size; 37 } 38 public boolean isEmpty(){ 39 return size == 0; 40 } 41 private boolean isRed(Node node){ 42 if(node == null){ 43 return BLACK; 44 } 45 return node.color; 46 } 47 ////////////////////////////////////////// 48 // y x // 49 // / \ 左旋转 / \ // 50 // T1 x ---------> y T3 // 51 // / \ / \ // 52 // T2 T3 T1 T2 // 53 ////////////////////////////////////////// 54 private Node leftRotate(Node y){ 55 Node x = y.right; 56 Node t2 = x.left; 57 58 y.right = t2; 59 x.left = y; 60 61 x.color = y.color; 62 y.color = RED; 63 64 return x; 65 } 66 ////////////////////////////////////////// 67 // y x // 68 // / \ 右旋转 / \ // 69 // x T2 -------> z y // 70 // / \ / \ // 71 // z T1 T1 T2 // 72 ////////////////////////////////////////// 73 private Node rightRotate(Node y){ 74 Node x = y.left; 75 Node t1 = x.right; 76 77 y.left = t1; 78 x.right = y; 79 80 x.color = y.color; 81 y.color = RED; 82 83 return x; 84 } 85 private void flipColors(Node node){ 86 node.color = RED; 87 node.left.color = BLACK; 88 node.right.color = BLACK; 89 } 90 public void add(K key, V value){ 91 root = add(root, key, value); 92 root.color = BLACK; 93 } 94 private Node add(Node node, K key, V value){ 95 if(node == null){ 96 size ++; 97 return new Node(key, value); 98 } 99 if(key.compareTo(node.key) < 0){ 100 node.left = add(node.left, key, value); 101 } else if(key.compareTo(node.key) > 0){ 102 node.right = add(node.right, key, value); 103 } else { 104 node.value = value; 105 } 106 if(isRed(node.right) && !isRed(node.left)){ 107 node = leftRotate(node); 108 } 109 if(isRed(node.left) && isRed(node.left.left)){ 110 node = rightRotate(node); 111 } 112 if(isRed(node.left) && isRed(node.right)){ 113 flipColors(node); 114 } 115 return node; 116 } 117 public void levelOrder(){ 118 levelOrder(root); 119 } 120 private void levelOrder(Node node){ 121 Queue<Node> queue = new LinkedList<>(); 122 queue.add(node); 123 while(!queue.isEmpty()){ 124 Node cur = queue.remove(); 125 System.out.println(cur); 126 if(cur.left != null){ 127 queue.add(cur.left); 128 } 129 if(cur.right != null){ 130 queue.add(cur.right); 131 } 132 } 133 } 134 }
五、红黑树在java中的应用
在java的众多集合类中,仔细研究大家可能会发现,TreeMap,TreeSet,都是红黑树的实现。本质上TreeMap是真正的红黑树的实现,TreeSet是对TreeMap的二次封装。还有一个重要的集合,HashMap,对是他是他就是他,几乎每次面试面试官都会死磕一遍HashMap,除此之外他也是我们日常开发工作中最常用的集合之一。感兴趣的可以研究一下HashMap。在HashMap中当一个索引位置维护的链表长度超过8即转换为红黑树,小于6从红黑树转换为链表。为什么是6,8而不是8,8主要是一个复杂度震荡的问题。
爱国,敬业
参考文献:
《玩转数据结构-从入门到进阶-刘宇波》
《算法4》
如有错误的地方还请留言指正。
原创不易,转载请注明原文地址:https://www.cnblogs.com/hello-shf/p/11364565.html
原文地址:https://www.cnblogs.com/hello-shf/p/11364565.html