I.はじめに
前の記事で私たちは、ステップバイステップで説明「バイナリ」「二分探索木」「AVL-平衡二分木を」左から右へ、互いの上に構築し、。特定のバイナリ検索ツリーでデータが検索ツリーとして編成されている方法を私たちにアイデアを与え、当然のことながら、自然な二分探索木の問題がある-極端な場合には、リンクされたリストに、次回縮重。したがって、それはLL、LRによって、すなわちリバランス、AVL-平衡二分木をリードし、RR、RL 4つの回転動作は、平衡二分探索木を維持します。赤黒木:このセクションでは、我々は、高レベルのツリー構造を整理し続けています。確かに我々はすべて赤黒木の多くの多くの書籍ボーエンは説明しているものの、赤黒木、理解することが困難になるためにどのように色反転のバランスを維持する方法を知っているが、マスターまたは赤黒木になるように、我々はまだ困難な熟達になりたいです。本稿では、赤、黒の木を理解するために開始するには2-3からの木、赤黒木の「アルゴリズム4」の分析を参照してください。、赤、黒の木が理由である理由を理解することは、単にダーウィンの進化論のように、いずれかの種が2-3として木から石から飛び出しません開始。データ構造の開発はまた、生物学的進化論に従っています。ツリーに赤黒木は2-3からの進化の木です。
第二に、2-3ツリー
自然2.1,2-3ツリー
2-3は、完全なバイナリツリー(完全なバイナリツリー)に似ていますが、その名の通り、2-3ツリーは、ノードが3人の子供を持つことができますです。ビジョンのように完璧なバランスを維持するために2-3ツリー。次の要件を備えます。
1つの 2ノードは、1つおよび2つだけの子ノードを有しており、唯一つのデータ項目を含むことができます。 2 3は、三人の子供のノードを有し、左から右に昇順にのみ2つのデータ項目を含んでいなければならない 3空のノードに挿入されたノード上のノードの1つに挿入することができない、新しいノードのみ分裂または融合することによって生成することができる
4それだけで完全なバイナリツリー(完全なバイナリツリー)とすることができるのみ2-3 2ノードツリー
どのようなこれらの3つのプロパティの私たちグラフィカルに詳細な理解。
二人の子供を持っている必要があり、ノードが2ノード、及び唯一のデータ項目が含まれています。図1に示される、2つだけのノードと子供のみ2、ノード5、ノード8ノード。及び2は、直接または6つのデータ項目を含むことができます。
3三人の子供のノード、及び左から右へと昇順にのみ2つのデータ項目を含まなければならない: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)は、5つの要素を挿入します。
ノード(要素1)を作成する要素1,2を挿入します。
一旦融合2,1,2要素3つのノードを構成する素子を挿入します。なぜ子供の要素の右ノードとしてノード要素2を生成することはできませんか?ので、「ノードがノードに挿入された空のノードに挿入することができない、新しいノードが唯一の核分裂や核融合によって生成することができます。」
3,1,2,3要素を一時的要素が融合した4ノードを形成するインサート。
これは、2-3ツリーは4ノードが存在することができないためであるスプリットは、ノード4は、その子ノードについてのルートノードとしての中間要素は、それぞれ約2つの要素を分割するように形成されています。次いで、完全二分木の目に見える形。
ノード要素3見出される要素間の大小関係に応じて、要素4を挿入します。新たに挿入されたノードが空のノードに挿入することができないので、4つの要素は、探索木の性質に応じて、最後のノードとの統合があります。すなわち、三要素ノードに融合3,4。4右側の要素に要素3。
融合された要素ノード3,4は、一時的に4ノードを形成する場所を見つけるために三元に、挿入要素4と、エレメント5エレメント5を挿入します。
分割は、ノード4 3,4,5として素子4 3が形成された素子2と、除算演算とノードの上方に配置されています。四つの要素との大小関係に応じて、ルートノードとして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