上文我们成功构建了红黑树,接下来我们将对红黑树进行代码实现。在代码实现前,我们需要图像添加过程规律进行总结,方便我们进行代码实现。
红黑树性质:
1.每个结点要么是红的要么是黑的。
2.根结点是黑的。
3.如果一个结点是红的,那么它的两个儿子都是黑的。(红色不相连)
4.对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。
1.PUT规律
通过构建过程可以看出我们所以的操作还是围绕红黑树与234树的节点对应关系进行操作的,所以我们还是回归到对于2节点,3节点和4节点添加过程进行规律总结。
2节点 新增 :
二节点+新增节点:上黑下红,不需要调整
3节点新增:
三节点+新增节点:根据基本性质,只有中间3黑2、4红满足,其他形态都需要通过旋转变色后才能满足。3黑2、4红也是才是正真的稳定态。
4节点新增:分裂态
分裂态 :只需要将对应节点变色便可以平衡
对红黑树的添加元素其实质就是对于2节点,3节点和4节点添加,通过观察总结以下规律:
1.新增节点时,先以红色节点挂载到红黑树上,在进行调整操作
2.只有父节点为红色节点时才会进行调整
3.2节点+新增节点情况,不需要进行调整。
4.3节点+新增节点。当新增后无叔叔节点时需要进行调整,且2,4状态时需要先调整父节点后才方便调整到稳定态
5.裂变态+新增节点:新增后有叔叔节点,变色调整。但是这里由于3元素节点进行了变色,3元素之前的平衡可能给打破,这里需要向上递归来进行平衡校验
添加过程的总体操作是:先添加 在调整
2节点添加:添加后不处理
3节点添加:需要处理,需要判断什么情况是3节点-》没有叔叔节点则是3节点
4节点添加:裂变太需要处理,如何判断4节点 -》存在叔叔节点则是4节点
2.代码实现
1.红黑树节点:节点添加是双向的,所以需要指向父节点
class RBNode<K extends Comparable<K>, V> {
private K key;
private V value;
private boolean color;
private RBNode parent;
private RBNode left;
private RBNode right;
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
public boolean isColor() {
return color;
}
public void setColor(boolean color) {
this.color = color;
}
public RBNode getParent() {
return parent;
}
public void setParent(RBNode parent) {
this.parent = parent;
}
public RBNode getLeft() {
return left;
}
public void setLeft(RBNode left) {
this.left = left;
}
public RBNode getRight() {
return right;
}
public void setRight(RBNode right) {
this.right = right;
}
public RBNode(K key, V value, boolean color, RBNode parent, RBNode left, RBNode right) {
this.key = key;
this.value = value;
this.color = color;
this.parent = parent;
this.left = left;
this.right = right;
}
public RBNode(K key, V value, RBNode parent) {
this.key = key;
this.value = value;
this.color = RED;
this.parent = parent;
this.left = null;
this.right = null;
}
}
2.put
public void put(K key, V value) {
//判断根为空不
RBNode target = this.root;
if (target == null) {
root = new RBNode(key, value, null);
return;
}
//命名双亲节点,记录上一个节点是谁,因为要遍历到null节点
RBNode parent;
int cmp;
do {
parent = target;
cmp = key.compareTo((K) target.key);
if (cmp > 0) {
target = target.right;
} else if (cmp < 0) {
target = target.left;
} else {
target.setValue(value);
return;
}
} while (target != null);
RBNode<K, V> e = new RBNode<>(key, value, parent);
if (cmp > 0) {
parent.right = e;
} else if (cmp < 0) {
parent.left = e;
}
//添加后调整
fixAfterPut(e);
}
调整:
/**
*
* pr pr
* p p / /
* \ \ p p
* pr pr / \
* \ / pl pr
* rr rl
* @param p
*/
//注意以上四种形态需要进行旋转调整位置
/**
* 2节点+新增元素:新红父黑 --不需要调整
*
*3节点+新增元素:
*红色节点+上黑下红 =》排序后中间节点为黑色,两边节点为红色
*
* 4节点+新增元素:开始分裂,中间元素升级为父节点,新增元素于剩下其中一个合并
* 红黑树:新增红色节点+爷爷节点黑,父节点和叔叔节点都是红色 =》爷爷节点变红,父亲和叔叔节点变黑 如果爷爷是根节点,则爷爷变黑
*
*
*/
private void fixAfterPut(RBNode x) {
//当前节点不为空 ,根节点不是根节点,x的父节点为红色
while (x != null && x != root && x.parent.color == RED) {
//1.处理左三
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//叔叔节点为空和不为空(
RBNode uncle = rightOf(parentOf(parentOf(x)));
//(4节点情况) 这里没有必要判断uncle为空,因为空为黑色
if (colorOf(uncle) == RED) {
setRedColor(parentOf(parentOf(x)));
setBlackColor(parentOf(x));
setBlackColor(uncle);
//这里涉及到分裂的情况,分裂后爷爷之前的也许打破了平衡,
// 所以需要把x指向爷爷,然后向上递归
x = parentOf(parentOf(x));
} else {
//(3节点情况)
//判断是不是左右(变种)
if (x == rightOf(parentOf(x))) {
//x= parentOf(x); //x的父亲为x ,不会打破平衡,所以不需要递归
rotateLeft(parentOf(x)); //将结构改为左左
}
//变色
setBlackColor(parentOf(x));
setRedColor(parentOf(parentOf(x)));
//针对爷爷you旋转
rotateRight(parentOf(parentOf(x)));
}
} else {
//2.处理右三
//叔叔节点为空和不为空(
RBNode uncle = leftOf(parentOf(parentOf(x)));
//(4节点情况) 这里没有必要判断uncle为空,因为空为黑色
if (colorOf(uncle) == RED) {
setRedColor(parentOf(parentOf(x)));
setBlackColor(parentOf(x));
setBlackColor(uncle);
//这里涉及到分裂的情况,分裂后爷爷之前的也许打破了平衡,
// 所以需要把x指向爷爷,然后向上递归
x = parentOf(parentOf(x));
} else {
//(3节点情况)
//判断是不是右左(变种)
if (x == leftOf(parentOf(x))) {
//x= parentOf(x); //x的父亲为x ,不会打破平衡,所以不需要递归
rotateRight(parentOf(x)); //将结构改为右右
}
//变色
setBlackColor(parentOf(x));
setRedColor(parentOf(parentOf(x)));
//针对爷爷zuo旋转
rotateLeft(parentOf(parentOf(x)));
}
}
}
//将根节点变黑
setBlackColor(root);
}
3.HashMap源码
当HashMap桶中的元素个数超过一定数量时,就会将链表转化为红黑树的结构,具体源码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...省略部分代码...
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//当桶中元素个数超过阈值(8)时就进行树化
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
...省略部分代码...
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//将节点替换为TreeNode
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null) //hd指向头结点
hd = p;
else {
//这里其实是将单链表转化成了双向链表(方便向tl的前一个位置插入)
//tl是p的前驱,每次循环更新指向双链表的最后一个元素,用来和p相连,p是当前节点
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
//将链表进行树化
hd.treeify(tab);
}
}
从代码中可以看到,在treeifyBin函数中,先将所有节点替换为TreeNode,然后再将单链表转为双链表,方便之后的遍历和移动操作,。而最终的操作,实际上是调用TreeNode的方法treeify进行的。
final void treeify(Node<K,V>[] tab) {
//树的根节点
TreeNode<K,V> root = null;
//x是当前节点,next是后继
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
//如果根节点为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;;) {
//p指向遍历中的当前节点,x为待插入节点,k是x的key,h是x的hash值,ph是p的hash值,dir用来指示x节点与p的比较,-1表示比p小,1表示比p大,不存在相等情况,因为HashMap中是不存在两个key完全一致的情况。
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
//如果hash值相等,那么判断k是否实现了comparable接口,如果实现了comparable接口就使用compareTo进行进行比较,如果仍旧相等或者没有实现comparable接口,则在tieBreakOrder中比较
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);
}
//这里不是为了整体排序,而是为了在插入中保持一致的顺序
static int tieBreakOrder(Object a, Object b) {
int d;
//用两者的类名进行比较,如果相同则使用对象默认的hashcode进行比较
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
这里的逻辑其实不复杂,仅仅是循环遍历当前树,然后找到可以该节点可以插入的位置,依次和遍历节点比较,比它大则跟其右孩子比较,小则与其左孩子比较,依次遍历,直到找到左孩子或者右孩子为null的位置进行插入。
balanceInsertion函数,将红黑树进行插入平衡处理,保证插入节点后仍保持红黑树的性质。
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true;
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
if ((xp = x.parent) == null) { //插入节点为根节点,变色即可
x.red = false;
return x;
}
else if (!xp.red || (xpp = xp.parent) == null) //父节点是黑色,不需要调整
return root;
if (xp == (xppl = xpp.left)) { // 其父节点是祖父节点的左节点
if ((xppr = xpp.right) != null && xppr.red) {// 裂变态 插入的节点父节点和祖父节点都存在,变色即可
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp; //变色后 x位置到爷爷位置,向上递归
}
else {
if (x == xp.right) { //3节点新增 左右态 插入节点有父亲右边 ,,需要在父节点旋转成 左左
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) { //3节点新增 左左态
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);
}
}
}
}
}
}
这个函数稍后在TreeNode的插入中进行介绍,这里先看看moveRootToFront,这个函数是将root节点移动到桶中的第一个元素,也就是链表的首节点,这样做是因为在判断桶中元素类型的时候会对链表进行遍历,将根节点移动到链表前端可以确保类型判断时不会出现错误。
/**
* 把给定节点设为桶中的第一个元素
*/
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;
//first指向链表第一个节点
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
//如果root不是第一个节点,则将root放到第一个首节点位置
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;
}
//这里是防御性编程,校验更改后的结构是否满足红黑树和双链表的特性
//因为HashMap并没有做并发安全处理,可能在并发场景中意外破坏了结构
assert checkInvariants(root);
}
}
由此我们变明白红黑树的整个插入过程是如何进行的。但是正真有难度的是红黑树的删除,但是明白红黑树节点等级关系后,删除也不是什么难题。后续我们会进一步讲解红黑树的删除