Java HashMap红黑树学习
- 一、红黑树介绍
- 二、红黑树的基本操作
- 2.1 旋转
- 2.1.1 左旋
- 2.1.2 右旋
- 2.2 添加
- 2.3 删除
一、红黑树介绍
(1)红黑树(Red-Black Tree,简称R-B Tree),是一种特殊的平衡二叉查找树。
(2)节点非黑即红
(3)根节点是黑的
(4)叶子节点也是黑的 [注意:这里叶子节点,是指为空的叶子节点!]
(5)如果一个节点是红色的,则它的子节点必须是黑色的。
(6)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。(确保没有一条路径 会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。)
(7)时间复杂度:O(log n)
二、红黑树的基本操作
2.1 旋转
旋转的目的是让树保持红黑树的特性:
- 添加或删除红黑树中的节点之后,红黑树就发生变化,可能不满足红黑树的5条性质,就不再是红黑树,而是一颗普通的树。而通过旋转,可以使这颗树重新成为红黑树。
- 旋转包括两种:左旋 和 右旋。
2.1.1 左旋
变为
把主节点E左旋变成左节点,右节点S左旋变成主节点
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> y, pp, yl;
// 1. 先检查 P 是不是空,并且 P的右边有东西
if (p != null && (y = p.right) != null) {
// 2. P 的右指针指向了 Y 的左节点
if ((yl = p.right = y.left) != null)
// 3.原先 Y 的左节点换父节点
yl.parent = p;
// 4. Y 的父节点换成了 P 的父节点。
if ((pp = y.parent = p.parent) == null)
// 假如 “原先 root 指向 P”,进入此代码
(root = y).red = false;
// 5. 顶部父节点换孩子节点(原先孩子节点是 P, 现在要变成 Y)
// 先判断原先孩子节点P到底是左孩子节点,还是右孩子节点,然后再替换
else if (pp.left == p)
// 原先是左孩子节点
pp.left = y;
else
// 原先是右孩子节点
pp.right = y;
// 6. Y 的左孩子节点变成了 P
y.left = p;
// 7. P 的父节点变成了 Y
p.parent = y;
}
return root;
}
2.1.2 右旋
变成
同理:
static <K,V> HashMap.TreeNode<K,V> rotateRight(HashMap.TreeNode<K,V> root,
HashMap.TreeNode<K,V> p) {
HashMap.TreeNode<K,V> x, pp, lr;
// 1. 先检查 P 是否为空,并且检查 P 是否有左孩子节点
if (p != null && (x = p.left) != null) {
// 2. P 的左指针指向了 X 的右孩子节点
if ((lr = p.left = x.right) != null)
// 3. 原先 X 的右节点换父节点
lr.parent = p;
// 4. X 的父节点换成了 P 的父节点
if ((pp = x.parent = p.parent) == null)
// 假如 “原先 root 指向 P”,进入此代码
(root = x).red = false;
else if (pp.right == p)
// 原先是右孩子节点
pp.right = x;
else
// 原先是左孩子节点
pp.left = x;
// 6. X 的右孩子节点变成 P
x.right = p;
// 7. P 的父节点变成了 X
p.parent = x;
}
return root;
}
2.2 添加
- 将红黑树当作一颗二叉查找树,将节点插入。
- 将插入的节点着色为"红色"。
- 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。
/*
* 将结点插入到红黑树中
*
* 参数说明:
* node 插入的结点
*/
private void insert(RBTNode<T> node) {
// 1. 设置节点的颜色为红色
node.color = RED;
int cmp; // 节点的比较结果 0 1 -1
RBTNode<T> y = null; // 缓冲使用,记录上一个节点
RBTNode<T> x = this.mRoot;
// 2. 将红黑树当作一颗二叉查找树,将节点添加到二叉查找树中。
while (x != null) { // 遍历到最后,没有左孩子或者右孩子了,退出
y = x;// 缓冲使用,记录上一个节点
cmp = node.key.compareTo(x.key);
if (cmp < 0)
x = x.left; // node 比 X 小,那么往左边遍历
else
x = x.right;// node 比 X 大,那么往右边遍历
}
//已经找到并且退出,此时认爸爸,指向了上一个节点 y
node.parent = y;
if (y!=null) { // 为 null 表明是最顶部了
cmp = node.key.compareTo(y.key); // 看下插入的节点是y的左孩子还是右孩子
if (cmp < 0)
y.left = node;
else
y.right = node;
} else {
this.mRoot = node;
}
// 3. 将它重新修正为一颗二叉查找树
insertFixUp(node);
}
2.3 删除
删除节点的情形:
(1)被删除节点没有孩子节点,即为叶节点。【直接将该节点删除即可】
(2)被删除节点只有一个孩子节点。【直接删除该节点,并用该节点的唯一子节点顶替它的位置】
(3)被删除节点有两个孩子节点。【先找出它的后继节点;然后把“它的后继节点的内容”复制给“该节点的内容”;之后,删除“它的后继节点”。】
/**
* @param map 用于树化或者链化
* @param tab 桶数组,该 node 位于某一个桶内
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
// ====== section 1:通过prev和next删除当前节点 ======
int n;
if (tab == null || (n = tab.length) == 0)
return;
// 获取桶 index
int index = (n - 1) & hash; // 用与字符取余,看我之前的文章
// first 指向了桶的第一个元素,可能是链表头,也可能是树的 root节点 ,此处是树的根节点
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], rl;
TreeNode<K,V> root = first; // root 指向了 first
TreeNode<K,V> rl; //
// succ 指向了 Node 节点的后继节点 next,对于二叉查找树来说,就是右子树的最左节点 。
TreeNode<K,V> succ = (TreeNode<K,V>)next;
// pred 指向了 this节点的前驱节点
TreeNode<K,V> pred = prev;
// 如果前驱结点是 null,那么他肯定是root节点
if (pred == null)
// 首先 first先指向 succ,也就是this节点的后继节点next
// 然后 tab[index] 指向了 first指向的位置,也就是指向了 this节点的 next
tab[index] = first = succ; // 此处说明一下,假如 succ为空,那么代表这棵树啥都没了
else
// 不是root节点,那么前驱结点的next后继指针指向了this的后继节点,意思就是断开了this节点
pred.next = succ;
// 看上面 succ 原本指向了 this节点的后继节点 next,如果不为空,说明 succ 是一个节点
if (succ != null)
// this的后继节点的 prev前驱指针指向了 this节点的前驱节点,照样是断开了 this节点
succ.prev = pred;
// 这棵树啥都不剩下了。直接 return
if (first == null)
// ************ 此处有 return ************
return;
// ====== section 2:当节点数量小于7时转换成链栈的形式存储 ======
// root的爸爸不是null,那证明不是根节点,那么继续往上面找上去,直到找到真正的 root
if (root.parent != null)
root = root.root();
// 树是空
if (root == null
// 可以移动 并且
|| (movable
// root节点的右子树是空,
&& (root.right == null
// 或者root节点的左子树是空 ,rl指向了左子树
|| (rl = root.left) == null
// 或者左子树的左子树是空
|| rl.left == null))) {
tab[index] = first.untreeify(map); // too small ,此轮条件不判断树种节点的总数量,只判断根的左右子树是否符合链化的条件
// ************ 此处有 return ************
return;
}
// ====== section 3:判断当前树节点情况 ======
// p 指向当前节点, pl 当前Node的左子树,pr当前Node的右子树
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
// 有俩孩子,属于情形三,那么就必须找到右子树的最左节点,也就是转化为情形1或者情形2
if (pl != null && pr != null) {
// s 指向thisNode 的右子树
TreeNode<K,V> s = pr, sl;
// s 是为了找到最左节点
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; // swap colors todo
// sr 指向了最左节点的右子树,因为 s已经是最左节点了,所以s肯定没有左子树
TreeNode<K,V> sr = s.right;
// pp 指向了 p的父亲(p在上面指向了this节点)
TreeNode<K,V> pp = p.parent;
// s是当前节点右子树的最左节点,pr是当前节点的右子树,s == pr 说明右子树的最左节点就是当前节点的右子树
// demo:看上图的红黑树,假如要删除“18”,那么“18”的s 右子树的最左节点是“19”,pr是当前节点的右子树也是“19”,那么他们俩互相换位置
if (s == pr) { // p was s's direct parent
// 那么互换位置
p.parent = s;
s.right = p;
}
// 如果不是上面那个情况,比如此时要删除的是节点“18”,那么
else {
// s是当前节点右子树的最左节点,此时s是节点“15”
// sp是节点“15”的爸爸节点“16”,那么此时要把 节点“18” 和s 节点“15”换个位置
TreeNode<K,V> sp = s.parent;
// 当前节点与右子树的最左节点换位置
// 当前节点换爸爸
if ((p.parent = sp) != null) {
// 替换元素换儿子
// s 是 sp的左孩子,
if (s == sp.left)
sp.left = p;
// s 是 sp的右孩子
else
sp.right = p;
}
// s的右指针指向了原先节点的右子树,也就是接手他的右子树
if ((s.right = pr) != null)
// 原本的右子树指针换个爸爸
pr.parent = s;
}
// 当前这个要删除的节点已经换到右子树的最左节点了,那么他肯定没有左孩子
p.left = null;
// 假如删除的节点还有右孩子,那么就接住
// 比如要删除的节点是“4”,那么把“4”和“5”换位置之后,原先“4”的右子树还是得接住
if ((p.right = sr) != null)
sr.parent = p;
// s目前已经是替换位置完毕的了,已经上位的了,如果s有左子树,那么接住
if ((s.left = pl) != null)
pl.parent = s;
// s目前已经是替换位置完毕的了,如果他的爸爸是null,那么s就是根节点root
if ((s.parent = pp) == null)
root = s;
// 如果一开始要删除的节点p位于他父亲pp的 左边,那么接住
else if (p == pp.left)
pp.left = s;
// 如果一开始要删除的节点p位于他父亲pp的 右边,那么也接住
else
pp.right = s;
// 如果“替换节点”有右孩子,那么替换他的右边孩子。
// 比如上图,你要删除的是p节点“14”,那么找到右子树的最左节点s是“11”,那么此时要把他的右孩子“13”给换上去
if (sr != null)
replacement = sr;
else
// 如果“替换节点”没有右孩子,那么替换节点就是她自己
// 比如上图,你要删除的p节点是“6”,由于他没有右边的孩子,所以把“5”直接换了即可
replacement = p;
}
// 情形1,只有一个孩子,那么直接替换即可
else if (pl != null)
replacement = pl;
// 情形1,只有一个孩子,那么直接替换即可
else if (pr != null)
replacement = pr;
// 没有左子树 并且没有右子树 ,那么属于情形1,直接删除该节点即可
else
replacement = p;
// ====== section 4:实现删除树节点逻辑 ======
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);
}