红黑树的引入
书中前一章讲的是二叉树, 搜索二叉树中某个元素的时间复杂度取决于树的平衡性, 比如你依次插入1,2,3,4,5, 那么这棵树实际上和链表没什么区别, 但是如果你依次插入4, 3, 5, 1, 2, 此时的搜索效率要远高于之前. 简单来讲普通二叉树的结构(影响其各种操作的效率)很大程度上取决于所插入数据以及插入的顺序. 就查找而言, 由于查找效率其实就取决于树的高度, 所以也就会在O(n)以及O(lg n)之间浮动, 树越平衡, h越解决O(lg n), 树越不平衡, 越接近O(n). 红黑树就是一种相对平衡的二叉树, 它的意义就在于使得二叉树在各种操作之后仍然保持相对的平衡. 网上关于红黑树的讲解很多, 感觉大部分就像是把书中说的翻译了一下, 没什么意思, 书里面关于这里也有了循环不变量以及各种一堆的证明, 我也没仔细看, 这里只谈谈自己的理解.
红黑树的概念
关于红黑树的性质书里面讲了5点, 我大概概括一下 :
- 根和叶子都是黑的(并且所有的叶子都指向一个固定的叶子, 这里干净其实也不是必须, 但是这样能省空间另外也不影响红黑树实现).
- 红节点的孩子都是黑节点.
- 对于每一个节点, 到任意叶子的path里面经过的黑节点数量相同.
反正你只要知道, 满足以上性质, 就是红黑树, 是红黑树, 各种操作就能保证在O(lg n). 我个人是这样想的, 从第二点可以知道, 不可能存在连续的红节点, 也就是说明对于任意一个path, 黑节点的数量绝对是要大于等于红节点, 假设对于根节点, 到叶子的黑节点数量是x, 那么h <= 2x. 另一方面, 对于一个固定的节点而言, 如果他到叶节点的path里面黑节点数是x, 那么他起码拥有2^x - 1个非叶节点. 为什么这么说呢? 我们可以考虑从一种最原始的情况开始考虑, 假设所有的点都是黑的, 由于第三点, 这棵树必定是一颗满二叉树, 这时候你可以在树里面插入红节点, 那么假设我们不插入红节点, 对于一颗高度为x的满二叉树, 其非叶节点的数量是(2^h - 1). (数学归纳法也能证明, 算法导论上是数学归纳法, 我这里是自己理解的思路) 所以对于红黑树的根节点而言, 他起码拥有2^x - 1个非叶节点, 也就是说n >= 2^x - 1 即 x <= log (n + 1), 也就是 h / 2 <= x <= log (n + 1). 所以可以认为h是在O(log(n + 1))的复杂度下.
红黑树的操作
红黑树的搜索与插入
显然的, 红黑树的搜索和二叉树并没有什么区别. 其实插入也区别不大, 只是在原先插入的基础上整合了一个修复函数用来维持红黑树的性质, 先来看代码 :
import javafx.util.Pair;
public class RB_BinaryTree {
private final static RB_BinaryTree leaf = new RB_BinaryTree();
private static RB_BinaryTree root = null;
enum COLOR{
RED, BLACK
};
private COLOR color = COLOR.RED;
private RB_BinaryTree left = leaf;
private RB_BinaryTree right = leaf;
private RB_BinaryTree parent = leaf;
private Pair<Integer, String> element = null;
public RB_BinaryTree(){
color = COLOR.BLACK;
}
private RB_BinaryTree(RB_BinaryTree parent, Pair<Integer, String> element){
this.parent = parent;
this.element = element;
}
public void print(){
System.out.println(root);
root.printHelper();
}
private void printHelper(){
if(this == leaf){
System.out.println("leaf");
}
else{
System.out.println(this);
left.printHelper();
right.printHelper();
}
}
public String search(Integer key){
RB_BinaryTree current = root;
while(current != leaf) {
if (key.equals(current.element.getKey())) {
return current.element.getValue();
} else if (key > current.element.getKey()) {
current = current.right;
} else {
current = current.left;
}
}
return null;
}
public void insert(Pair<Integer, String> element){
// root contains nothing
if(root == null){
root = this;
root.element = element;
return;
}
RB_BinaryTree current = root;
RB_BinaryTree previous = root;
while(current != leaf){
previous = current;
if(element.getKey() >= current.element.getKey()){
current = current.right;
}else {
current = current.left;
}
}
if(element.getKey() >= previous.element.getKey()){
previous.right = new RB_BinaryTree(previous, element);
current = previous.right;
}else {
previous.left = new RB_BinaryTree(previous, element);
current = previous.left;
}
current.insertFix();
}
private void insertFix(){
RB_BinaryTree current = this;
while(current.parent.color == COLOR.RED){
//so parent can't be root
RB_BinaryTree uncle;
if(current.parent == current.parent.parent.left) {
uncle = current.parent.parent.right;
if (uncle.color == COLOR.RED) {
/*
* uncle is also red ---> case 1
*/
uncle.color = COLOR.BLACK;
current.parent.color = COLOR.BLACK;
current = current.parent.parent;
current.color = COLOR.RED;
}
else {
if(current.parent.right == current){
/*
* uncle is black and current node is the right node of its parent ---> case 2
*/
current = current.parent;
current.leftRotation();
}
/*
* uncle is black and current node is the left node of its parent ---> case 3
*/
current.parent.color = COLOR.BLACK;
current.parent.parent.color = COLOR.RED;
current = current.parent;
if(current.parent == root){
root = current;
}
current.parent.rightRotation();
}
}
else{
// symmetric
uncle = current.parent.parent.left;
if (uncle.color == COLOR.RED) {
uncle.color = COLOR.BLACK;
current.parent.color = COLOR.BLACK;
current = current.parent.parent;
current.color = COLOR.RED;
}
else {
if(current.parent.left == current){
current = current.parent;
current.rightRotation();
}
/*
* uncle is black and current node is the left node of its parent ---> case 3
*/
current.parent.color = COLOR.BLACK;
current.parent.parent.color = COLOR.RED;
current = current.parent;
if(current.parent == root){
root = current;
}
current.parent.leftRotation();
}
}
}
root.color = COLOR.BLACK;
}
private void leftRotation(){
if( parent != leaf){
if(parent.left == this){
parent.left = right;
}else{
parent.right = right;
}
}
RB_BinaryTree newCurrent = right;
right = newCurrent.left;
newCurrent.parent = parent;
parent = newCurrent;
newCurrent.left = this;
}
private void rightRotation(){
if( parent != leaf){
if(parent.left == this){
parent.left = left;
}else{
parent.right = left;
}
}
RB_BinaryTree newCurrent = left;
left = newCurrent.right;
newCurrent.parent = parent;
parent = newCurrent;
newCurrent.right = this;
}
@Override
public String toString() {
return color.toString() + " " + element.getKey() + ": " + element.getValue();
}
}
搜索的代码很容易理解, 这里就不提了, 插入的代码除了最后一行的insertFix
函数, 其他也都和普通的二叉树插入一模一样. 这里我主要来说这个insertFix
函数. 为什么需要fix, 那么肯定是由于插入操作可能存在破坏红黑树某个性质, 那就具体来看看是哪一个 :
- 首先插入的节点默认作为红节点, 为什么不能插入黑节点呢 ? 我个人理解是这样的, 一开始这棵树上只有一个根节点是黑色的, 如果你插入的都是黑节点, 那永远不会出现红色节点.
- 既然插入的是红节点, 那么永远不要担心插入之后会违背我上面提到的性质3(节点到任意叶子节点的path中黑色结点的数量相同).
所以破坏红黑树的性质只有两种情况 :
- 插入节点本身就是根节点
- 插入节点的父亲已经是红色节点
对于第一条, 我们只需要再讲根节点改为黑色即可(实际上, 在不断向上修复的过程中也可能导致根节点被修改为红色) . 对于第二点, 其实有六种情况, 但是由于左右对称性, 只需要考虑三种即可. 现在只考虑父节点是祖父节点的左孩子的情况. (这时候叔叔节点是祖父节点的右孩子).
-
叔叔节点是红色节点, 那么考虑这时候的情况, 节点以及他的爸爸和叔叔都是红的, 爷爷是黑的(爷爷必须是黑的, 否则这棵树插入之前就已经不是红黑树了). 那么它只能是如下情况. (1表示黑, 0 表示红). 你会发现不管这个点是他父亲的左孩子, 还是右孩子, 都没有区别. 操作很简单, 就是把他的父亲和叔叔变成黑色, 祖父变成红色即可. 这时候你可以思考一下这样改动的后果, 网上大部分文章只会告诉你要这么改, 那么这么改为什么不会对原有的树的其他位置造成影响呢? 我来简单分析一下, 下面其他情况的改动可以用相同的思路去思考. 首先考虑这几个点 :
-
祖父点 : 它从黑色改成了红色, 这时候其实很有可能祖父和曾祖父之间又会发生矛盾, 这也是我们的所有变换都套在一个while里面的原因. 另外我上面提到有可能会在修复过程中导致根节点被修改成红色, 就是在这里, 可以考虑这个祖父点就是根节点, 那么这里根节点实际已经变成红色了.
-
父亲和叔叔 : 这两个点从红色改成黑色, 改成黑不可能会违背1, 2性质, 但是对于性质3, 你可以算一下, 由于父亲和叔叔都变色了, 所以变换前和变换后各个节点到下一代的path中黑色节点的数量任然是持平的(比较要到达下一代要么经过父亲, 要么经过叔叔).
1 1 0 0 0 0 0 0 ---> 1 1 1 1 0 0 0 0
-
-
叔叔节点是黑色节点, 并且该节点是父亲的右孩子. 这时候操作更加简单, 就是将父亲做一次左旋转, 旋转之后该节点成为了父亲的父亲. 但是这样做仍然没有解决连续两个红色节点的冲突. 这时候引出情况3, 实际上情况2和情况3属于一种情况, 每次碰到情况2只需要转换一下就是情况3.
1 1 0 1 ---> 0 1 0 0
-
叔叔节点是黑色节点, 并且该节点是父亲的左孩子. 这时候的操作是, 将祖父又黑色改为红色, 将父亲由红色改为黑色, 然后对祖父做一次右旋转. 这时候父亲变成了祖父的父亲, 父亲的之前右孩子成为了祖父的右孩子(这里需要注意父亲之前是红色, 所以右孩子肯定是黑色, 挂到变成红色的祖父下面是合法的, 同时祖父的左孩子仍然是他的左孩子(黑色的叔叔)). 那么这里, 对于上层来说, 因为之前的祖父, 现在的祖父(之前是父亲)都是黑色的, 所以实际上并不会出问题. 对于下层来说, 父亲的左右孩子仍然是直达祖父不需要经过任何黑色节点, 叔叔的左右孩子要到达祖父仍然需要经过叔叔这个黑色节点, 所以只要之前这个距离是相同的, 现在仍然会是相同的. 这里还要注意, 在这个位置由于祖父的节点变了, 而这个祖父极有可能就是根节点, 所以这时候需要动态的更新你的根节点, 在实际写代码过程中要特别注意, 我用java的时候, 刚开始没注意, 直接导致根节点丢失.
1 1 0 1 ---> 0 0 0 1
红黑树的删除
删除和插入类似, 都是对基本二叉树进行改进, 所以也算是在二叉树原有的删除操作的基础上进行一波修复. 先来看代码, 里边也包括完整的红黑树操作, 所以比较长 :
import javafx.util.Pair;
public class RB_BinaryTree {
private final static RB_BinaryTree leaf = new RB_BinaryTree();
private static RB_BinaryTree root = null;
enum COLOR {
RED, BLACK
}
;
private COLOR color = COLOR.RED;
private RB_BinaryTree left = leaf;
private RB_BinaryTree right = leaf;
private RB_BinaryTree parent = leaf;
private Pair<Integer, String> element = null;
public RB_BinaryTree() {
color = COLOR.BLACK;
}
private RB_BinaryTree(RB_BinaryTree parent, Pair<Integer, String> element) {
this.parent = parent;
this.element = element;
}
public void print() {
System.out.println("Root : ");
System.out.println(root);
System.out.println("\nPreOrder : ");
root.printHelper();
}
private void printHelper() {
if (this == leaf) {
System.out.println("leaf");
} else {
System.out.println(this);
left.printHelper();
right.printHelper();
}
}
public RB_BinaryTree search(Integer key) {
RB_BinaryTree current = root;
while (current != leaf) {
if (key.equals(current.element.getKey())) {
return current;
} else if (key > current.element.getKey()) {
current = current.right;
} else {
current = current.left;
}
}
return null;
}
public void insert(Pair<Integer, String> element) {
// root contains nothing
if (root == null) {
root = this;
root.element = element;
return;
}
RB_BinaryTree current = root;
RB_BinaryTree previous = root;
while (current != leaf) {
previous = current;
if (element.getKey() >= current.element.getKey()) {
current = current.right;
} else {
current = current.left;
}
}
if (element.getKey() >= previous.element.getKey()) {
previous.right = new RB_BinaryTree(previous, element);
current = previous.right;
} else {
previous.left = new RB_BinaryTree(previous, element);
current = previous.left;
}
current.insertFix();
}
private void insertFix() {
RB_BinaryTree current = this;
while (current.parent.color == COLOR.RED) {
//so parent can't be root
RB_BinaryTree uncle;
if (current.parent == current.parent.parent.left) {
uncle = current.parent.parent.right;
if (uncle.color == COLOR.RED) {
/*
* uncle is also red ---> case 1
*/
uncle.color = COLOR.BLACK;
current.parent.color = COLOR.BLACK;
current = current.parent.parent;
current.color = COLOR.RED;
} else {
if (current.parent.right == current) {
/*
* uncle is black and current node is the right node of its parent ---> case 2
*/
current = current.parent;
current.leftRotation();
}
/*
* uncle is black and current node is the left node of its parent ---> case 3
*/
current.parent.color = COLOR.BLACK;
current.parent.parent.color = COLOR.RED;
current = current.parent;
if (current.parent == root) {
root = current;
}
current.parent.rightRotation();
}
} else {
// symmetric
uncle = current.parent.parent.left;
if (uncle.color == COLOR.RED) {
uncle.color = COLOR.BLACK;
current.parent.color = COLOR.BLACK;
current = current.parent.parent;
current.color = COLOR.RED;
} else {
if (current.parent.left == current) {
current = current.parent;
current.rightRotation();
}
/*
* uncle is black and current node is the left node of its parent ---> case 3
*/
current.parent.color = COLOR.BLACK;
current.parent.parent.color = COLOR.RED;
current = current.parent;
if (current.parent == root) {
root = current;
}
current.parent.leftRotation();
}
}
}
root.color = COLOR.BLACK;
}
private void leftRotation() {
if (parent != leaf) {
if (parent.left == this) {
parent.left = right;
} else {
parent.right = right;
}
}
RB_BinaryTree newCurrent = right;
right = newCurrent.left;
newCurrent.parent = parent;
parent = newCurrent;
newCurrent.left = this;
}
private void rightRotation() {
if (parent != leaf) {
if (parent.left == this) {
parent.left = left;
} else {
parent.right = left;
}
}
RB_BinaryTree newCurrent = left;
left = newCurrent.right;
newCurrent.parent = parent;
parent = newCurrent;
newCurrent.right = this;
}
public void delete(RB_BinaryTree node) {
RB_BinaryTree oldChild;
COLOR oldColor = node.color;
if (node.left == leaf) {
oldChild = node.right;
transplant(node, node.right);
} else if (node.right == leaf) {
oldChild = node.left;
transplant(node, node.left);
} else {
// successor must have no left child
RB_BinaryTree successor = node.right.minimum();
oldChild = successor.right;
oldColor = successor.color;
if (successor.parent != node) {
transplant(successor, successor.right);
/*
if successor is the child of the node we want to delete, it must be the right child,
at that situation, we no longer need to let the deleting node's right child to be its
right child, otherwise we will point the node be itself's parent.
*/
successor.right = node.right;
successor.right.parent = successor;
}else {
// avoid the leaf not point to successor when successor is the child of deleted point and the successor's right child is leaf
oldChild.parent = successor;
}
transplant(node, successor);
successor.left = node.left;
successor.left.parent = successor;
successor.color = node.color;
}
if (oldColor == COLOR.BLACK) {
oldChild.deleteFix();
}
}
private void transplant(RB_BinaryTree oldNode, RB_BinaryTree newNode) {
if (oldNode == root) {
root = newNode;
} else if (oldNode.parent.left == oldNode) {
oldNode.parent.left = newNode;
} else {
oldNode.parent.right = newNode;
}
newNode.parent = oldNode.parent;
}
private RB_BinaryTree minimum() {
if (this.left == leaf) return this;
return this.left.minimum();
}
private void deleteFix() {
RB_BinaryTree current = this;
while (current != root && current.color == COLOR.BLACK) {
RB_BinaryTree brother;
if (current.parent.right == current) {
brother = current.parent.left;
if (brother.color == COLOR.RED) {
current.parent.color = COLOR.RED;
brother.color = COLOR.BLACK;
current.parent.rightRotation();
brother = current.parent.left;
}
// when code arrives here, must be brother.color == BLACK
if(brother.left.color == COLOR.BLACK && brother.right.color == COLOR.BLACK){
brother.color = COLOR.RED;
brother.parent.color = COLOR.BLACK;
current = current.parent;
}
else{
if(brother.left.color == COLOR.BLACK){
brother.right.color = COLOR.BLACK;
brother.color = COLOR.RED;
brother.leftRotation();
brother = brother.parent;
}
brother.left.color = COLOR.BLACK;
brother.color = current.parent.color;
current.parent.color = COLOR.BLACK;
if(current.parent == root){
root = brother;
}
current.parent.rightRotation();
// break out
current = root;
}
}
else {
brother = current.parent.right;
if (brother.color == COLOR.RED) {
current.parent.color = COLOR.RED;
brother.color = COLOR.BLACK;
current.parent.leftRotation();
brother = current.parent.right;
}
if(brother.left.color == COLOR.BLACK && brother.right.color == COLOR.BLACK){
brother.color = COLOR.RED;
current = current.parent;
}
else{
if(brother.right.color == COLOR.BLACK){
brother.left.color = COLOR.BLACK;
brother.color = COLOR.RED;
brother.rightRotation();
brother = brother.parent;
}
brother.right.color = COLOR.BLACK;
brother.color = current.parent.color;
current.parent.color = COLOR.BLACK;
if(current.parent == root){
root = brother;
}
current.parent.leftRotation();
// break out
current = root;
}
}
}
current.color = COLOR.BLACK;
}
@Override
public String toString() {
return color.toString() + " " + element.getKey() + ": " + element.getValue();
}
}
删除的话, 和插入的思路相同, 但是还是有一个地方要注意, 那就是当要删除点的successor就是要删除点的孩子同时这个successor的右边还是叶子节点的情况, 此时要将叶子节点的父亲指向successor, 否则叶子节点的父亲可能是任意一个node, 这可能导致最后的fix函数(如果需要的话)出问题. 我撸完代码才反应过来, 之前一直以为这两句话是废话. 函数重点说这个deleteFix
函数. 什么情况下需要? 如果我们所删除点的successor本来是个红色的节点, 那么这个节点被移动到删除节点位置, 改色后, 并不会对任何一条性质继续破坏, 所以无需fix. 但是如果我们删除的点的successor本身是个黑色的节点, 由于该节点的左孩子一定是叶子节点, 那么只要可能是他的右孩子会违背红黑树的性质, 这时候需要fix, 那么具体违背了什么性质呢?
- 因为successor的右孩子接替了successor的位置, 所以从successor位置开始往下的叶子都比他们正常所经过的黑色节点的数量少了1(successor被拿去充公了), 下文中我们用-1表示.
- 如果successor之前的父节点是红色, 并且successor的右孩子也是红色, 此时出现了两个连续的红色节点.
对于第二条的这种情况, 其实很简单, 只需要把successor的右孩子改成黑色节点, 这样可以将以上两条性质同时恢复. 难点在于如果他只是违背了第一条而没有维护第二条. 此时是什么情况呢? 此时会出现4中情况, 为了方便successor的右孩子我们称之为current节点(和代码中一致), 同时考虑的是current节点是右孩子的情况 :
-
current的兄弟节点是红色的, 那么此时可以断定current的父节点肯定是黑色, 这时候我们可以把current的兄弟改为黑色, 其父亲改为红色, 然后对其父亲进行右旋转, 结果会导致兄弟做了父亲的父亲, 做了current节点的爷爷, 此时current节点仍然是-1的. 但是他的兄弟是他之前的兄弟右孩子, 到这里仍然是正常的. 所以这种情况的作用就是把current节点的兄弟改为黑色. 所以此时完全不需要重新开始循环. 这里和上面插入一样, 所以就不仔细分析了.具体大概是这样(刚开始中间一排右边的是current) :
1 1 0 1 --> 1 0 1 1 1 1
-
current节点的兄弟是黑色的, 同时兄弟的左右孩子都是黑色的. 碰到这种情况, 我们只需要把current的兄弟节点改为红色, 此时current以及current的兄弟同时-1, 所以current的父亲直接直接-1, 所以我们可以把current指向current的父亲(当然此时如果current的父亲是红色的话(从情况1过来的话), 那么它将跳出循环, 因为此时只需要把current的父亲改为红色即可. 但是如果是黑色的话就需要继续循环)
? ? 1 1 --> 1 1
-
current的兄弟节点的左孩子是黑色的, 遇到这种情况, 我们的目的是将current节点的兄弟节点的右孩子改为红色的(也就是情况4), 这里也就是说情况1执行后会掉入情况2中, 情况3执行后会掉入情况4中. 那么如何改呢? 由于current节点的兄弟节点的两个孩子不都是黑色的, 此时右孩子是黑色的, 说明左孩子是红色的. 此时我们只需要把兄弟的左孩子变为黑色, 把兄弟变成红色, 然后对兄弟进行左旋转, 此时右孩子做了父亲的父亲, 检查可以发现初次之外不会违背任何红黑树性质.
? ? 1 1 --> 1 1 1 0 0 1 1 1
-
current的兄弟节点的左孩子是红色的, 此时将父节点的颜色赋给兄弟节点, 然后将父节点改为黑色, 同时兄弟节点的左孩子也改为黑色, 然后对父节点做右旋转. 这里其实有两种情况, 如果父节点一开始是黑色的, 那么由于兄弟节点肯定是黑色的, 此时相当于前两个操作的颜色改动无效, 然后右旋转之后, 兄弟节点做了父亲的父亲, 做了current的爷爷, 此时current由-1变成了0, 因为多垫了一层兄弟节点, 而兄弟节点的右孩子成了父节点的左孩子, 由于之前他的父亲和爷爷都是黑色, 现在爷爷和父亲换了位置而已, 所以性质保持, 同理兄弟节点的左孩子也是. 另外一种情况, 如果父节点一开始是红色的, 那么由于兄弟节点肯定是黑色的, 做相同操作后, 右边仍然下沉一层, 也就是说恢复了右边的同时保持了左边. 你会发现无论是哪一种情况, 之前父节点的颜色和新父节点的颜色都是相同的, 这代表这样做也不会导致父节点的上层(如果有的话)与父节点的颜色同红. 所以其实如果进行到第四种情况, 就直接可以跳出循环了. 但是这里要注意的是, 这里的父节点是有可能是根节点的, 所以我在代码实现的时候加了一个条件判断处理这种情况, 但是算法导论的伪代码中(包括插入)都是将这种对根的更新加载了旋转中, 而我这里旋转的代码里面并没有加入, 其实是不严谨的.
一些想法
网上很多人说红黑树难以理解, 情况很多. 但是如果不是以研究算法为目的, 单纯只是想要理解和代码实现的话, 我个人感觉这个树理解和实现个大概都不难, 但由于情况多, 毛毛躁躁极容易出错, 所以需要一个安静的环境, 那支笔一本本子, 边推导边写, 很快就能写出来. 实际上, 书上的大量篇幅我都是直接跳过, 只看了两个伪代码实现, 然后开始自己画图, 分析, 然后边分析边实现, 感觉还好.