红黑树
接上篇hashmap的后续,关于红黑树的操作。
优点:红黑树查找方便,时间复杂度是树的高度。
红黑树的基本原理,大家应该都知道,我这里再写一遍:
- 树上的每个节点不是黑色就是红色
- 根节点是黑色
- 叶子节点是黑的
- 一个红色节点他的两个子节点都是黑的
- 任意一个节点到他叶子节点任意一条路包含的黑色节点数目是相同的。
来吧我们就来看看红黑树的形成与删除、左旋、右旋、查找。
基本结构
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; // 节点的前一个(并不是树中的前一个)
//之前hashmap也说过,从链表转换为红黑树之前,先变成TreeNode<K,V>类型的链表,
//然后转换成红黑树,这里的prev是这里TreeNode<K,V>链表的前一个。
boolean red; // 节点的颜色
TreeNode(int hash, K key, V val, Node<K,V> next) {
//TreeNode的父类的父类是Node。
super(hash, key, val, next);
}
}
增加节点
当链表的长度大于等于8的时候,并且table的长度大于等于64,就将链表转换为红黑树。
下面这个方法是hashmap中treeifyBin方法调用的。
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
// 遍历TreeNode链表
for (TreeNode<K,V> x = this, next; x != null; x = 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;;) {
// dir只有两种结果,-1 或者 1 这是 p 节点和 x 节点键的hash值比较大小的结果
// ph 是 p 的键的hash值;h 是 x 的键的hash值
// h > ph dir = 1; h < ph dir = -1;
// dir 决定了 x 应该放在 p 的右边还是左边。
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);
// 如果上面比较不出来 p 和 x 键的hash的值,那么就用tieBreakOrder
//这个方法比较,这个比较的也是他们两个键的hashcode的大小。
TreeNode<K,V> xp = p;
//只有p节点的左或者右没有,x才加入进去,否则一直循环直到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);
}
- 遍历链表,取节点x,节点的左右节点都为空。
- root是否为空,为空。root = x,root的父节点为空,颜色为黑色。
- root不为空。从root开始遍历树,取树节点p,与节点x比较键的hash大小(ph 为 p 的键的hash值,h 为 x 的键的hash值)
- 若h > ph, x 应该放在 p 的右边。
- 若 p 的右节点为空,那么 x 成为 p 的右节点
- 若 p 的右节点不为空, p 的右节点成为 p。接着比较。
- 若 h < ph,x 应该放在 p 的左边。
- 若 p 的左节点为空,那么 x 成为 p 的左节点。
- 若 p 的左节点不为空,p 的左节点成为 p 。接着比较。
- 若h > ph, x 应该放在 p 的右边。
加完一个节点平衡树一次。
平衡节点
// 参数是根节点和加进来的节点,加进来的节点有可能会破坏树的平衡。平衡之后有可能会改变树的根节点,所以要返回根节点。
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true;
// xp 是 x 的父节点,xpp 是 x 的爷爷节点,xppl 是爷爷节点的左节点,xppr 是爷爷节点的右节点。
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
if ((xp = x.parent) == null) {
// x 节点的父节点为null,说明 x 为根节点,结束
x.red = false;
return x;
}
else if (!xp.red || (xpp = xp.parent) == null)
// 父节点不是红的或者爷爷节点不存在,结束
return root;
//上述条件都不满足,那么就说明,x 有爷爷节点,并且 x 和他的父节点的颜色都是红的,
//平衡,首先要看x的父节点是爷爷节点的左节点还是右节点,位置不同,方法不同。
if (xp == (xppl = xpp.left)) {
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
else {
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
如下几种情况,看图:
- xpp存在并且xpp的左节点存在
- 将xp的右节点赋值给xpp的左节点,xp的右节点比xp大比xpp小。
- xpp的父节点赋值给xp的父节点
- xp的右节点就是xpp。
2. 插入一个节点,是这种状况也要进行平衡
左旋
- 这种情况下以xp为基准,旋转后两个节点。xp 是否存在,且 xp 的右节点xr是否存在
- xr的左节点赋值给xp的右节点
- xp 的父节点赋值给 xr 的父节点
- xr 的左节点就是 xp
这样之后还没结束,因为他还不是平衡的,就和第一种情况相同,所以接下来的做法就和第一种相同。
3. 还有一种情况
这种的就是通过变换节点的颜色达到树的平衡,有可能会影响到其他节点,所以这种情况我们就要一直遍历到根节点,确保没有连续两个都是红色的节点就好了。
我只介绍了左边的情况,右边和这个差不多。
这就是加一个节点的情况。
删除节点
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;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
// next是要删除节点的下一个,prev是要删除节点的前一个
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
// 首先先从TreeNode链表里将这个节点删除掉。
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null)
return;
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;
}
// 从红黑树删除这个节点
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
// p是要删除的节点 pl 是 p 的左节点, pr 是 P 的右节点。
// 当 p 的左右节点都不为null
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
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;
}
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;
}
else if (pl != null)
replacement = pl;
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);
}
这里我给大家画个图来解释,比较清楚。以下图的红黑树为例解释。p是要删除的节点。
简单来说,删除p节点,这里的操作就是将p和s的位置交换,让s代替p。
为什么是s这个位置呢。因为s这个节点刚好比pl这边都大,又比pr这边的都小。所以我们首先就要找到pr左边的最后一个左节点。
我们这个图上s还有一个右节点,那这个节点怎么办呢。我们将这个节点给s的父节点,让s的右节点成为s父节点的左节点。
再看代码,删除树上的节点有这几种情况
- p节点的左右节点都存在
- 那么首先找到s节点的位置,
- 如果s等于pr,那么就让p的父节点变成s,s的右节点变成p
- 如果s不等于pr,那么将p节点的父节点变成s节点的父节点sp。也就是sp现在成了p节点的爸爸。p成了sp的左节点。
- pr变成了s节点的右节点
- 将s的右节点赋值给p的右节点
- 将p的左节点赋值给s的左节点
- 将p的父节点赋值给s的父节点
- 那么首先找到s节点的位置,
- p节点的左右节点只有一个存在
- p节点没有左右节点
- 根据replacement判断是直接删除p节点,还是有其他操作。
当然进行删除操作也有可能会破坏树的平衡所以要进行平衡,
这部分我还没看,有时间再去看。