您的位置:首页 > 其它

TreeMap及其红黑树算法个人解析

2018-01-24 16:43 525 查看
  工作中,有时会遇到需要对map进行排序的情况,java中常用的两种带排序的map,一种是LinkedHashMap,而另外种就是TreeMap了,LinkedHashMap是基于散列进行存储的,这里不过多讨论了。而TreeMap则是使用红黑树来实现排序的,这里我们重点研究下TreeMap的实现。

红黑树的基本概念

百度百科红黑树

  

TreeMap基本数据结构

为了实现红黑树,TreeMap定义的静态内部类entry如下

static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
//左孩子节点,存放比当前节点小的节点
Entry<K,V> left = null;
//右孩子节点,存放比当前节点大的节点
Entry<K,V> right = null;
/**
*父节点,理论上只需要左右孩子指针,就可以生成一棵树,但是红黑树进行排序时,
*涉及到节点换位,所以引入父指针来实现双向指针,方便换位。
*/
Entry<K,V> parent;
//当前节点颜色
boolean color = BLACK;

Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
}


而TreeMap中的一些基本属性如下

public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
/**
* 排序器,方便使用者自定义treeMap排序规则
* 如果为null,则使用自然排序
*/
private final Comparator<? super K> comparator;
//树的根节点
private transient Entry<K,V> root = null;
//TreeMao中数据的个数
private transient int size = 0;
//对TreeMap更新的次数
private transient int modCount = 0;
}


TreeMap put方法

描述完TreeMap的基本结构,现在步入正题,解析put方法吧,

TreeMap的put方法代码如下

