原文:红黑树深入剖析及Java实现,本文修改了原文的一些小错误,如果想看红黑树的Java实现可以到原文去看。
红黑树是平衡二叉查找树的一种。为了深入理解红黑树,我们需要从二叉查找树开始讲起。
BST
二叉查找树(Binary Search Tree,简称BST)是一棵二叉树,它的左子节点的值比父节点的值要小,右节点的值要比父节点的值大。它的高度决定了它的查找效率。在理想的情况下,二叉查找树增删查改的时间复杂度为O(logN)(其中N为节点数),最坏的情况下为O(N)。当它的高度为logN+1时,我们就说二叉查找树是平衡的。
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;
从程序中可以看出,当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;
}
}
插入操作先通过循环查找到待插入的节点的父节点,和查找父节点的逻辑一样,都是比大小,小的往左,大的往右。找到父节点后,对比父节点,小的就插入到父节点的左节点,大就插入到父节点的右节点上。
BST的删除操作
在二叉查找树中删除一个给定的结点p有三种情况:
- 结点p无左右孩子,则直接删除该结点,修改父节点相应指针。
- 结点p只有左孩子(右孩子),则把p的父节点和p的左孩子(右孩子)相连,然后删除p。
- 左右孩子同时存在,找到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的父节点相连
}
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的定义如下:
- 任何一个节点都有颜色,黑色或者红色;
- 根节点是黑色的;
- 父子节点之间不能出现两个连续的红节点;
- 任何一个节点向下遍历到其子孙的叶子节点,所经过的黑节点个数必须相等;
- 空节点被认为是黑色的。
数据结构表示如下:
class Node<T>{
public T value;
public Node<T> parent;
public boolean isRed;
public Node<T> left;
public Node<T> right;
}
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节点的父节点不是黑色的话,则继续做修复操作。
插入操作-case 2
case-2:新插入节点的叔叔节点为空,且祖父节点、父节点和新节点处于一条斜线上。
此时操作是将B节点进行右旋操作,并且和父节点A互换颜色。通过该修复操作RBTRee的高度和颜色都符合红黑树的定义。如果B和C节点都是右节点的话,只要将操作变成左旋就可以了。
插入操作-case 3
case-3:叔叔节点为空,且祖父节点、父节点和新节点不处于一条斜线上。
此时操作是将C节点进行左旋,这样就从case 3转换成case 2了,然后针对case 2进行操作处理就行了。case 2操作做了一个右旋操作和颜色互换来达到目的。如果树的结构是下图的镜像结构,则只需要将对应的左旋变成右旋,右旋变成左旋即可。
插入操作的总结
插入后的修复操作是一个向root节点回溯的操作,一旦牵涉的节点都符合了红黑树的定义,修复操作结束。之所以会向上回溯是由于case 1操作会将父节点,叔叔节点和祖父节点进行换颜色,有可能会导致祖父节点不平衡(红黑树定义3)。这个时候需要对祖父节点为起点进行调节,向上回溯,直到root节点为止,根据定义root节点永远是黑色的。
RBTree的删除操作
删除操作首先需要做的也是BST的删除操作,删除后就需要做删除修复操作,使的树符合红黑树的定义,符合定义的红黑树高度是平衡的。
删除修复操作是针对删除黑色节点才有的,当黑色节点被删除后会让整个树不符合RBTree的定义的第四条。需要做的处理是从兄弟节点上借调黑色的节点过来,如果兄弟节点没有黑节点可以借调的话,就只能往上追溯,将每一级的黑节点数减去一个,使得整棵树符合红黑树的定义。
删除修复操作在遇到被删除的节点是红色节点或者到达root节点时,修复操作完毕。
删除修复操作分为四种情况:
删除操作-case 1
case-1:待删除的节点的兄弟节点是红色的节点。
由于兄弟节点是红色节点的时候,无法借调黑节点,所以需要将兄弟节点上升到父节点,由于兄弟节点是红色的,根据RBTree的定义,兄弟节点的子节点是黑色的,就可以从它的子节点借调了。
case 1这样转换之后就会变成后面的case 2,case 3,或者case 4中的一种了。上升操作需要对兄弟节点做一个左旋或右旋操作,然后和父结点变换颜色,兄弟节点的左孩子旋转后作为它原本父结点的右孩子。
删除操作-case 2
case-2:待删除的节点的兄弟节点是黑色的节点,且兄弟节点的子节点都是黑色的。
因为兄弟节点和兄弟节点的子节点都是黑色的,所以可以将兄弟节点变红,但因为兄弟节点的父结点可红可黑,这个时候需要将父节点A变成新的节点,继续向上调整,直到整颗树的颜色符合RBTree的定义为止。
删除操作-case 3
case-3:待删除节点的兄弟节点是黑色的节点,且兄弟节点的左子节点是红色的,右子节点是黑色的(这是兄弟节点在右边的情况),如果兄弟节点在左边的话,就是兄弟节点的右子节点是红色的,左子节点是黑色的。我理解是和待删除节点离得近的是红的。
此时操作是对这个红色的结点做旋转上升,然后变换它和它原本父结点的颜色,转换为case-4。
删除操作-case 4
case-4:待删除节点的兄弟节点是黑色的节点,且兄弟节点的右子节点是红色的(兄弟节点在右边的情况),如果兄弟节点在左边,就是左子节点是红色的。现在是离得远的是红的。
如下图A是待删除节点,此时操作是对兄弟节点D做旋转上升,变换D和父结点B的颜色,D的左孩子C现在变为B的右孩子,D的红色右孩子变为黑色。
删除操作的总结
红黑树的删除操作是最复杂的操作,复杂的地方就在于当删除了黑色节点的时候,如何从兄弟节点去借调黑节点,以保证树的颜色符合定义。由于红色的兄弟节点是没法借调出黑节点的,这样只能通过旋转操作让他上升到父节点,而由于它是红节点,所以它的子节点就是黑的,可以借调。
对于兄弟节点是黑色节点的可以分成3种情况来处理,第1种情况是:当兄弟节点的子节点都是黑色节点时,可以直接将兄弟节点变红,这样局部的红黑树颜色是符合定义的。但是整颗树不一定是符合红黑树定义的,需要往上追溯继续调整。
第2种情况是:兄弟节点的子节点为左红右黑。第3种情况是:兄弟节点的右子结点是红的(左子结点随意)。我们可以先将第2情况通过旋转变为第3种情况,这时兄弟节点为黑,兄弟节点的右节点为红,可以借调出两个节点出来做黑节点,这样就可以保证删除了黑节点,整棵树还是符合红黑树的定义的,因为黑色节点的个数没有改变。