红黑树是每个节点都带有颜色属性的二叉查找树,颜色为 红色 或 黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
- 节点是红色或黑色。
- 根是黑色。
- 所有叶子都是黑色(叶子是NIL节点)。
- 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
- 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
下面是一个具体的红黑树的图例:
旋转
旋转是一种能保持二叉搜索树性质的搜索树局部操作。其中两种旋转分别为左旋和右旋:
在某个结点 x 上进行左旋时,假设它的右孩子为y而不是树的 T.nil 结点;x为其右孩子而不是 T.nil 结点的树内任意节点。
左旋以 x 到 y 的链为“支轴”进行,使得 y 成为该子树的新的根节点,x 成为 y 的左孩子,y 的左孩子变成 x 的右孩子;右旋与此相反。
左旋代码:
1 /** 2 * 左旋 3 * 左旋示意图(对节点x进行左旋): 4 * px px 5 * / / 6 * x y 7 * / \ --(左旋)--> / \ 8 * lx y x ry 9 * / \ / \ 10 * ly ry lx ly 11 * 12 * @param x 13 */ 14 private void leftRotate(RBTNode<T> x) { 15 RBTNode<T> y = x.right; // y是x的右节点 16 17 x.right = y.left; // 把x的左节点变为y的右节点 18 // 若y有左子节点,把y的左节点的父节点换成x 19 if (y.left != null) { 20 y.left.parent = x; 21 } 22 23 y.parent = x.parent; // y的父节点(原来是x)设为x的父节点 24 25 // 若x是根节点,y直接变根节点 26 if (x.parent == null) { 27 this.mRoot = y; 28 } else { 29 if (x.parent.left == x) { 30 x.parent.left = y; // 如果x是x父节点的左孩子,把x的父节点的左孩子指向y 31 } else { 32 x.parent.right = y; // 如果x是x父节点的右孩子,把x的父节点的右孩子指向y 33 } 34 } 35 36 y.left = x; // 将y的左节点指向x 37 x.parent = y; // 将x的父节点设为y 38 }
右旋代码:
1 /** 2 * 右旋,操作和左旋相反 3 * 右旋示意图(对节点y进行左旋): 4 * py py 5 * / / 6 * y x 7 * / \ --(右旋)--> / \ 8 * x ry lx y 9 * / \ / \ 10 * lx rx rx ry 11 * 12 * @param y 13 */ 14 private void rightRotate(RBTNode<T> y) { 15 RBTNode<T> x = y.left; // y的左孩子 16 17 y.left = x.right; 18 if (x.right != null) { 19 x.right.parent = y; 20 } 21 22 x.parent = y.parent; 23 24 if (y.parent == null) { 25 this.mRoot = x; 26 } else { 27 if (y.parent.left == y) { 28 y.parent.left = x; 29 } else { 30 y.parent.right = x; 31 } 32 } 33 34 x.right = y; 35 y.parent = x; 36 }
红黑树新结点插入代码:
就像一个普通的二叉搜索树一样,将新结点插入树中,并将其着为红色。之后为了能保证红黑的性质,还需要一个辅助代码对结点重新着色并且旋转。
1 /** 2 * 插入操作 3 * 4 * @param node 5 */ 6 private void insert(RBTNode<T> node) { 7 int result; 8 RBTNode<T> y = null; 9 RBTNode<T> x = this.mRoot; 10 11 // 查找树中插入点的父结点y的位置 12 while (x != null) { 13 y = x; // 注意这里,y不是空的 14 result = node.key.compareTo(x.key); 15 if (result < 0) { 16 x = x.left; 17 } else { 18 x = x.right; 19 } 20 } 21 22 node.parent = y; 23 if (y != null) { 24 result = node.key.compareTo(y.key); 25 if (result < 0) { 26 y.left = node; 27 } else { 28 y.right = node; 29 } 30 } else { 31 this.mRoot = node; 32 } 33 34 node.color = RED; 35 36 // 插入后修正树 37 insertFixUp(node); 38 }
辅助修正函数:
1 /** 2 * 红黑树插入修正函数 3 * 4 * @param node 5 */ 6 private void insertFixUp(RBTNode<T> node) { 7 RBTNode<T> parent, gparent; // 父节点,祖父节点 8 9 while (((parent = parentOf(node)) != null) && isRed(parent)) { 10 gparent = parentOf(parent); 11 12 // 父节点是祖父节点的左孩子 13 if (parent == gparent.left) { 14 RBTNode<T> uncle = gparent.right; // 叔叔节点,祖父的右节点 15 16 // ① 叔叔节点是红色的 17 if ((uncle != null) && isRed(uncle)) { 18 node.setColor(BLACK); 19 parent.setColor(BLACK); 20 gparent.setColor(RED); 21 node = gparent; 22 continue; 23 } 24 25 // ② 叔叔是黑色,且当前节点是右孩子 26 if (parent.right == node) { 27 RBTNode<T> tmp; 28 leftRotate(parent); 29 tmp = parent; 30 parent = node; 31 node = tmp; 32 } 33 34 // ③ 叔叔是黑色,且当前节点是左孩子 35 parent.setColor(BLACK); 36 gparent.setColor(RED); 37 rightRotate(gparent); 38 39 } else { // 父节点是祖父节点的右孩子 40 41 RBTNode<T> uncle = gparent.left; // 叔叔节点,祖父的左节点 42 43 // ① 叔叔节点是红色的 44 if ((uncle != null) && isRed(uncle)) { 45 uncle.setColor(BLACK); 46 parent.setColor(BLACK); 47 gparent.setColor(RED); 48 node = gparent; 49 continue; 50 } 51 52 // ② 叔叔是黑色,且当前节点是左孩子 53 if (parent.left == node) { 54 RBTNode<T> tmp; 55 rightRotate(parent); 56 tmp = parent; 57 parent = node; 58 node = tmp; 59 } 60 61 // ③ 叔叔是黑色,且当前节点是右孩子 62 parent.setColor(BLACK); 63 gparent.setColor(RED); 64 leftRotate(gparent); 65 } 66 } 67 68 this.mRoot.setColor(BLACK); 69 }
修正过程实例:
以下图中的 z 为插入后的结点,y 表示叔结点uncle,图中的每个子树的低端的节点是红黑树代码中的边界,边界中每个节点有黑色的哨兵没有画出来。
下面是介绍的是上面代码中 父节点是祖父节点的左孩子 的代码。
先看图中的第一个树,插入的 z 结点和 z.parent 父节点都是 RED,这违反了性质四。
情况 1(得到的是图中的第二个树):由于图中的第一个树中叔结点是红色,z 结点和 z.parent 父节点都是 RED,结点都要被重新着色,并沿着指针 z 上升;
情况 2(得到的是图中的第三个树):由于图中的第二个树中 z 及其父节点 z.parent 都为红色,其叔结点为黑色,左旋父节点 z.parent后得到;
情况 3(得到的是图中的第四个树):z 是其父节点的左孩子,重新着色后右旋的到图中的第四个树,这样之后就是合法的红黑树了。
分析红黑树的插入时间复杂度:
一颗具有 n 个节点的红黑树高度为O(log n),则按照一个普通的二叉查找树的方式插入结点需要花费 O(log n);修正代码中,当情况 1发生,指针 z沿着树上升2层,才会执行 while 循环,while 循环可能执行的总次数为 O(log n)。所以红黑树的插入的总的时间复杂度为 O(log n)。此外,插入算法中总的来说旋转次数不超过 2 次。
红黑树的删除:
1 /** 2 * 删除树中某个节点 3 * 4 * @param node 要删除的结点 5 */ 6 private void remove(RBTNode<T> node) { 7 RBTNode<T> child, parent; 8 boolean color; 9 10 // 要删除的结点node有2个子结点 11 if ((node.left != null) && (node.right != null)) { 12 RBTNode<T> replace = node; 13 14 // 寻找后继结点 15 replace = replace.right; 16 while (replace.left != null) { 17 replace = replace.left; 18 } 19 20 // 判断删除的结点是不是根结点 21 if (parentOf(node) != null) { 22 if (parentOf(node).left == node) { 23 parentOf(node).left = replace; 24 } else { 25 parentOf(node).right = replace; 26 } 27 } else { 28 this.mRoot = replace; 29 } 30 31 32 child = replace.right; // 后继结点的右孩子?左孩子呢?左孩子有早就是后继结点了,所以直接看后继结点还有没有右孩子 33 parent = parentOf(replace); // 后继结点的父结点 34 color = replace.color; // 后继结点的颜色 35 36 // 要删除的结点node是后继结点的父结点 37 if (parent == node) { 38 parent = replace; // 这里应该后继结点直接替换node,留下后继结点的右子树 39 } else { // 后继结点的父结点不是要删除的结点node 40 // 后继结点的孩子不为空 41 if (child != null) 42 child.setParent(parent); // 把<后继结点的右孩子>的<父结点>设为<后继结点的父结点> 43 parent.left = child; // <后继结点的父结点>的<左孩子>指向<后继结点的右孩子> 44 45 replace.right = node.right; // 后继结点的右孩子指向删除结点的右子树 46 node.right.setParent(replace); //删除结点的右子树的父结点设置为后继结点 47 } 48 49 replace.parent = node.parent; 50 replace.color = node.color; 51 replace.left = node.left; 52 node.left.parent = replace; 53 54 if (color == BLACK) 55 removeFixup(child, parent); 56 57 node = null; 58 59 return; 60 } 61 62 // 选一个要删除的结点的孩子 63 if (node.left != null) { 64 child = node.left; 65 } else { 66 child = node.right; 67 } 68 69 parent = node.parent; 70 color = node.color; 71 72 // 要删除的结点的孩子不为空 73 if (child != null) 74 child.parent = parent; 75 76 // 要删除的结点的父结点是不是树根 77 if (parent != null) { 78 if (parent.left == node) { 79 parent.left = child; 80 } else { 81 parent.right = child; 82 } 83 } else { 84 this.mRoot = child; 85 } 86 87 if (color == BLACK) 88 removeFixup(child, parent); 89 90 node = null; 91 }