public V put(K key, V value) {
Entry<K,V> t = root;
//如果根节点为null,这当前节点直接设置成根节点,然后返回。
if (t == null) {
compare(key, key);
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
//cmp即为2个节点key比较后的大小返回值,<0为小,=0为相等,>0为大
int cmp;
//新插入节点的父节点
Entry<K,V> parent;
// 获取排序器
Comparator<? super K> cpr = comparator;
//排序器不为null情况下,进行设置节点
if (cpr != null) {
/**
*此循环用来查找节点适合插入的位置,从根节点进行遍历,
*如果插入新节点的key小于遍历的当前节点的key,则向左子节点
*继续进行遍历,如果大于则向右子节点进行遍历,等于则用新插入
*的节点替换当前遍历的节点,然后返回,如遍历到叶子节点(注:
*这里的叶子节点指的是有数据,但是子节点都为null的节点,而
*TreeMap的红黑树算法中,默认红黑树叶子节点都为null,以满
*足红黑树性质,同时方便运算),TreeMap中不存在插入key,
*则循环结束。定位出新插入节点的父节点
*/
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
//排序器为null情况下,采用key的自然排序
Comparable<? super K> k = (Comparable<? super K>) key;
//循环作用与上述描述一致
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
//如果put运行到这里,则代表树中不存在与插入key相同key的节点,
//所以需要新增节点,新增节点都是作为树的叶子节点添加
Entry<K,V> e = new Entry<>(key, value, parent);
//判断新增节点是父节点的左节点还是右节点
if (cmp < 0)
parent.left = e;
else
parent.right = e;
//核心算法,用来对二叉树进行平衡,也即是红黑树算法。
fixAfterInsertion(e);
size++;
modCount++;
return null;
}


TreeMap 红黑树插入算法

这里我们先讲完fixAfterInsertion方法是怎么实现新增节点,保证红黑树性质不变的,然后再描述为何不使用普通的二叉树树或平衡二叉树(avl树)。fixAfterInsertion代码如下,虽然代码不多,但是理解起来颇为艰难。

红黑树的性质还是简单在这里提一下吧,方便对照代码

性质1. 节点是红色或黑色。

性质2. 根节点是黑色。

性质3 每个叶节点(NIL节点,空节点)是黑色的。

性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

/**
*注意此算法为CLR算法(按根左右的方式遍历)
*注意此算法中,每个树节点都有左右两个子节点,尽管其中的一个或两个可能是空叶子
*请注意分析put方法,添加的x节点一定位于树的最后一层
*/
private void fixAfterInsertion(Entry<K,V> x) {
//每个新增非null节点先默认为红
x.color = RED;
/**
*此循环判断遍历节点x(初始为新增节点)的位置及颜色是否合理,不合理则调整,调整完后,为了保证调整后的树也满足红黑树性质,需再向上遍历将x赋值为x.parent或x.parent.parant,再次遍历调整。循环终止条件为遍历的节点为根节点,或x的父节点不为红(因为父节点为黑时,直接在父节点下添加红色的子节点,依旧满足红黑树的性质),当满足此条件时,整棵树已经满足了红黑树性质。此算法循环中的操作最多只涉及到三层,即x层,x.parent层,x.parent.parent层,如果x.parant.parent存在,则是调整以x.parant.parant为根的三层树结构中的节点,保证这棵子树维持红黑树性质。
*/
while (x != null && x != root && x.parent.color == RED) {
//条件a:判断x的父节点是否为当前子树的左节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
/**
设置y为x父节点的右兄弟节点(因为条件1已经决定如果进入此分支,
*则x父节点一定位于以x.parent.parent为根的左子树上面)
*/
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//条件b:判断父节点右兄弟节点是否为红
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
//条件c:判断x节点是否为父节点的右节点
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
/**
设置y为x父节点的左兄弟节点(因为条件1已经决定如果进入此分支,
*则x父节点一定位于以x.parent.parent为根的右子树上面)
*/
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
//条件d:判断父节点左兄弟节点是否为红
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
//条件e:判断x节点是否为父节点的左节点
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
//保证红黑树根节点始终为黑色,维持性质2
root.color = BLACK;
}


分析上述代码由于while循环体内,最多只对以x.parent.parent为根的三层树进行操作

(如果不存在,则操作的是x.parent为根的两层树),而由条件abcde则分出6种可能

分支(注意这些分支隐含默认条件为x父节点为红)

分支1.x父左,x父兄红(处理:将x父节点,x的父兄节点,x父节点的父节点变色,使以x.parent.parent为根的三层子树满足红黑树性质,因为x.parent.parent的颜色发生了改变,如果x.parent.parent存在父节点且颜色为红,会导致红黑树性质改变,所以将x赋值为x.parent.parent,再次执行循环判断)

分支2.x父左,x父兄黑,x右(处理:将x赋值为x.parent,然后对x进行使树的节点相对位置颜色不变,此时已经将x赋值为)

分支3.x父左,x父兄黑,x左(处理:先修改相应节点颜色,然后对x.parent.parent右旋转,最终使树的节点相对位置颜色不变,但是部分节点位置发生改变)

分支4.x父右,x父兄红

分支5.x父右,x父兄黑,x右

分支6.x父右,x父兄黑,x左

这6种分支已经包含了在一个三层的红黑树中,在父节点为红色的情况下(在TreeMapd的插入算法中,因为插入的新节点都默认为红色,如果父节点为黑色直接插入节点不影响红黑树性质,而如果父节点为红色,则不满足红黑树性质4,所以需要调整),新增节点所产生的所有可能性,现在我们就通过具体的例子来更加深入理解这个方法吧,这里我选取的是按一定规律插入一组key,来覆盖每一种可能分支,这组key是:128,64,192,32,48,16,8,160,224,232,228,240,230,236

插入过程如图



图中圈住的图,代表插入节点破坏了红黑树性质,然后调整节点满足红黑树性质的过程。(图片中只给出了每次循环的初始状态和最终状态)

插入128:作为根节点直接插入

插入64:父节点为黑,直接插入

插入192:父节点为黑,直接插入

插入32:x=32,满足分支1,按分支1进行处理,对128,64,192进行颜色反转,然后x=128,因为128为根节点,循环结束,结束循环后将root设置为黑色,虽然左右图对比,128颜色未变,实际上代码中128进行2次颜色反转,最终树形态如右图。

插入48:x=48,满足分支2,分支2进行了3步处理,第一步x=32,然后对x进行左旋操作,第二步对x.parant(48),x.parant.parant(64)进行颜色反转,第三步对x.parent.parent(64)进行右旋操作。x.parent(48).color为黑色,循环结束。

插入16:x=16,满足分支1,按分支1处理,过程就不再描述,大家直接看结果图即可

插入8:x=8,满足分支3,按分支3处理,第一步对x.parent(16),x.parent.parent(32)进行颜色反转,第二步对x.parent.parent(32)进行右旋操作。x.parent(16).color为黑色,循环结束。因分支三操作实际上是分支2后两步操作,所以不再给出中间图,直接给出开始和结果图,过程参照插入48第二图开始。

父节点为左的分支插入我们已经全部了解了,现在让我们看看父节点为右的插入,接上图最终形态继续插入160,224,232,228,240,230,236,对于左边不再变化的节点树,直接用子树代替了。



插入160:父节点为黑,直接插入

插入224:父节点为黑,直接插入

插入232:x=232,满足分支4,对192,160,224进行颜色反转,然后x=192,x.parent(128)为黑,循环结束

插入228:x=228,满足分支5,第一步x=x.parent(232),对x(232)右旋,第二步x.parent(228),x.parent.parent(224)进行颜色反转,第三步x.parent.parent(224)左旋

,此时x.parent(228)为黑,循环结束。

下面是插入240,230,236



插入240:x=240,满足分支4,x.parent(232),x.parent.parent(228),x.parent.parent.left(224)进行颜色反转,x=x.parent.parent(228),继续循环,满足分支4,x.parent(192),x.parent.parent(128),x.parent.parent.left(48)颜色反转,x=x.parent.parent(128),x为root,循环结束,root颜色设置为黑色。

插入230:父节点为黑,直接插入

插入236:x=236,满足分支4,x.parent(240),x.parent.parent(232),x.parent.parent.le
8673
ft(230)进行颜色反转,x=x.parent.parent(232),继续循环,满足分支6,x.parent(228),x.parent.parent(192)颜色反转,x=x.parent.parent(192)左旋,x.parent(228)为黑色,循环结束。

TreeMap remove

TreeMap使用红黑树的理由

至此,TreeMap中的红黑树插入算法可以算解析完毕了,我们已经了解TreeMap中是怎么实现红黑树的插入的。排序算法有多种,为何TreeMap采用红黑树算法呢,红黑树的本身性质是其中很重要的因素,由于红黑树的性质,在查找,插入,及删除时,红黑树的时间复杂度都为O(logn),此外,由于它的设计,任何不平衡都会在三次旋转之内解决。虽然还存在一些更复杂的数据结构,能够在一次旋转就实现平衡,但是红黑树本身在性能和复杂度上相对而言更均衡。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: