数据结构及算法基础--树(Tree)(四)红黑树(Red Black Tree)
上篇文章我们学习了二叉排序树,而二叉排序树的性能取决于二叉树的层数:
- 最好的情况是 O(logN),存在于完全二叉排序树情况下,其访问性能近似于折半查找;
- 最差时候会是 O(N),比如插入的元素是有序的,生成的二叉排序树就是一个链表,这种情况下,需要遍历全部元素才行
为了改变二叉树的不足,我们开始使用红黑树。
对红黑树的定义其实是在二叉搜索树的规定进行了扩展,扩展内容为:
1、每个节点要么是黑色,要么是红色;
2、根节点永远是黑色;
3、所有的叶节点都是黑色(注意,java中实现的红黑树中的叶节点其实是null,在红黑树中表示为NIL,为空);
4、每个红色节点的两个子节点一定是黑色;
5、任意一个节点到其每个叶子节点的路径包含相同的黑色节点;
黑色高度指的是:从根节点到叶子节点路径中包含的黑色节点个数。
红黑树的左旋和右旋。这在红黑树中是非常重要的一环,我们在进行插入或者删除的时候为了保障红黑树的结构,我们都会使用一定的左旋和右旋操作来维持。
左旋和右旋的概念通过一张图便可以理解:
我们在java结合框架的treemap中可以查看到左旋右旋操作的具体过程:
/** From CLR */ private void rotateLeft(Entry<K,V> p) { if (p != null) { Entry<K,V> r = p.right; p.right = r.left; if (r.left != null) r.left.parent = p; r.parent = p.parent; if (p.parent == null) root = r; else if (p.parent.left == p) p.parent.left = r; else p.parent.right = r; r.left = p; p.parent = r; } } /** From CLR */ private void rotateRight(Entry<K,V> p) { if (p != null) { Entry<K,V> l = p.left; p.left = l.right; if (l.right != null) l.right.parent = p; l.parent = p.parent; if (p.parent == null) root = l; else if (p.parent.right == p) p.parent.right = l; else p.parent.left = l; l.right = p; p.parent = l; } }
这是一段很简单的代码,相信即便不了解treemap和java的Entry类的同学仍然能够明白这段代码的过程。
接下来就是红黑树最重要的操作了。就是在插入或者删除过程后,通过调整来让这个树仍然满足红黑树的条件。
1)插入:
我们其实主要需要考虑的是规则4和规则5,但是如果我们插入红色节点,我们就只需要根据规则4进行调整。所以我们将插入元素染红。
如果插入节点的父节点是黑色,我们则不需要进行其他的变化变可以满足条件。如果父节点是红色,我们则需要通过一系列变化来进行调整。
注:插入后我们主要关注插入节点的父亲节点的位置,而父亲节点位于左子树或者右子树的操作是相对称的,这里我们只介绍一种,即插入位置的父亲节点为左子树。
其实这里面只有两种情况
1、父节点的兄弟节点是红色
2、父节点的兄弟节点是黑色
情况1、父节点的兄弟节点是红色:
假设插入的是节点 N,这时父亲节点 P 和叔叔节点 U 都是红色,爷爷节点 G 一定是黑色。红色节点的孩子不能是红色,这时不管 N 是 P 的左孩子还是右孩子,只要同时把 P 和 U 染成黑色,G 染成红色即可。这样这个子树左右两边黑色个数一致,也满足特征 4。但是这样改变后 G 染成红色,G 的父亲如果是红色岂不是又违反特征 4 了?
我们这个时候可以使用向树的高层循环前进来进行变换,当判断的节点等于根节点或者当前节点的父节点为黑色的时候,我们跳出循环,完成调整。
情况2、父节点的兄弟节点是黑色:
通过把 爷爷节点 G 右旋,P 变成了这个子树的根节点,G 变成了 P 的右子树。
右旋后 G 跑到了右子树上,这时把 P 变成黑的,多了一个黑节点,再把 G 变成红的,完成调整
当然还有一种情况,如果 N 是 P 的右孩子,就需要多进行一次左旋,把情况化解成上述情况。
在代码中,这一切情况都有很好的考量:
/** From CLR */ private void fixAfterInsertion(Entry<K,V> x) { x.color = RED; while (x != null && x != root && x.parent.color == RED) { if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { Entry<K,V> y = rightOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { if (x == rightOf(parentOf(x))) { x = parentOf(x); rotateLeft(x); } setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateRight(parentOf(parentOf(x))); } } else { Entry<K,V> y = leftOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { if (x == leftOf(parentOf(x))) { x = parentOf(x); rotateRight(x); } setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateLeft(parentOf(parentOf(x))); } } } root.color = BLACK; }
2)删除调整
好吧,其实删除调整的原理是非常非常复杂的,要考虑很多种情况。本人能力有限还未理解彻底,也没有找到没有错误并且完整的资料,我只能通过jdk文档中的代码进行分析,然而我也只是将整个过程梳理了一遍,对每个情况判断的情况原理和机制还是不太了解,将来我学习更多知识过后,我会来细细讲解这部分内容:
private void fixAfterDeletion(Entry<K,V> x) { while (x != root && colorOf(x) == BLACK) { if (x == leftOf(parentOf(x))) { Entry<K,V> sib = rightOf(parentOf(x)); if (colorOf(sib) == RED) { setColor(sib, BLACK); setColor(parentOf(x), RED); rotateLeft(parentOf(x)); sib = rightOf(parentOf(x)); } if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) { setColor(sib, RED); x = parentOf(x); } else { if (colorOf(rightOf(sib)) == BLACK) { setColor(leftOf(sib), BLACK); setColor(sib, RED); rotateRight(sib); sib = rightOf(parentOf(x)); } setColor(sib, colorOf(parentOf(x))); setColor(parentOf(x), BLACK); setColor(rightOf(sib), BLACK); rotateLeft(parentOf(x)); x = root; } } else { // symmetric Entry<K,V> sib = leftOf(parentOf(x)); if (colorOf(sib) == RED) { setColor(sib, BLACK); setColor(parentOf(x), RED); rotateRight(parentOf(x)); sib = leftOf(parentOf(x)); } if (colorOf(rightOf(sib)) == BLACK && colorOf(leftOf(sib)) == BLACK) { setColor(sib, RED); x = parentOf(x); } else { if (colorOf(leftOf(sib)) == BLACK) { setColor(rightOf(sib), BLACK); setColor(sib, RED); rotateLeft(sib); sib = leftOf(parentOf(x)); } setColor(sib, colorOf(parentOf(x))); setColor(parentOf(x), BLACK); setColor(leftOf(sib), BLACK); rotateRight(parentOf(x)); x = root; } } } setColor(x, BLACK); }
红黑树的运用在java1.8种是非常广泛的,很多集合类型中都包含了红黑树的运用。感兴趣的同学可以通过treemap中的相关代码进行学习。源码永远是最好的老师。即便某些过程的原理不了解,我们也可以加深对某段代码或者某个数据结构的理解。