红黑树算法原理

拜拜、爱过 提交于 2020-02-01 15:58:53

原文:红黑树深入剖析及Java实现,本文修改了原文的一些小错误,如果想看红黑树的Java实现可以到原文去看。
红黑树是平衡二叉查找树的一种。为了深入理解红黑树,我们需要从二叉查找树开始讲起。

BST

二叉查找树(Binary Search Tree,简称BST)是一棵二叉树,它的左子节点的值比父节点的值要小,右节点的值要比父节点的值大。它的高度决定了它的查找效率。在理想的情况下,二叉查找树增删查改的时间复杂度为O(logN)(其中N为节点数),最坏的情况下为O(N)。当它的高度为logN+1时,我们就说二叉查找树是平衡的。
bst

BST的查找操作

T  key = a search key
Node root = point to the root of a BST

while(true){
    if(root==null){
        break;
    }
    if(root.value.equals(key)){
        return root;
    }else if(key.compareTo(root.value)<0){
        root = root.left;
    }else{
        root = root.right;
    }
}
return null;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

从程序中可以看出,当BST查找的时候,先与当前节点进行比较:

  • 如果相等的话就返回当前节点;
  • 如果小于当前节点则继续查找当前节点的左节点;
  • 如果大于当前节点则继续查找当前节点的右节点。

直到当前节点指针为空或者查找到对应的节点,程序查找结束。

BST的插入操作

Node node = create a new node with specify value
Node root = point the root node of a BST
Node parent = null;

