通过2-3树理解红黑树

一、简介

  前面的文章我们循序渐进的讲解了《二叉树》《二分搜索树》《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

时间: 2024-10-30 00:58:32

通过2-3树理解红黑树的相关文章

通过2-3-4树理解红黑树

前言 红黑树是数据结构中比较复杂的一种,最近与它交集颇多,于是花了一周的空闲时间跟它死磕,终于弄明白并实现了红黑树.写文总结一下,希望能给试图理解红黑树的同学一些灵感,也让我能记得更深刻. 在研究红黑树时吃了不少苦头,原因有二: 红黑树的插入和删除非常复杂,很多人并没有理解或完全实现,或实现了的没有任何注释,让人很难参考: 网络上红黑树的理解方式较为单一,一般是 双黑.caseN 法,而插入和删除的情况很多,每种都有对应的处理方式,如果死记硬背的话,再过一段时间再回忆各种情况可能就一头雾水了.

浅谈算法和数据结构: 七 二叉查找树 八 平衡查找树之2-3树 九 平衡查找树之红黑树 十 平衡查找树之B树

http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html 前文介绍了符号表的两种实现,无序链表和有序数组,无序链表在插入的时候具有较高的灵活性,而有序数组在查找时具有较高的效率,本文介绍的二叉查找树(Binary Search Tree,BST)这一数据结构综合了以上两种数据结构的优点. 二叉查找树具有很高的灵活性,对其优化可以生成平衡二叉树,红黑树等高效的查找和插入数据结构,后文会一一介绍. 一 定义 二叉查找树(B

AVL树,红黑树,B-B+树,Trie树原理和应用

前言:本文章来源于我在知乎上回答的一个问题 AVL树,红黑树,B树,B+树,Trie树都分别应用在哪些现实场景中? 看完后您可能会了解到这些数据结构大致的原理及为什么用在这些场景,文章并不涉及具体操作(如插入删除等等) 目录 AVL树 AVL树原理与应用 红黑树 红黑树原理与应用 B/B+树 B/B+树原理与应用 Trie树 Trie树原理与应用 AVL树 简介: AVL树是最早的自平衡二叉树,在早期应用还相对来说比较广,后期由于旋转次数过多而被红黑树等结构取代(二者都是用来搜索的),AVL树内

浅谈算法和数据结构: 九 平衡查找树之红黑树

原文:浅谈算法和数据结构: 九 平衡查找树之红黑树 前面一篇文章介绍了2-3查找树,可以看到,2-3查找树能保证在插入元素之后能保持树的平衡状态,最坏情况下即所有的子节点都是2-node,树的高度为lgN,从而保证了最坏情况下的时间复杂度.但是2-3树实现起来比较复杂,本文介绍一种简单实现2-3树的数据结构,即红黑树(Red-Black Tree) 定义 红黑树的主要是像是对2-3查找树进行编码,尤其是对2-3查找树中的3-nodes节点添加额外的信息.红黑树中将节点之间的链接分为两种不同类型,

B树、B+树、红黑树、AVL树

定义及概念 B树 二叉树的深度较大,在查找时会造成I/O读写频繁,查询效率低下,所以引入了多叉树的结构,也就是B树.阶为M的B树具有以下性质: 1.根节点在不为叶子节点的情况下儿子数为 2 ~ M2.除根结点以外的非叶子结点的儿子数为 M/2(向上取整) ~ M3.拥有 K 个孩子的非叶子节点包含 k-1 个keys(关键字),且递增排列4.所有叶子结点在同一层,即深度相同 (叶节点可以看成是一种外部节点,不包含任何关键字信息) 在B-树中,每个结点中关键字从小到大排列,并且当该结点的孩子是非叶

算法-树(2)—2-3树,红黑树

本篇文章主要介绍2-3树,并由2-3树重点介绍RB树(红黑树) 后附完整代码 2-3树 1. 2-3树 2-3树概念: 一颗2-3查找树,或为空树,或为由2-结点,3-结点构成的树. 2-结点:含有一个键值对和两个链接,左链接的结点均小于该结点,右链接的结点均大于该结点. 3-结点:含有两个键值对和三个链接,左链接的结点小于该节点的小的键值,中链接介于该结点的两个键值之间,右链接大于改结点的大的键值. 2. 2-3树的查找添加 1).查找: 参照上一篇文章,实现较为简单,即比较需要查找的key值

结合java.util.TreeMap源码理解红黑树

前言 本篇将结合JDK1.6的TreeMap源码,来一起探索红-黑树的奥秘.红黑树是解决二叉搜索树的非平衡问题. 当插入(或者删除)一个新节点时,为了使树保持平衡,必须遵循一定的规则,这个规则就是红-黑规则: 1) 每个节点不是红色的就是黑色的 2) 根总是黑色的 3) 如果节点是红色的,则它的子节点必须是黑色的(反之倒不一定必须为真) 4) 从跟到叶节点或者空子节点的每条路径,必须包含相同数目的黑色节点 插入一个新节点 红-黑树的插入过程和普通的二叉搜索树基本一致:从跟朝插入点位置走,在每个节

高级树、AVL 树和红黑树

高级树.AVL 树和红黑树 二叉树遍历 Pre-order/In-order/Post-orde 前序(Pre-order):根-左-右 中序(In-order):左-根-右 后序(Post-order):左-右-根 示例代码 def preorder(self, root): if root: self .traverse_path.append(root.val) self .preorder(root.left) self .preorder(root.right) def inorder

查找(二):彻底理解红黑树和平衡查找树

平衡查找树 在之前的二分搜索和二叉查找树中已经能够很好地解决查找的问题了,但是它们在最坏情况下的性能还是很糟糕,我们可以在查找二叉树中,每次动态插入或删除某结点时,都重新构造为完全二叉树,但是这样代价太大,所以就引出了平衡查找树. 详细的数学定义就不给出了,因为既不直观也记不住,直接给出一个平衡二叉树的图: 相信这个图一看就明白了,平衡查找树(以下简称BST或2-3查找树),下面给出一些基本的定义: 一棵2-3查找树或为一棵空树,或由一下结点组成: 2-结点,含有一个键(及其对应的值)和两条链接