红黑树
注:图片来自博主origins https://www.cnblogs.com/liyuan989/p/4071942.html
及百度图库
一 简介
红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。红黑树的基本思想是用标准的二叉查找树和一些额外的信息来表示2-3查找树。即可以说红黑树既是二叉查找树,也是2-3树。它结合了二叉查找树中简洁高效的查找方法和2-3树中高效的平衡查找方法。
红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。
相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证O(logN)的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)。
红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高,所以在插入和删除中所做的后期维护操作肯定会比红黑树要耗时好多,但是他们的查找效率都是O(logN),所以红黑树应用还是高于AVL树的. 实际上插入 AVL 树和红黑树的速度取决于你所插入的数据.如果你的数据分布较好,则比较宜于采用 AVL树(例如随机产生系列数),但是如果你想处理比较杂乱的情况,则红黑树是比较快的
二 性质
如下图所示,可以基本看出红黑树的性质:
1)每个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每个叶结点(叶结点即指树尾端NIL指针或NULL结点)是黑的。
4)如果一个结点是红的,那么它的俩个儿子都是黑的。
5)对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点。
public class RBTree<T extends Comparable<T>> { private RBTNode<T> mRoot; // 根结点 private static final boolean RED = false; private static final boolean BLACK = true; public class RBTNode<T extends Comparable<T>> { boolean color; // 颜色 T key; // 关键字(键值) RBTNode<T> left; // 左孩子 RBTNode<T> right; // 右孩子 RBTNode<T> parent; // 父结点 public RBTNode(T key, boolean color, RBTNode<T> parent, RBTNode<T> left, RBTNode<T> right) { this.key = key; this.color = color; this.parent = parent; this.left = left; this.right = right; } } ... }
三 红黑树旋转
红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的节点之后,红黑树就发生了变化,可能不满足红黑树的5条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转,可以使这颗树重新成为红黑树。简单点说,旋转的目的是让树保持红黑树的特性。旋转包括两种:左旋和右旋。
3.1 左旋
左旋的过程是将x的右子树绕x逆时针旋转,使得x的右子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。
private void leftRotate(RBTNode<T> x) { // 设置x的右孩子为y RBTNode<T> y = x.right; // 将 “y的左孩子” 设为 “x的右孩子”; // 如果y的左孩子非空,将 “x” 设为 “y的左孩子的父亲” x.right = y.left; if (y.left != null) y.left.parent = x; // 将 “x的父亲” 设为 “y的父亲” y.parent = x.parent; if (x.parent == null) { this.mRoot = y; // 如果 “x的父亲” 是空节点,则将y设为根节点 } else { if (x.parent.left == x) x.parent.left = y; // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子” else x.parent.right = y; // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子” } // 将 “x” 设为 “y的左孩子” y.left = x; // 将 “x的父节点” 设为 “y” x.parent = y; }
3.2 右旋
右旋的过程是将x的左子树绕x顺时针旋转,使得x的左子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。
private void rightRotate(RBTNode<T> y) { // 设置x是当前节点的左孩子。 RBTNode<T> x = y.left; // 将 “x的右孩子” 设为 “y的左孩子”; // 如果"x的右孩子"不为空的话,将 “y” 设为 “x的右孩子的父亲” y.left = x.right; if (x.right != null) x.right.parent = y; // 将 “y的父亲” 设为 “x的父亲” x.parent = y.parent; if (y.parent == null) { this.mRoot = x; // 如果 “y的父亲” 是空节点,则将x设为根节点 } else { if (y == y.parent.right) y.parent.right = x; // 如果 y是它父节点的右孩子,则将x设为“y的父节点的右孩子” else y.parent.left = x; // (y是它父节点的左孩子) 将x设为“x的父节点的左孩子” } // 将 “y” 设为 “x的右孩子” x.right = y; // 将 “y的父节点” 设为 “x” y.parent = x; }
四 插入
用BST的方法将结点插入,将该结点标记为红色的(因为如果标记为黑色,则会导致根结点到叶子结点的路径会多出一个黑结点,无法满足性质5,而且不容易进行调整),插入的情况包括下面几种:
4.1 插入到一个空的树
插入结点则为根结点,只需要将红色结点重新转染成黑色结点来满足性质2;
4.2 新结点的父结点为黑色
满足所有条件;
4.3 新结点的父结点为红色
因为性质2和性质4,所以树必然有祖父结点,则又包括以下的情况:
- N的叔父节点为红色。这种情况,将N的父节点和叔父节点的颜色都改为黑色,若祖父节点是根节点就将其改为黑色,否则将其颜色改为红色,并以祖父节点为插入的目标节点从情况1开始递归检测。
2.N的叔父节点为黑色, 且N和N的父节点在同一边(即父节点为祖父的左儿子时,N也是父节点的左儿子,父节点为祖父节点的右儿子时;N也是父节点的右儿子)。以父节点为祖父节的左儿子为例,将父节点改为黑色,祖父节点改为红色,然后以祖父节点为基准右旋。(N为父节点右儿子时做相应的左旋。
3.N的叔父节点为黑色,且N和N的父节点不在同一边(即父节点为祖父的左儿子时,N是父节点的右儿子;父节点为祖父节点的右儿子时,N也是父节点左右儿子)。以父节点为祖父节点的左儿子为例。以父节点为基准,进行左旋,然后以父节点为目标插入节点进入情况3的b情况进行操作。
public void insert(RBTNode<T> node) { int cmp; RBTNode<T> y=null; RBTNode<T> x=this.mRoot; while(x!=null) { y=x; cmp=node.key.compareTo(x.key); if(cmp<0) { x=x.left; }else { x=x.right; } } node.parent=y; if(y==null) { this.mRoot=node; }else { cmp=node.key.compareTo(y.key); if(cmp>0) { y.right=node; }else { y.left=node; } } //设置节点的颜为红色 node.color=RED; //修正为一棵红黑树 insertFixUp(node); }
/* * 红黑树插入修正函数 * * 在向红黑树中插入节点之后(失去平衡),再调用该函数; * 目的是将它重新塑造成一颗红黑树。 * * 参数说明: * node 插入的结点 // 对应《算法导论》中的z */ private void insertFixUp(RBTNode<T> node) { RBTNode<T> parent, gparent; // 若“父节点存在,并且父节点的颜色是红色” while (((parent = parentOf(node))!=null) && isRed(parent)) { gparent = parentOf(parent); //若“父节点”是“祖父节点的左孩子” if (parent == gparent.left) { // Case 1条件:叔叔节点是红色 RBTNode<T> uncle = gparent.right; if ((uncle!=null) && isRed(uncle)) { setBlack(uncle); setBlack(parent); setRed(gparent); node = gparent; continue; } // Case 2条件:叔叔是黑色,且当前节点是右孩子 if (parent.right == node) { RBTNode<T> tmp; leftRotate(parent); tmp = parent; parent = node; node = tmp; } // Case 3条件:叔叔是黑色,且当前节点是左孩子。 setBlack(parent); setRed(gparent); rightRotate(gparent); } else { //若“z的父节点”是“z的祖父节点的右孩子” // Case 1条件:叔叔节点是红色 RBTNode<T> uncle = gparent.left; if ((uncle!=null) && isRed(uncle)) { setBlack(uncle); setBlack(parent); setRed(gparent); node = gparent; continue; } // Case 2条件:叔叔是黑色,且当前节点是左孩子 if (parent.left == node) { RBTNode<T> tmp; rightRotate(parent); tmp = parent; parent = node; node = tmp; } // Case 3条件:叔叔是黑色,且当前节点是右孩子。 setBlack(parent); setRed(gparent); leftRotate(gparent); } } // 将根节点设为黑色 setBlack(this.mRoot); }
五 删除
删除的节点有两个儿子时,可以转化为删除的节点只有一个儿子时的问题。对于二叉查找树,在删除带有两个非叶子儿子的节点的时候,我们找到要么在它的左子树中的最大元素、要么在它的右子树中的最小元素,并把它的值转移到要删除的节点中。我们接着删除我们从中复制出值的那个节点,它必定有少于两个非叶子的儿子。因为只是复制了一个值,不违反任何性质,这就把问题简化为如何删除最多有一个儿子的节点的问题。它不关心这个节点是最初要删除的节点还是我们从中复制出值的那个节点。
那么所有情况都可以转化为删除只有一个儿子的节点的情况,我们约定这个要删除的节点为N(若N“没有”儿子节点,并用他的任意一个为叶子节点的儿子节点顶替即可)
5.1 . N为红色节点时
直接删除N,用它的黑色儿子代替它的位置。
5.2 . N为黑色节点,且父节点为红色
直接删除N,用它的儿子节点代替它的位置,并将该儿子节点改为黑色。
5.3. N为黑色节点,且父节点为黑色。
我们之间删除N,用它的儿子节点代替它,该儿子节点成为N',将N’的颜色改为黑色。
1.N’的兄弟节点和兄弟节点的2个儿子都为黑色。交换兄弟节点和父节点的颜色即可。
- 2.N‘的兄弟节点为黑色、且兄弟节点的红色儿子和兄弟节点在一边(即兄弟节点为左儿子时,红色儿子也为左儿子。兄弟节点为右儿子时,红色儿子也为右儿子)。我们以兄弟节点为右儿子为例。将祖父节点和兄弟节点的颜色互换,并将红色右儿子的颜色改为黑色,然后以祖父节点为基准左旋。(若兄弟节点为左儿子,则相应的右旋)
3.N‘的兄弟节点为黑色、且兄弟节点的红儿子和兄弟节点不在一边(即兄弟节点为左儿子时,红色儿子也为右儿子。兄弟节点为右儿子时,红色儿子也为左儿子)。我们以兄弟结点为右儿子为例。将兄弟节点和它的红色儿子的颜色互换,然后以兄弟节点为基准右旋。此时对于N’来说就进入了上文b情况。(若兄弟节点为右儿子,则相应的左旋)
4.N‘的兄弟节点为红色。以兄弟节点为右儿子为例,将父节点和兄弟节点的颜色互换,然后以父节点为基准左旋(若兄弟节点为左儿子则相应的右旋),此N’有一个黑色的兄弟节点,接下来N就可以进入a、b、c三种情况分别操作了。
5.N‘的兄弟节为黑色,父节点也为黑色。此时将兄弟节点的颜色改为红色。然后以父节点为目标插入节点从头开始依次判断。
public void insert(RBTNode<T> node) { int cmp; RBTNode<T> y=null; RBTNode<T> x=this.mRoot; while(x!=null) { y=x; cmp=node.key.compareTo(x.key); if(cmp<0) { x=x.left; }else { x=x.right; } } node.parent=y; if(y==null) { this.mRoot=node; }else { cmp=node.key.compareTo(y.key); if(cmp>0) { y.right=node; }else { y.left=node; } } //设置节点的颜为红色 node.color=RED; //修正为一棵红黑树 insertFixUp(node); }
/* * 红黑树删除修正函数 * * 在从红黑树中删除插入节点之后(红黑树失去平衡),再调用该函数; * 目的是将它重新塑造成一颗红黑树。 * * 参数说明: * node 待修正的节点 */ private void removeFixUp(RBTNode<T> node, RBTNode<T> parent) { RBTNode<T> other; while ((node==null || isBlack(node)) && (node != this.mRoot)) { if (parent.left == node) { other = parent.right; if (isRed(other)) { // Case 1: x的兄弟w是红色的 setBlack(other); setRed(parent); leftRotate(parent); other = parent.right; } if ((other.left==null || isBlack(other.left)) && (other.right==null || isBlack(other.right))) { // Case 2: x的兄弟w是黑色,且w的俩个孩子也都是黑色的 setRed(other); node = parent; parent = parentOf(node); } else { if (other.right==null || isBlack(other.right)) { // Case 3: x的兄弟w是黑色的,并且w的左孩子是红色,右孩子为黑色。 setBlack(other.left); setRed(other); rightRotate(other); other = parent.right; } // Case 4: x的兄弟w是黑色的;并且w的右孩子是红色的,左孩子任意颜色。 setColor(other, colorOf(parent)); setBlack(parent); setBlack(other.right); leftRotate(parent); node = this.mRoot; break; } } else { other = parent.left; if (isRed(other)) { // Case 1: x的兄弟w是红色的 setBlack(other); setRed(parent); rightRotate(parent); other = parent.left; } if ((other.left==null || isBlack(other.left)) && (other.right==null || isBlack(other.right))) { // Case 2: x的兄弟w是黑色,且w的俩个孩子也都是黑色的 setRed(other); node = parent; parent = parentOf(node); } else { if (other.left==null || isBlack(other.left)) { // Case 3: x的兄弟w是黑色的,并且w的左孩子是红色,右孩子为黑色。 setBlack(other.right); setRed(other); leftRotate(other); other = parent.left; } // Case 4: x的兄弟w是黑色的;并且w的右孩子是红色的,左孩子任意颜色。 setColor(other, colorOf(parent)); setBlack(parent); setBlack(other.left); rightRotate(parent); node = this.mRoot; break; } } } if (node!=null) setBlack(node); }