//find the parent node to append the new node
while(true){
   if(root==null)break;
   parent = root;
   if(node.value.compareTo(root.value)<=0){
      root = root.left;  
   }else{
      root = root.right;
   } 
}
if(parent!=null){
   if(node.value.compareTo(parent.value)<=0){//append to left
      parent.left = node;
   }else{//append to right
      parent.right = node;
   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

插入操作先通过循环查找到待插入的节点的父节点,和查找父节点的逻辑一样,都是比大小,小的往左,大的往右。找到父节点后,对比父节点,小的就插入到父节点的左节点,大就插入到父节点的右节点上。

BST的删除操作

在二叉查找树中删除一个给定的结点p有三种情况:

  1. 结点p无左右孩子,则直接删除该结点,修改父节点相应指针。
  2. 结点p只有左孩子(右孩子),则把p的父节点和p的左孩子(右孩子)相连,然后删除p。
  3. 左右孩子同时存在,找到p的中序直接前驱s,也就是以p的左孩子为根结点的子树中最右的结点,把s的右孩子和p的右孩子相连,p的父节点和p的左孩子相连,然后删除p。
//从二叉查找树中删除指针p所指向的结点 
if(p.right == null) //p的右子树为空   
{  
    p = p.left;
}  
else if(p.left == null) //p的左子树为空   
{  
    p = p.right;
}  
else //左右子树均不空   
{  
    BSTNode s = p.left; //左孩子  
    while(s.right != null) //寻找结点p的中序前驱结点,                      
    {                   //也就是以s为根结点的子树中最右的结点   
        s = s.right;      
    }  
    s.right = p.right; //p的右孩子和s的右孩子相连   
    p = p.left; //p的左孩子和p的父节点相连 
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

RBTree

然而BST的问题是:数在插入的时候会导致树倾斜,不同的插入顺序会导致树的高度不一样,而树的高度直接的影响了树的查找效率。理想的高度是logN,最坏的情况是所有的节点都在一条斜线上,这样的树的高度为N。

基于BST存在的问题,一种新的树——平衡二叉查找树(Balanced BST)产生了。平衡树在插入和删除的时候,会通过旋转操作将高度保持在logN。其中两款具有代表性的平衡树分别为AVL树和红黑树。AVL树追求全局平衡,导致插入和删除性能差,在实际应用中不如追求局部平衡的红黑树(Red-Black Tree,简称RBTree),比如Linux内核中的完全公平调度器、高精度计时器、ext3文件系统等,各种语言的函数库如Java的TreeMap和TreeSet,C++ STL的map、multimap、multiset等,Java 8中的HashMap也用到了RBTree取代过长的链表。

RBTree的定义

RBTree的定义如下:

  1. 任何一个节点都有颜色,黑色或者红色;
  2. 根节点是黑色的;
  3. 父子节点之间不能出现两个连续的红节点;
  4. 任何一个节点向下遍历到其子孙的叶子节点,所经过的黑节点个数必须相等;
  5. 空节点被认为是黑色的。

数据结构表示如下:

class  Node<T>{
   public   T value;
   public   Node<T> parent;
   public   boolean isRed;
   public   Node<T> left;
   public   Node<T> right;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

RBTree在理论上还是一棵BST树,但是它在对BST的插入和删除操作时会维持树的平衡,即保证树的高度在[logN,logN+1](极端的情况下可以出现RBTree的高度达到2*logN,但实际上很难遇到)。这样RBTree的查找时间复杂度始终保持在O(logN)从而接近于理想的BST。RBTree的删除和插入操作的时间复杂度也是O(logN)。
RBTree的查找操作就是BST的查找操作。

RBTree的插入操作

RBTree的插入与BST的插入方式是一致的,只不过是在插入过后,可能会导致树的不平衡,这时就需要对树进行旋转操作和颜色修复(在这里简称插入修复),使得它符合RBTree的定义。

新插入的节点是红色的,插入修复操作如果遇到父节点的颜色为黑则修复操作结束。也就是说,只有在父节点为红色节点的时候是需要插入修复操作的。

插入修复操作分为以下的三种情况

插入操作-case 1

case-1:新插入节点的叔叔节点为红色。

此时操作是将父节点和叔叔节点与祖父节点的颜色互换,这样就符合了RBTRee的定义。下图中,操作完成后A节点变成了新的节点。如果A节点的父节点不是黑色的话,则继续做修复操作
插入操作case1

插入操作-case 2

case-2:新插入节点的叔叔节点为空,且祖父节点、父节点和新节点处于一条斜线上。

此时操作是将B节点进行右旋操作,并且和父节点A互换颜色。通过该修复操作RBTRee的高度和颜色都符合红黑树的定义。如果B和C节点都是右节点的话,只要将操作变成左旋就可以了。
插入case2

插入操作-case 3

case-3:叔叔节点为空,且祖父节点、父节点和新节点不处于一条斜线上。

此时操作是将C节点进行左旋,这样就从case 3转换成case 2了,然后针对case 2进行操作处理就行了。case 2操作做了一个右旋操作和颜色互换来达到目的。如果树的结构是下图的镜像结构,则只需要将对应的左旋变成右旋,右旋变成左旋即可。
插入case3

插入操作的总结

插入后的修复操作是一个向root节点回溯的操作,一旦牵涉的节点都符合了红黑树的定义,修复操作结束。之所以会向上回溯是由于case 1操作会将父节点,叔叔节点和祖父节点进行换颜色,有可能会导致祖父节点不平衡(红黑树定义3)。这个时候需要对祖父节点为起点进行调节,向上回溯,直到root节点为止,根据定义root节点永远是黑色的。

RBTree的删除操作

删除操作首先需要做的也是BST的删除操作,删除后就需要做删除修复操作,使的树符合红黑树的定义,符合定义的红黑树高度是平衡的。

删除修复操作是针对删除黑色节点才有的,当黑色节点被删除后会让整个树不符合RBTree的定义的第四条。需要做的处理是从兄弟节点上借调黑色的节点过来,如果兄弟节点没有黑节点可以借调的话,就只能往上追溯,将每一级的黑节点数减去一个,使得整棵树符合红黑树的定义。
删除修复操作在遇到被删除的节点是红色节点或者到达root节点时,修复操作完毕。

删除修复操作分为四种情况:

删除操作-case 1

case-1:待删除的节点的兄弟节点是红色的节点。

由于兄弟节点是红色节点的时候,无法借调黑节点,所以需要将兄弟节点上升到父节点,由于兄弟节点是红色的,根据RBTree的定义,兄弟节点的子节点是黑色的,就可以从它的子节点借调了。

case 1这样转换之后就会变成后面的case 2,case 3,或者case 4中的一种了。上升操作需要对兄弟节点做一个左旋或右旋操作,然后和父结点变换颜色,兄弟节点的左孩子旋转后作为它原本父结点的右孩子
删除case1

删除操作-case 2

case-2:待删除的节点的兄弟节点是黑色的节点,且兄弟节点的子节点都是黑色的。

因为兄弟节点和兄弟节点的子节点都是黑色的,所以可以将兄弟节点变红,但因为兄弟节点的父结点可红可黑,这个时候需要将父节点A变成新的节点,继续向上调整,直到整颗树的颜色符合RBTree的定义为止。
删除2

删除操作-case 3

case-3:待删除节点的兄弟节点是黑色的节点,且兄弟节点的左子节点是红色的,右子节点是黑色的(这是兄弟节点在右边的情况),如果兄弟节点在左边的话,就是兄弟节点的右子节点是红色的,左子节点是黑色的。我理解是和待删除节点离得近的是红的。

此时操作是对这个红色的结点做旋转上升,然后变换它和它原本父结点的颜色,转换为case-4。
删除3

删除操作-case 4

case-4:待删除节点的兄弟节点是黑色的节点,且兄弟节点的右子节点是红色的(兄弟节点在右边的情况),如果兄弟节点在左边,就是左子节点是红色的。现在是离得远的是红的。

如下图A是待删除节点,此时操作是对兄弟节点D做旋转上升,变换D和父结点B的颜色,D的左孩子C现在变为B的右孩子,D的红色右孩子变为黑色。
删除4

删除操作的总结

红黑树的删除操作是最复杂的操作,复杂的地方就在于当删除了黑色节点的时候,如何从兄弟节点去借调黑节点,以保证树的颜色符合定义。由于红色的兄弟节点是没法借调出黑节点的,这样只能通过旋转操作让他上升到父节点,而由于它是红节点,所以它的子节点就是黑的,可以借调。

对于兄弟节点是黑色节点的可以分成3种情况来处理,第1种情况是:当兄弟节点的子节点都是黑色节点时,可以直接将兄弟节点变红,这样局部的红黑树颜色是符合定义的。但是整颗树不一定是符合红黑树定义的,需要往上追溯继续调整。

第2种情况是:兄弟节点的子节点为左红右黑。第3种情况是:兄弟节点的右子结点是红的(左子结点随意)。我们可以先将第2情况通过旋转变为第3种情况,这时兄弟节点为黑,兄弟节点的右节点为红,可以借调出两个节点出来做黑节点,这样就可以保证删除了黑节点,整棵树还是符合红黑树的定义的,因为黑色节点的个数没有改变。

    原文:红黑树深入剖析及Java实现,本文修改了原文的一些小错误,如果想看红黑树的Java实现可以到原文去看。
    红黑树是平衡二叉查找树的一种。为了深入理解红黑树,我们需要从二叉查找树开始讲起。

    易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
    该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!