红黑树 可以看作二叉搜索树和平衡二叉树(AVL
树)的一个折中。
二叉搜索树:一棵空树,或者是具有下列性质的 二叉树:
若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
左、右子树也分别为二叉排序树;
没有键值相等的结点。
AVL
树本质上还是一棵二叉搜索树,它的特点是:
本身首先是一棵二叉搜索树。
带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。也就是说,AVL
树,本质上是带了平衡功能的二叉查找树(二叉排序树,二叉搜索树)
红黑树是一种半平衡的二叉搜索树,它放弃了二叉搜索树的绝对平衡,换来了较为简单的可维护性,使得二叉搜索树插入新数据,以及搜索数据时,都具有不错的搜索性能。
之所以说红黑树是一种半平衡的二叉搜索树,是因为红黑树中所有叶子节点的深度相差不会超过一倍。
红黑树的特性:
只要二叉搜索树符合以上 5 条性质,它就是红黑树。事实上,提出这 5 条性质的目的就是为了获得红黑树的“所有叶子节点的深度相差不会超过一倍”这个特性。之所以这么费尽心思的维护一个红黑树,是因为实践证明红黑树的这些规则遵循起来是相对简单的。
关于红黑树的具体插入和删除分析,可以参考文末的几篇博客,后续不再具体分析。
关于红黑树的插入情景如下图(摘自 30张图带你彻底理解红黑树):
那么结合 HashMap
源码分析,HashMap
中关于红黑树的操作,都放在了 TreeNode
类中。
在上文分析 HashMap
中,留下了关于红黑树的一部分,其中 put
方法的调用中涉及到的 TreeNode
的 putTreeVal
(插入) 和 treeify
(调整链表为红黑树) 方法。
先分析 treeify
,在分析具体源码之前,我们可以确认现在箱中的节点已经为树节点(但仍然为链表结构),且调用 treeify
方法的对象为链表头节点。
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
// next节点为下次需要处理的节点
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
// 设置根节点,只在第一次执行
if (root == null) {
x.parent = null;
// 标记为黑节点
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
// 找到树上的可插入点
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// 平衡插入
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
上面执行的目的是将链表转换为红黑树。第一个循环是为了遍历链表,第二个循环是为了找到已构建的红黑树中当前节点的插入位置,并且需要在 balanceInsertion
方法中处理平衡红黑树操作(涉及到上图中的几种情形):
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
// 插入节点为红色节点
x.red = true;
/*
* xp :x 的父节点
* xpp :x 的祖父节点
* xppl : x 的祖父节点的左子节点
* xppr:x 的祖父节点的右子节点
*/
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
// 情景1:空树
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 情景3:插入节点的父节点为黑节点
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 情景4:插入节点的父节点为红色节点
// 且父亲节点是祖父节点的左子节点
if (xp == (xppl = xpp.left)) {
// 情景4.1:叔叔节点存在,且为红色,做平衡调整
// 父节点和叔叔节点设置为黑色,祖父节点设置为红色,并继续做插入操作自平衡处理,直到 // 平衡为止(自下向上)
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
// 情景4.2:叔叔节点不存在,或者为黑节点
else {
// 情景4.2.2:插入节点是其父节点的右子节点
if (x == xp.right) {
// 左旋,重新调整
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 情景4.2.3:插入节点是其父节点的左子节点
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
// 父亲节点是祖父节点的右子节点
else {
// 情景4.1:叔叔节点存在并且为红节点
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
// 情景4.3:叔叔节点不存在,或者为黑节点
// 情景4.3.2:插入节点是其父节点的左子节点
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 情景4.3.1:插入节点是其父节点的右子节点
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
结合红黑树的插入情景来分析源码就会发现容易多了。
关于构建完红黑树后调用的 moveRootToFront(tab, root)
方法,它的作用正如它声明的那样确保 root 节点是箱子节点的第一个节点。
再来看 putTreeVal
方法:
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;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
// 待插入节点存在,则直接返回,按插入重复元素处理
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
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;
}
dir = tieBreakOrder(k, pk);
}
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);
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;
}
}
}
整体的流程走完了,但其中还涉及比较多的细节,可自行品味。
关于红黑树的删除情景如下图(摘自 30张图带你彻底理解红黑树):
这一块的分析,就没有记录下来;删除操作更加复杂,仅仅是看懂就花了很多时间。可以参考文末的 30张图带你彻底理解红黑树 这篇博客,如果这篇博客理解了,那么整个底层也就明白了。
另附一个很好的在线绘制红黑树的网站:红黑树可视化
参考博文
红黑树原理和算法介绍
自平衡二叉树和红黑树
30张图带你彻底理解红黑树
数据结构可视化
手撕Java类HashMap
手撕HashMap迭代器
我与风来
认认真真学习,做思想的产出者,而不是文字的搬运工
错误之处,还望指出