文章目录
一、红黑树简介
1.红黑树是什么
之前阅读HashMap源码的时候,发现当链表长度大于8时会自动转化成红黑树。那时就埋下了一个种子,想要弄清楚红黑树到底是什么,于是就有了现在这一篇博文。
红黑树,实际上一种平衡的二叉排序树。那么,何为二叉排序树?根据百度百科的定义,二叉排序树有三个特性:
(1) 如果它的左子树不为空,那么左子树上的所有结点的值都小于根结点的值。
(2) 如果它的右子树不为空,那么右子树上的所有结点的值都大于根结点的值。
(3) 它的左右子树都为二叉排序树。
这两个都是二叉排序树,其中a是完全二叉树,b是满二叉树。
上图展示的是一个极度不平衡的二叉树,已然退化成了链表,查询效率很低。那么,有没有什么办法可以避免出现这种情况,在新增结点的同时保持二叉树的平衡,不然它退化成链表呢?答案是红黑树。当然这只是其中一种方法,还有平衡二叉树AVL等。
2.红黑树有什么性质
红黑树具有五个特性:
(1) 结点是红色或黑色
(2) 根结点是黑色
(3) 每个叶结点(NIL结点,空结点)是黑色的
(4) 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
(5) 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点
这五个特性可以保证二叉树的平衡,避免了退化成链表的情况。由性质4和性质5可以推出另一个性质:从根结点到叶子的最长可能路径不大于最短可能路径的两倍长,这个性质让红黑树即使在最坏情况下也能保持高效率。证明过程如下:
由性质4“不能有连续的两个红色结点”可得,最短路径是连续的黑色结点。再看性质5 “从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点”可以知道,黑色结点数与最短路径相同,要想路径尽可能地长,就只能在黑色结点之间插入红色结点,所以最长路径应是一条红黑结点交替出现的路径,不大于最短路径的两倍长。
在百度百科红黑树的条目下,有提到这么一段话,笔者觉得不错,另外分享给大家。
在很多树数据结构的表示中,一个结点有可能只有一个子结点,而叶子结点不包含数据。用这种范例表示红黑树是可能的,但是这会改变一些属性并使算法复杂。为此,本文中我们使用 “nil 叶子” 或"空(null)叶子",如上图所示,它不包含数据而只充当树在此结束的指示。这些结点在绘图中经常被省略,导致了这些树好象同上述原则相矛盾,而实际上不是这样。与此有关的结论是所有结点都有两个子结点,尽管其中的一个或两个可能是空叶子。
3.红黑树有什么操作
在阅读源码之前,如果能够理解红黑树的操作,可以很大程度地降低阅读的难度。
红黑树的基本操作和其他树形结构一样,一般都包括查找、插入、删除等操作。
二、红黑树源码
在对红黑树的性质、操作有了一定了解之后,我们接着对红黑树的源码进行解读。红黑树是在JDK1.8之后才被引入的数据结构,目的是优化传统HashMap在哈希冲突频发时查询效率很低的问题,时间复杂度由O(n)变为O(log(n))。
1.红黑树结构
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//父结点
TreeNode<K,V> parent;
//左子结点
TreeNode<K,V> left;
//左子结点
TreeNode<K,V> right;
//前驱结点
TreeNode<K,V> prev; // needed to unlink next upon deletion
//是否是红色结点
boolean red;
//哈希值(继承自Map.Entry)
final int hash;
//关键字(继承自Map.Entry)
final K key;
//值(继承自Map.Entry)
V value;
//后继结点(继承自Map.Entry)
Node<K,V> next;
/**
* 调用的是LinkedHashMap.Entry的构造方法,而LinkedHashMap.Entry继承HashMap.Node并且该构造方法调用的也是super方法
* 所以,实际上是调用HashMap.Node的Node(int hash, K key, V value, Node<K,V> next)方法
*/
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
2.putTreeVal方法
/**
* else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
* 调用hashMap的put方法时,如果链表已转化成红黑树,则调用此方法。
(如果当前链表的长度大于8时,会调用treeifyBin方法将链表转化成红黑树TreeNode)
此方法的目的是获取与传入hash值和key一一相等的红黑树结点,value的替换发生在put方法里。
* @param map 哈希表
* @param tab 数组
* @param h 哈希值
* @param k key值
* @param v value值
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
//从根结点开始寻找合适位置的结点
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
//如果当前结点的hash值大于待查询的hash值,则dir标记为-1
if ((ph = p.hash) > h)
dir = -1;
//如果当前结点的hash值小于待查询的hash值,则dir标记为1
else if (ph < h)
dir = 1;
/**
* 如果当前结点的hash值等于待查询的hash值且当前结点的key等于待查询的key,则返回当前结点
*/
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
/**
* 如果传入的key为不可比较的类或者k和当前结点的k通过compareComparables方法比较后相等,
则继续往下执行
* /
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//整个循环只搜索一次,因为第一次搜索就已经遍历了全树
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
/**
* 第一次进入时,p为根结点
* 如果p的左/右子结点不为空,且能够从该结点出发找到hash和key一一相等的结点,
则返回该结点
*/
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
/**
* 这个方法适用于两个类hash值相等且不可比较的情况,可以得出
下一个结点的插入位置(左-1或右1)
*/
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
/**
* -1取p的左子结点,1则取p的右子结点
* 在相应位置插入结点,下面会详细说明这一过程
* /
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
为了更好地理解结点是如何通过指针的变化插入到红黑树之中的,笔者将结合图片一步步演示代码是如何运作的。
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
不妨假设,dir等于-1且p.left等于null,如上图所示。
上文中有提到newTreeNode构造器实际上是调用HashMap.Node的Node(int hash, K key, V value, Node<K,V> next)方法,所以x结点的next指针指向了xpn结点。
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
如上图所示,xp结点的left和next指针发生改变,由红色虚线改成红色实线的位置。如果xpn结点不为空,则它的前驱结点指向x结点。
moveRootToFront(tab, balanceInsertion(root, x));
插入结点后,可能会引起红黑树失衡,违背了红黑树的五点性质。这时,需要调用moveRootToFront和balanceInsertion方法调整结点位置,使红黑树重新平衡。balanceInsertion方法是用于修复新红黑树,使得其满足五点性质。moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root)方法是确保传入的根结点是数组中的第一个结点。这两个方法后面会详细说到。
3.balanceInsertion方法
/**
* 修复红黑树
* @param root 根结点
* @param x 从这个结点开始向上修复红黑树
*/
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
/**
* x结点改为红色结点;
* 由此可知,插入的结点一开始都是红色结点。如果插入的是黑色结点,会使得某一条路径上的
黑色结点数目增加1,而其他路径黑色结点的数目保持不变,这就违背了“从任一结点到其每个
叶子的所有路径都包含相同数目的黑色结点”性质,徒增后续调整的成本。
*/
x.red = true;
/**
* 通过改变x的引用,使得循环继续,直到x为根结点或者x的父结点为根结点。
*/
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
/**
* 情况1:x为根结点,根结点为黑色
*/
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
/**
* 情况2:x的父结点是黑色结点或者x的父结点是根结点(实际上前者包含后者)
*/
else if (!xp.red || (xpp = xp.parent) == null)
return root;
/**
* 如果x的父结点是x的祖父结点的左孩子
*/
if (xp == (xppl = xpp.left)) {
/**
* 情况3:如果x的父结点是x的祖父结点的左孩子,
祖父结点的右孩子不为空,且为红色结点
*/
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
/**
* 情况4:如果x的父结点是x的祖父结点的左孩子,
祖父结点的右孩子为空,且x是父结点的右孩子
*/
if (x == xp.right) {
/**
* 左旋,将xp旋转为其右孩子的左孩子
* 这里的x = xp是为了改变x的引用,使得下一次循环从x的父结点开始
* x = xp并不会把实际的x改成xp,因为当方法参数为引用变量时,实际上传的是变量引用地址的副本。
这里的x = xp只是修改了副本的值,对实际x的引用地址没有影响。
*/
root = rotateLeft(root, x = xp);
//因为x=xp,所以需要重新定义xp和xpp结点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
/**
* 情况5:如果x的父结点是x的祖父结点的左孩子,
祖父结点的右孩子为空,且x是父结点的左孩子
*/
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
/**
* 右旋,将xp旋转为其左孩子的右孩子
*/
root = rotateRight(root, xpp);
}
}
}
}
/**
* 如果x的父结点是x的祖父结点的右孩子
*/
else {
/**
* 情况6:如果x的父结点是x的祖父结点的右孩子,
祖父结点的左孩子不为空,且为红色结点
*/
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
/**
* 情况7:如果x的父结点是x的祖父结点的右孩子,
祖父结点的左孩子为空,且x是父结点的左孩子
*/
if (x == xp.left) {
/**
* 左旋,将xp旋转为其右孩子的左孩子
* 这里的x = xp是为了改变x的引用,使得下一次循环从x的父结点开始
* x = xp并不会把实际的x改成xp,因为当方法参数为引用变量时,实际上传的是变量引用地址的副本。
这里的x = xp只是修改了副本的值,对实际x的引用地址没有影响。
*/
root = rotateRight(root, x = xp);
//因为x=xp,所以需要重新定义xp和xpp结点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
/**
* 情况8:如果x的父结点是x的祖父结点的右孩子,
祖父结点的左孩子为空,且x是父结点的右孩子
*/
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
/**
* 左旋,将xp旋转为其右孩子的左孩子
*/
root = rotateLeft(root, xpp);
}
}
}
}
}
}
总结:
修复红黑树的情况有很多,光是看着注释是很难理解的。最好还是要对着每一种情况去模拟结点的变化过程,在草稿纸上对着一行行代码画出此时的红黑树结构,画完后就会有一种豁然开朗的感觉。
4.rotateLeft和rotateRight方法
/**
* 左旋,将p旋转为其右孩子的左孩子
* @param root 根结点
* @param x 以这个结点为中心开始旋转
*/
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
分为两步:
1.将p的右孩子r的左孩子rl引用指向p
2.如果原先rl引用指向的结点不为空,则把该结点的引用指向p的右孩子
/**
* 右旋,将p旋转为其左孩子的右孩子
* @param root 根结点
* @param x 以这个结点为中心开始旋转
*/
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
分为两步:
1.将p的左孩子l的右孩子lr引用指向p
2.如果原先lr引用指向的结点不为空,则把该结点的引用指向p的左孩子
5.moveRootToFront方法
/**
* 确保给定的根结点是数组中的第一个结点
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
Node<K,V> rn;
tab[index] = root;
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
/**
* 检查此时的红黑树是否符合红黑树的性质
如果不符合,程序会抛出AssertionError,并终止执行。
*/
assert checkInvariants(root);
}
}
假设给定的根结点root不是数组的第一个结点first,那么会把root结点取出,放到first的前面。如下图所示:
6.checkInvariants方法
/**
* 检查此时的红黑树是否符合红黑树的性质
如果不符合,程序会抛出AssertionError,并终止执行。
* 一般从根结点开始,采用递归的方式检查每一个结点
* @param t 从该结点开始检查
*/
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (TreeNode<K,V>)t.next;
//t的前驱结点不为空,并且它的后继节点不为t
if (tb != null && tb.next != t)
return false;
//t的后继结点不为空,并且它的前驱节点不为t
if (tn != null && tn.prev != t)
return false;
//t的父结点不为空,并且它的左右孩子都不是t(相当于认了个干爹)
if (tp != null && t != tp.left && t != tp.right)
return false;
//t的左孩子不为空,与此同时,它的父结点不是t或者它的哈希值大于t的哈希值
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
//t的右孩子不为空,与此同时,它的父结点不是t或者它的哈希值小于t的哈希值
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
//t、t的左孩子、t的右孩子都不为空且都是红色结点
if (t.red && tl != null && tl.red && tr != null && tr.red)
return false;
//如果t的左孩子不为空,则继续从左孩子开始检查
if (tl != null && !checkInvariants(tl))
return false;
//如果t的右孩子不为空,则继续从右孩子开始检查
if (tr != null && !checkInvariants(tr))
return false;
return true;
}
插入小结:
1.找到插入位置,根据给定的哈希值、value值、key值初始化一个结点并插入到该位置
2.调用balanceInsertion方法修复因为插入新结点而可能失衡的红黑树
3.调用moveRootToFront方法,确保调用balanceInsertion后的根结点是数组的第一个元素
4.在moveRootToFront方法的最后会检查是否符合红黑树的特性,如果不符合,则报错并终止程序
7.getTreeNode方法
/**
* if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
如果该结点是红黑树结点,则调用getTreeNode获取与传入的哈希值和key值一一匹配的结点
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
//从根结点开始搜索
return ((parent != null) ? root() : this).find(h, k, null);
}
/**
* Finds the node starting at root p with the given hash and key.
* The kc argument caches comparableClassFor(key) upon first use
* comparing keys.
* 根据给定的hash值和关键字从根结点开始寻找一个结点。
* kc是key的class
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
/**
* ph 当前结点hash值
* dir
* pk 当前结点key值
*/
int ph, dir; K pk;
/**
* pl 当前结点左孩子
* pr 当前结点右孩子
*/
TreeNode<K,V> pl = p.left, pr = p.right, q;
//如果当前结点的hash值大于待查询的hash值
if ((ph = p.hash) > h)
//准备寻找当前结点的左孩子
p = pl;
//如果当前结点的hash值小于待查询的hash值
else if (ph < h)
//准备寻找当前结点的右孩子
p = pr;
//如果当前结点的hash值等于待查询的hash值且当前结点的key值等于待查询的key值,则返回当前结点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//如果当前结点的左孩子为空
else if (pl == null)
//准备寻找当前结点的右孩子
p = pr;
//如果当前结点的右孩子为空
else if (pr == null)
//准备寻找当前结点的左孩子
p = pl;
//如果传入的key不为空且是可比较的类,k和当前结点的k通过compareComparables方法比较后不相等
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
//根据compareComparables方法的返回值决定继续寻找的方向(左孩子还是右孩子)
p = (dir < 0) ? pl : pr;
//如果当前结点的右子树中存在这样的结点就返回该结点
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
//如果当前结点的右子树中不存在这样的结点,就继续寻找当前结点的左孩子
p = pl;
} while (p != null);
return null;
}
8.removeTreeNode方法
/**
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
如果该结点是红黑树结点,则调用removeTreeNode删除结点。node为待删除结点
* @param map
* @param tab
* @param movable
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
//如果是红黑树为空,则不需要删除结点
if (tab == null || (n = tab.length) == 0)
return;
/*
* 通过按位与操作算出该结点在数组中的下标
该算法与取余效果一样,但效率比取余高
*/
int index = (n - 1) & hash;
//first、root是桶中的头结点
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
//succ是待删除结点的后继节点,pred是待删除结点的前驱结点
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
//如果pred等于空(待删除结点为根结点)
if (pred == null)
/*
* tab[index]、first都指向待删除结点的后继节点
*/
tab[index] = first = succ;
else
//如果pred不为空,则待删除结点的前驱节点的后继结点指向待删除结点的后继节点
pred.next = succ;
//如果succ不为空,则待删除结点的后继节点的前驱结点指向待删除结点的前驱节点
if (succ != null)
succ.prev = pred;
//如果头结点为空,则结束程序
if (first == null)
return;
//如果头结点有父结点,则root重新指向根结点
if (root.parent != null)
root = root.root();
/**
* 如果根结点为空,或者根结点的右孩子为空,或者根节点的左孩子为空
或者根结点的左孩子的左孩子为空,则把红黑树转成链表
* 当结点数量太少的情况下,链表的查询速度要优于红黑树,维护成本较低
*/
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
/**
* 上面这一段只是修改了待删除结点前驱后继结点的指针,
对实际的结点位置并没有产生影响,待删除结点仍在红黑树中。
*/
//p是待删除结点,pl是待删除结点的左孩子,pr是待删除结点的右孩子
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
//待删除结点的左右孩子都不为空
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
//寻找待删除结点右子树中最左边叶子的父结点s,也就是右子树中哈希值最小的结点
while ((sl = s.left) != null) // find successor
s = sl;
//1.将s的颜色与待删除结点对调
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
//2.将s的右孩子、父结点指针与待删除结点的右孩子、父结点指针对调
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
//3.将s的左孩子指向待删除结点的左孩子,待删除结点的左孩子指向NIL结点
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
//如果待删除结点的左孩子不为空,则将其设为代替结点replacement
else if (pl != null)
replacement = pl;
//如果待删除结点的右孩子不为空,则将其设为代替结点replacement
else if (pr != null)
replacement = pr;
//如果待删除结点是叶子结点,则代替结点是它本身
else
replacement = p;
//如果代替结点不为叶子结点,则开始删除待删除结点
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
//如果待删除结点是根结点,根结点重新指向代替结点
if (pp == null)
root = replacement;
//如果待删除结点是父结点的左孩子,父结点的左孩子重新指向代替结点
else if (p == pp.left)
pp.left = replacement;
//如果待删除结点是父结点的右孩子,父结点的左孩子重新指向代替结点
else
pp.right = replacement;
//清除待删除结点的左孩子、右孩子、父结点的指针
p.left = p.right = p.parent = null;
}
//删除结点后的红黑树可能会失衡,调用此方法修复红黑树,使其满足红黑树五点性质
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
//删除结点
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
//确保给定的根结点是数组中的第一个节点
if (movable)
moveRootToFront(tab, r);
}
/**
* 将红黑树转成链表
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
//根据q结点返回一个新的结点 new Node<>(q.hash, q.key, 0q.value, next);
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
/**
* 修复红黑树,返回新的根结点
* @param root 根结点
* @param x 从这个结点开始向上修复红黑树
*/
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
TreeNode<K,V> x) {
for (TreeNode<K,V> xp, xpl, xpr;;) {
//情况1:如果x为空或根结点
if (x == null || x == root)
return root;
//情况2:如果x的父结点为空
else if ((xp = x.parent) == null) {
//x涂为黑色结点,并作为新的根结点
x.red = false;
return x;
}
//情况3:如果x为红色结点,则涂黑
else if (x.red) {
x.red = false;
return root;
}
//情况4:如果x是父结点的左孩子
else if ((xpl = xp.left) == x) {
//情况4.1:x的兄弟结点不为空且为红色结点
if ((xpr = xp.right) != null && xpr.red) {
xpr.red = false;
xp.red = true;
root = rotateLeft(root, xp);
xpr = (xp = x.parent) == null ? null : xp.right;
}
//情况4.2:x的兄弟结点为空
if (xpr == null)
x = xp;
//情况4.3:x的兄弟结点不为空且为黑色结点
else {
TreeNode<K,V> sl = xpr.left, sr = xpr.right;
//情况4.3.1:x的兄弟结点左右孩子均不为红色结点
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) {
xpr.red = true;
x = xp;
}
//情况4.3.2:x的兄弟结点左右孩子至少有一个是红色结点
else {
/**
* x兄弟结点的右孩子为空或者是黑色结点
*/
if (sr == null || !sr.red) {
if (sl != null)
sl.red = false;
xpr.red = true;
root = rotateRight(root, xpr);
xpr = (xp = x.parent) == null ?
null : xp.right;
}
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red;
if ((sr = xpr.right) != null)
sr.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateLeft(root, xp);
}
x = root;
}
}
}
//情况5:如果x是父结点的右孩子
else { // symmetric
//情况5.1:x的兄弟结点不为空且为红色结点
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
//情况5.2:x的兄弟结点为空
if (xpl == null)
x = xp;
//情况5.3:x的兄弟结点不为空且为黑色结点
else {
TreeNode<K,V> sl = xpl.left, sr = xpl.right;
//情况5.3.1:x的兄弟结点左右孩子均不为红色结点
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
}
//情况5.3.2:x的兄弟结点左右孩子至少有一个是红色结点
else {
if (sl == null || !sl.red) {
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl);
xpl = (xp = x.parent) == null ?
null : xp.left;
}
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateRight(root, xp);
}
x = root;
}
}
}
}
}
总结:
相对于插入操作,删除操作更为复杂。在阅读完源码后,笔者对删除的一系列操作有了一定的认知。删除结点,实际上是从红黑树中删除具有特定哈希值的结点,这个过程一共分为五个步骤:
此时,不妨假设待删除结点为X,X的后继节点为nextNode结点,X的前驱结点为prevNode结点。
1、上文中有提到红黑树结点具有next和prev指针,分别指向后继节点和前驱结点。这一步是将nextNode结点的prev指针指向prevNode结点,将prevNode结点的next指针指向nextNode。
2、如果红黑树结点数量太少的话,会将红黑树转化成链表,提升查询速度,不需要进行后续操作。
3、如果X的左右孩子均不为空,则寻找X结点右子树中最左边叶子的父结点s(右子树中哈希值最小的结点),将s结点和x结点互换位置,此时X结点仍在红黑树中;如果X只有一个孩子,那么这个孩子将替换掉X,此时X结点已经从红黑树中删除了。
4、如果X为红色,则直接删除即可;如果X为黑色,则需要调用balanceDeletion方法调整红黑树结构。如果删除黑色结点,会使得某一条路径上的黑色结点数量减少1,违背了“从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点”性质,所以需要修复红黑树。
5、如果X左右孩子均不为空,在修复红黑树后,还需要将X结点从红黑树中删除。
以上就是删除结点的大致步骤。
接下来,该说一说修复红黑树了。一开始,阅读源码并不顺利,源码艰涩难懂,看得人头皮发麻,几经放弃。后来,笔者还是咬咬牙关,在草稿纸上一遍遍模拟源码的运行过程,耐心地品味源码设计的用意,渐渐地对这一块有了更深入地了解。所以,希望读者不要轻言放弃,无从下手的时候,多多在草稿纸上画图,熟悉之后也就那么一回事。
字丑见谅。回到正题,如果待删除结点是红色结点,直接删除就可以,并不会违背性质。如果待删除结点是黑色结点,就需要分情况讨论了。笔者根据balanceDeletion源码,将待删除结点为黑色结点时的场景分为8种。如果漏了某一种,还需要读者可以指正,大家共同进步。此时,不妨设待删除结点为X,待替换结点为S。
1、X为根结点。这种情况直接删除即可。
2、X是红色结点。这种情况也是直接删除。
3、X是黑色结点,这种情况较为复杂。删除了黑色结点会导致某一条路径上的黑色结点数量减少一,违背了“从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点”性质。通过阅读源码,可知道此时是以X兄弟结点的颜色以及它左右孩子的颜色为依据进行划分,再根据此时的情况采用不同的方法调整红黑树结构。不妨设X兄弟结点为B(brother),B的左孩子为bl,右孩子为br
3.1、B是红色结点
3.2、B是黑色结点,且bl、br都是黑色结点
3.3、B是黑色结点,且bl、br都是红色结点
3.4、B是黑色结点,且bl是黑色结点,br是红色结点
3.5、B是黑色结点,且bl是红色结点,br是黑色结点
笔者看过很多详细讲解调整过程的文章,大多看完后仍然是一头雾水,倒不如自己在草稿纸上就着这五种情况对着源码画出红黑树的变换过程,这样更能加深理解。所以,笔者在这里就不对这五种情况进行说明了,希望读者可以动起手来。