前面的文章已经介绍过二叉搜索树,AVL树,以及2-3Tree,今天我们再来学习一下二叉搜索树里面的大佬,它就是红黑树。红黑树(英语:Red–black tree)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,它在1972年由鲁道夫·贝尔发明,被称为"对称二叉B树",它现代的名字源于Leo J. Guibas和Robert Sedgewick于1978年写的一篇论文,红黑树的结构复杂,但它的操作有着良好的最坏情况运行时间,并且在实践中高效:它可以在O(logN)时间内完成查找,插入和删除。
同样是自平衡的二叉搜索树的AVL树,由于保持了严格的均衡策略,导致在插入和删除频繁时,性能会下降的比较厉害,而红黑树则是不强调严格的均衡性,所以在删除和插入的时候,综合性能要高于AVL树,但查询性能则略低于AVL树,所以这也是红黑树为什么被广泛应用的原因,因为你综合性能比较优越。
红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在符合二叉搜索树的基础上,对于任何有效的红黑树,为了保持均衡性,又增加了额外的定义也就是其性质,如下:
(1)根节点是黑色
(2)每个节点的颜色,只能黑色或者红色
(3)所有的叶子节点都是黑色,注意叶子节点是null节点,不存储数据,就是了为了补全节点用的,从这个定义来说红黑树是保持均衡性的满二叉树。
(4)每个红色节点必须有两个黑色的节点,翻译成另一种意思就是,在红黑树里面不会出现两个相邻的红色节点
(5)从任一节点出发到其叶子节点,都必须包含相同数目的黑色节点。
(6)在红黑树里面新加入的节点的颜色是红色的,注意这一条性质,很多网上的资料都没有说明。
下面看一个具体的图示:
基于这些性质,才确保了红黑树是一个综合性能高效的动态数据结构,二叉搜索树的性能依赖于树的高度,不平衡的二叉搜索树的问题就在于,如果插入数据是有序的,那么二叉树会退化成链表,从而导致时间复杂度变为O(N),试想一下现在有42亿的数据,如果时间复杂度是O(N),那么搜索效率可想而知,但如果是均衡的二叉树,那么查询次数只需要大概32次便可以,这就是均衡的重要性。红黑树不是从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,结果是这个树大致上是平衡的,为什么说最长路径不多于最短路径的两倍,我们从性质4就能够推导出来,如果每个红色节点必须要有两个黑色节点,那么同样数量的红色节点和黑色节点,红色节点每一个都需要一个黑色子节点,这样最坏情况下最长路径的的长度刚好是最短的2倍。
第二个问题是为什么新加入的节点必须是红色的,因为从性质5上来看,要保持相等黑色节点数量才能符合红黑树的性质,如果插入的节点默认是黑色的,那么一定会破坏红黑树的性质,而红色则不一定,除非它的父节点也是红色,这么看来红色破坏性质的几率更小。
第三个问题,我们可以思考下,红黑树可不可以全是由黑色节点组成。这个理论上是可以的,只有一个root节点,它的颜色肯定是黑色的,此外,理想情况下一颗完美二叉树的也可以由全部黑色组成,但 红黑树的新增节点为红色,所以想组成完美二叉树基本不可能。
不同于AVL树在每一个节点上维持了高度字段辅以旋转策略来维持平衡,红黑树在插入和删除的时候,维持平衡的手段主要是:变色+旋转
(一)插入操作
在红黑树上进行插入操作和删除操作会导致不再匹配红黑树的性质。恢复红黑树的性质需要少量O(logN)的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。虽然插入和删除很复杂,但操作时间仍可以保持为 O(logN)次。
情况一:只需要变色维持红黑树的性质
考虑这样一种情况,新插入节点的父节点不是黑色节点,也不是root节点,并且它的叔叔节点是红色,如下图:
这种情况下,比较容易解决,因为新插入的节点默认是红色的,所以出现了双红节点,违背了红黑树的性质,所以需要变色,解决手段是把父节点和叔叔节点变成黑色,然后爷爷节点变成红色,接着从 爷爷节点出发,依次校验修复直到根节点。
情况二:变色+旋转
考虑这样一种情况,新插入节点的父节点不是黑色节点,也不是root节点,并且它的叔叔节点是黑色,这种情况下处理就复杂了,不能简单通过变色来处理了。
旋转分四种情况:
case1:左左
这种情况下,新插入的节点和父节点都是左孩子,所以定义做左左case,解决策略是以爷爷节点右旋,然后变色。如下图:
case2:左右
这种情况下,新插入的节点是右孩子,但父节点是左孩子,所以定义做左右case,解决策略是先以父节点左旋,然后以爷爷节点右旋,然后变色,如下图:
case3:右右
这种情况下,新插入的节点是右孩子,且父节点也是右孩子,所以定义做右右case,解决策略是爷爷节点左旋,然后变色,如下图:
case4:右左
这种情况下,新插入的节点是左孩子,但父节点也是右孩子,所以定义做右左case,解决策略是先以父节点右旋,然后以爷爷节点左旋,最后变色,如下图:
这些是插入操作的维持均衡的手段,总体来说,相比AVL树略复杂,下面我们看下删除操作
(二)删除操作
删除操作相比插入操作更加复杂,对于二叉搜索树来说,删除场景分三种,删除的节点无孩子节点,删除的节点有一个孩子,删除的节点有两个孩子节点,但是在红黑树里面,每个叶子节点,都会定义其下面有两个黑色的null节点,再加上颜色操作,考虑的场景就复杂了。
情况一:简单的删除场景
这种情况下,孩子节点和父亲节点的颜色不一样,我们直接删除即可,如下图:
情况二:复杂的场景
这种情况下,孩子节点和父节点都是黑色,称为双黑,前面说过黑色节点的删除,毕然破坏红黑树的性质,所以要做修复:
case 1:兄弟节点是黑色,并且兄弟节点的有一个红色孩子节点:如下图:
右右场景:
右左场景:
case 2:兄弟节点是黑色,并且它的子节点都是黑色,需要通过变色来处理,并且递归处理到根节点:如下图:
case 3: 兄弟节点是红色,那么它的子节点肯定都是黑色,这种情况需要旋转和变色来处理,如下图:
这里用的是java语言,源码如下:
public class RBTree<K extends Comparable<K>,V> { private final static boolean RED = true; private final static boolean BLACK = false;
private Node<K,V> root;
private String prettyString;
public void add(K key, V value) {
Node<K,V> current=root; Node<K,V> parent=null;
if(root==null){ root=new Node(key,value,RED); root.color=BLACK; }else{
while (current.key!=null){
parent=current;
if(key.compareTo(current.key)>0){ current=current.right; }else{ current=current.left; }
}
//能到此处说明current.key=null Node newNode=new Node(key,value,RED); newNode.parent=parent;//parent赋值 if(key.compareTo(parent.key)<0){ parent.left =newNode;//左孩子 }else if(key.compareTo(parent.key)>0){ parent.right =newNode;//右孩子 } //进行矫正 addFixTree(newNode);
}
}
public void addFixTree(Node target){ //新添加的节点都是红色 if(target.parent.isBlack()){//父节点颜色是黑色,没有违背任何红黑树性质,直接返回 return; }else if(target.uncle().isRed()){ //如果叔叔节点是红色,那么只需要变色即可=> 父节点和叔叔全变黑色,爷爷节点变黑色,然后 //从爷爷节点开始,重复此步骤,对整棵树的可能修改的颜色进行校正 recolor(target); }else if(target.uncle().isBlack()){ //如果叔叔节点的颜色是黑色,需要分四种情况做旋转,这一点与AVL树的情况类似 //1.左旋 2.右旋 3.左右旋 4.右左旋 //这个地方不需要判断是否null
//left-left case if(target.parent.isLeft()&&target.isLeft()){ leftLeftCase(target);//只右旋 10 7 18 5 3 }else if(target.parent.isLeft()&&target.isRight()){ leftRightCase(target);//先左旋,然后右旋 10 7 18 5 6 }else if(target.parent.isRight()&&target.isRight()){ rightRightCase(target);//只左旋 5 4 9 10 11 }else if(target.parent.isRight()&&target.isLeft()){ rightLeftCase(target);//先右旋,然后左旋 5 4 9 12 10 }
}
}
public void leftRightCase(Node target){ rotateLeft(target.parent);//左旋 rotateRight(target.parent);//右旋 target=target.left; rotateColor(target);
}
public void rightLeftCase(Node target){ rotateRight(target.parent);//右旋 rotateLeft(target.parent);//左旋 target=target.right; rotateColor(target); }
public void leftLeftCase(Node target){ //左-左的情况,是需要右旋,右旋的节点是该节点的爷爷节点做为参照,具体见:https://www.geeksforgeeks.org/c-program-red-black-tree-insertion/ rotateRight(target.grandParent()); rotateColor(target); }
public void rotateColor(Node target){ //变色 if(target.isRed()&&target.parent.isRed()){ target.parent.setBlack();//parent为黑,子节点为两个红 if(target.isLeft()){ target.parent.right.setRed(); }else{ target.parent.left.setRed(); } root.parent=null; } }
public void rightRightCase(Node target){
rotateLeft(target.grandParent()); rotateColor(target);
}
/**** * * @param p */ public void rotateRight(Node p){
if(p!=null) {
Node l = p.left; p.left = l.right; if (l.right != null) l.right.parent = p; //设置parent节点 l.parent=p.parent; if(p.isRoot()){//如果p是root root=l; }else if(p.isRight()){ p.parent.right=l;//如果p原来是父的右孩子,就得把新的l接到原来p.parent.right }else { p.parent.left=l;//如果p原来是父的左孩子,就得把新的l接到原来p.parent.right } l.right=p;//设置右孩子 p.parent=l;//设置父节点 }
}
public void rotateLeft(Node p){
if(p!=null){ Node r=p.right; p.right=r.left; if(r.left!=null){ r.left.parent=p; } r.parent=p.parent; if(p.isRoot()){ root=r; }else if(p.isLeft()){ p.parent.left=r; }else { p.parent.right=r; } r.left=p; p.parent=r; }
}
private void setRoot(Node target){ root=target; if(target!=null){ root.setBlack(); } }
public void recolor(Node target){ if(target.isRoot()){ target.setBlack(); return; } //进来该方法的targe的颜色一定是红色的,所以不需要在判断 //recolor方法会调用递归多次,需要需要判断父节点是否为黑色,黑色不需要进行染色处理 if(target.parent.isBlack()){ return; }
//走到这里targe.parent 肯定是红色的
Node uncle=target.uncle(); // if(uncle!=null && uncle.isRed()){ target.parent.setBlack(); uncle.setBlack(); Node grandParent=target.grandParent(); //能进到这个方法,肯定grandParent不为null,取uncle的时候判断了 grandParent.setRed(); recolor(grandParent);//递归变色 }else { //走到这里,说明是本身是红色,父节点是红色,叔叔为黑,连续的双红,需要做修正 addFixTree(target); } }
public void inorder(Node root) { if (root.key == null) { return; } inorder(root.left);
System.out.println(root.key);
inorder(root.right);
}
/*** * 根据key搜索指定节点 * @param k * @return */ public Node<K,V> search(K k){ Node<K,V> p=root; while (p.key!=null){ int cmp=k.compareTo(p.key); if(cmp<0){ p=p.left; }else if(cmp>0){ p=p.right; }else { return p; } } return null; }
public Node<K,V> successor(Node<K,V> t){ //找到右子树里面找到最小的 Node<K,V> p=t.right; while (p.left.key!=null){ p=p.left; } return p; }
public void delete(K k){
Node<K,V> p=search(k); if(p==null){ return;}
if(p.left.key!=null&&p.right.key!=null){//拥有2个孩子节点 Node<K,V> s=successor(p);//找到后继 p.key=s.key; //改变p的key为s.key p.data=s.data;//改变p的data为s.data //注意上面是指针传递,所以p的内容已经被修改 p = s;//这里又把s.内存地址赋值给p,对p上一个的内容的不会产生影响 } //获取需要被替换掉的节点 Node<K,V> replacement=p.left.key!=null?p.left:p.right;
if(replacement!=null){ //去掉找到的p replacement.parent=p.parent;
//连接p.parent和末尾的节点 if(p.parent==null){ root=replacement; }else if(p.isLeft()){ p.parent.left=replacement; }else{ p.parent.right=replacement; }
//p节点的所有的引用置为null,方便gc p.left=p.right=p.parent=null; //如果删除的是黑色节点,就会导致不平衡,所以需要修复 if(p.isBlack()){ fixAfterDeletion(replacement); }
}else if(p.parent==null){ root=null; }else {//没有两个孩子,只有单个孩子,直接用父的引用直接其后面的即可 if(p.isBlack()){ fixAfterDeletion(p);//删掉的是黑色就得做均衡 }
if(p.parent!=null){ if(p.isLeft()){ p.parent.left=new Node<>(); }else if(p.isRight()){ p.parent.right=new Node<>(); } p.parent=null; }
}
}
private void fixAfterDeletion(Node<K,V> x){
while (x!=root&&x.isBlack()){
if(x.isLeft()){ Node<K,V> sib=x.parent.right; if(sib.isRed()){//如果x的兄弟节点是红色 sib.setBlack();//给x的兄弟设置成黑色 x.parent.setRed();//给他们的父节点设置成红色 rotateLeft(x.parent);//左边删除了,所以左边少节点,需要左旋 sib=x.parent.right;//新的兄弟节点 } //如果兄弟节点的孩子都是黑色,需要将其设置成红色 if(sib.left.isBlack()&&sib.right.isBlack()){ sib.setBlack(); x=x.parent;//继续向上遍历修复 }else { if(sib.right.isBlack()){ //兄弟的右边是黑色,左边是红色 sib.left.setBlack();//需要将其左边设置黑色 sib.setRed();//sib父节点设置成红色 rotateRight(sib);//右旋 sib=x.parent.right;
} sib.color=x.parent.color; x.parent.setBlack(); sib.right.setBlack(); rotateLeft(x.parent); x=root; }
}else{ //与if里面相反的逻辑 Node<K,V> sib=x.parent.left; if(sib.isRed()){ sib.setBlack(); x.parent.setRed(); rotateRight(x.parent); sib=x.parent.left; }
if(sib.right.isBlack()&&sib.left.isBlack()){ sib.setRed();; x = x.parent; }else {
if(sib.left.isBlack()){ sib.right.setBlack(); sib.setRed();; rotateLeft(sib); sib=x.parent.left; } sib.color=x.parent.color; x.parent.setBlack(); sib.left.setBlack(); rotateRight(x.parent); x=root; }
}
} x.setBlack(); }
public static void main(String[] args) {
RBTree<Integer,Integer> rbTree=new RBTree();// rbTree.add(30,5); rbTree.add(20,4); rbTree.add(10,9); rbTree.add(30,10); rbTree.add(25,10); rbTree.add(35,10); rbTree.delete(20); rbTree.inorder(rbTree.root);
// System.out.println(rbTree.search(1));
}
public Object remove(Comparable key) { return null; }
public Object lookup(Comparable key) { return null; }
public String toPrettyString() { return null; }
class Node<K extends Comparable<K>,V>{
private K key;
private V data;
private Node<K,V> left;
private Node<K,V> right;
private Node<K,V> parent;
private boolean color;
public Node(){ this.key=null; this.data=null; this.color=BLACK;//新添加的Node的节点颜色为黑色 }
public Node(K key,V data,boolean color){ this.key=key; this.data=data; this.color=color; this.left =new Node(); this.right =new Node(); }
public boolean hasRightChild(){ if(this.right !=null){ return true; } return false; }
public boolean isLeft(){ if(this.parent.left ==this) {return true;} return false; }
public boolean isRight(){ if(this.parent.right ==this) {return true;} return false; }
//找爷爷节点 public Node grandParent(){ if(parent!=null){ return parent.parent; } return null; }
public boolean isRoot(){ return parent==null; }
public boolean isBlack(){ return this.color==BLACK; }
public boolean isRed(){ return this.color==RED; }
public void setBlack(){ this.color=BLACK; }
public void setRed(){ this.color=RED; }
// 找叔叔节点 public Node uncle(){ Node grandParent=grandParent(); if(grandParent==null){ return null; }else if(parent==grandParent.left){ return grandParent.right; //父节点是左,那么父节点的右边是叔叔节点 }else { return grandParent.left; // 父节点本身是右,那么父节点的左边是叔叔节点 } }
public int compareTo(Node<K,V> node){ return this.key.compareTo(node.key); }
public String nodeColor(){ String color=""; if(this==null||this.color==BLACK){ color="B"; } else if(this.color==RED){ color="R"; } return color; }
@Override public String toString() {
String retString=""; if(this.key==null){ retString ="nil"; }else{ retString=this.key+"="+nodeColor(); }
return retString; } }
}
红黑树和AVL树都是平衡二叉搜索树里面最常见的两种类型,都支持在O(logN)的时间内,完成插入,删除和查询操作,但由于红黑树的综合性能要好于AVL树,所以在实际应用中更加常见。
区别主要有两点:
(1)对于搜索操作来说,AVL树是严格均衡树,其搜索性能要好于红黑树
(2)对于插入和删除操作来说,红黑树的性能更好,它的旋转次数更少,因为不需要维持严格的平衡。
此外还有一点需要了解,AVL树在每个节点要存储一个int类型的高度字段,而红黑树需要在每个节点存储一个boolean类型的颜色标记,还有父节点的引用地址,所以从额外的空间复杂度的大小来说两者基本持平。
本文主要介绍了平衡二叉树里面的红黑树的相关内容,红黑树是一种综合效率非常好的动态数据结构,所以在实际应用中非常广泛,比如Java里面的TreeMap和TreeSet都是基于红黑树实现的,红黑树的是一种近似平衡的二叉树,其平均时间复杂度为O(logN),综合性能处于logN和2logN之间,相比AVL树更加适合插入和删除频繁的场景,由于维持平衡需要变色和旋转来配合操作,所以导致红黑树的代码实现相比AVL树复杂了数倍,对于红黑树,我们的重点在于要理解维持平衡的原理和思想,掌握了这些就可以使得我们对于平衡二叉树的认知更上一层楼。