虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。
以上百度百科对红黑树的描述,提到了红黑树复杂,但是查询时间复杂度在O(log n)。
在聊红黑树之前,我们需要理解几个概念,二叉树,二叉搜索树以及平衡二叉树。
二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”
(left subtree)和“右子树”
(right subtree)。
二叉树的结构使用代码可以这么表示
class Entry<K, V> {
K key;
V value;
Entry left;
Entry right;
}
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值。
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
这是二叉搜索树的特点,左节点的元素比当前节点的小,右节点的元素比当前元素大。
如果我们要搜索某个元素,只要对比它比根节点的大小,比根节点大就在右子树,比根节点小就在左子树
虽然,正常情况下,二叉搜索树查找的时间复杂度为 O(log n),但是在元素递增的情况下,二叉搜索树会退化为一个链表结构,搜索的时间复杂度为O(n) 。
平衡树(Balance Tree,BT) ,它的左子树和右子树的深度之差(平衡因子)的绝对值不超过1,且它的左子树和右子树都是一颗平衡二叉树,除此之外,AVL树还必须是一个二叉搜索树。
满足以上2个条件,二叉树就不会出现链表结构,但是,平衡二叉树的维护高度平衡所付出的代价比从中获得的效率收益还大。
红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。当搜索,插入,删除操作较多的情况下,使用红黑树的成本会比AVL树小很多
前三个性质是相当于是对红黑树每个节点的约束,后面两个属性确保了红黑树的平衡,
红黑树的插入和二叉搜索树的插入一致,左节点的元素比当前节点的小,右节点的元素比当前元素大。当插入一个新元素时,红黑树可以通过染色和旋转两种方式,来维护第四、第五特性。
一般情况下,红黑树的新插入的节点都是红色,从第五特性知道,从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。当插入一个黑色节点,必定会破坏规则,插入一个红色节点,除非父节点时一个红色,降低破坏红黑树的可能性。
如图在
如图插入节点100,会导致违背第四特性红色节点子节点必须是黑色节点
这里我们可以尝试将90和60节点染黑,同时70节点染红的方式,调整树平衡
我们尝试将10和20节点变色,调整都会破坏第五、第四特性,
这个时候就需要通过右转进行调整了,同时进行染色调整,最后调整的效果如下图
逆时针旋转两个节点,让父节点被其右子节点取代,而父节点成为右子节点的左子节点。
左旋的gif展示(图片来自网络):
右旋的gif展示(图片来自网络):
顺时针旋转两个节点,让父节点被其左子节点取代,而父节点成为右子节点的右子节点。
在上面的具体的染色和旋转过程中,我没有具体描述如何实现步骤,大家可以参考我画了近百张图来理解红黑树],详细描述了有关旋转这块问题。或者跟着接下来对TreeMap的源码分析,什么时候是染色操作,什么时候是旋转操作。
接下来就是我们这次重点,如何实现一个TreeMap(红黑树)
TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的
Comparator 进行排序,具体取决于使用的构造方法。
源码中TreeMap是继承了AbstractMap类,在分析的时候,直接新建一个类
public class tree<K, V> {
}
//根节点
private transient Entry<K, V> root;
//定义节点黑色
private static final boolean BLACK = true;
//定义节点红色
private static final boolean RED = false;
//map的长度
private static int size;
从 TreeMap的描述可以知道,map可以根据Comparator进行判断,所以我们定义一个变量comparator
private final Comparator<? super K> comparator;
同时,我们直接写好构造方法
//如果外部实现了Comparator类,采用传入的Comparator进行排序
public tree(Comparator<? super K> comparator) {
this.comparator = comparator;
}
//默认Comparator为null
public tree() {
this.comparator = null;
}
tatic final class Entry<K, V> {
//key
private K key;
//值
private V value;
//左子树
private Entry left;
//右子树
private Entry right;
//父节点
private Entry parent;
//默认树的节点都是黑,即null为黑
boolean color = BLACK;
// 构造方法
public Entry() {
}
public Entry(K key, V value, Entry parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
}
其中在源码有个小细节,当我们更新值得时候会返回旧值,这就表明了put是有返回值的,当发生更新值时候,返回旧值。
public V setValue(V value) {
V oldv = this.value;
this.value = value;
return oldv;
}
由于我们只需要了解红黑树的构建过程,只实现了put方法,其实红黑树的删除也是一个复杂的过程,也涉及到了染色和旋转,下次专门写一个文章关于红黑树的删除。
public V put(K key, V value) {
}
当根节点不存在的时候,我们需要构建一个根节点
Entry<K, V> r = root;
//根节点
if (root == null) {
//判断是否为空
compare(key, key);
root = new Entry<>(key, value, null);
size = 1;
return null;
}
compare方法即能满足对空值的判断,也能对两个key进行判断,这个方法是我源码最喜欢的方法之一,以后编码的时候借鉴
private int compare(Object k1, Object k2) {
//这里要求了key是实现了Comparable接口,如果有实现那就调用实现的对比,如果没有那就使用key的实现的compareTo对比方法
return comparator == null ?
((Comparable<? super K>) k1).compareTo((K) k2) :
comparator.compare((K) k1, (K) k2);
}
Entry<K, V> parent;
int cmp;
Comparator<? super K> compare = comparator;
if (compare != null) {
do {
parent = r;
cmp = compare.compare(key, r.key);
if (cmp < 0) {
r = r.left;
} else if (cmp > 0) {
r = r.right;
} else
return r.setValue(value);
} while (r != null);
} else {
if (key == null) {
throw new NullPointerException();
}
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = r;
cmp = k.compareTo(r.key);
if (cmp < 0) {
r = r.left;
} else if (cmp > 0) {
r = r.right;
} else
return r.setValue(value);
} while (r != null);
}
这块代码主要是找到插入节点的父节点,通过对比当前的插入的key和当前的key对比大小,判断插入的节点在当前节点的右边还是左边,这里和二叉搜索树的插入类似
Entry<K, V> parent;
int cmp;
......
do {
parent = r;
cmp = k.compareTo(r.key);
if (cmp < 0) {
//小于0在左边
r = r.left;
} else if (cmp > 0) {
//对于0在右边
r = r.right;
} else
return r.setValue(value);
} while (r != null);
将新插入节点插入到父节点上
Entry<K, V> e = new Entry<>(key, value, parent);
if (cmp < 0) {
parent.left = e;
} else {
parent.right = e;
}
size++;
前面的部分都是搜索二叉树的拆入,当我们节点插入到树上后,会不会对节点的平衡影响?这个时候就要对插入的节点,进行调整
private void fixAfterInsertion(Entry<K, V> x) {
x.color = RED;
//调整方法
{
}
root.color = BLACK;
}
这里是根据默认插入的规则,新节点都是红色节点,以及第三特性,根节点都是黑色,我们直接将最简单的代码先实现。
while (x != null && x != root && x.parent.color == RED) {
}
只要当前元素的父类也是红色,我们才需要进行调整,为什么这么说,红色节点的插入到黑色节点后,并不会破坏树的平衡。
如图
插入65节点,并没有对整个树造成影响
插入22节点,由于父类是红色节点,破了第四特性。需要进行调整
1.判断当前父节点,是在爷节点(父节点的父节点)的左边还是右边,这里会涉及到后面是左旋还是右旋,染色操作和当前节点是否左节点还是右节点无关联。
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {}
这里源码中封装的几个方法
//返回节点右边节点
private static <K, V> Entry<K, V> rightOf(Entry<K, V> x) {
return x == null ? null : x.left;
}
//返回节点的左节点
private static <K, V> Entry<K, V> leftOf(Entry<K, V> x) {
return x == null ? null : x.left;
}
//返回节点父节点
private static <K, V> Entry<K, V> parentOf(Entry<K, V> x) {
return x == null ? null : x.parent;
}
//设置颜色
private <K, V> void setColor(Entry<K, V> x, boolean color) {
if (x != null) {
x.color = color;
}
}
//获取颜色 默认空节点返回黑色节点,符合第四特性
private <K, V> boolean colorOf(Entry<K, V> x) {
return (x == null ? BLACK : x.color);
}
染色的操作都和叔节点的颜色有关系,当父节点和叔节点都是红色,我们需要将父节点和叔节染黑,同时将爷节点染红,返回爷节点,继续进行下个循环的判断
Entry<K, V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x),BLACK);
setColor(y,BLACK);
setColor(parentOf(parentOf(x)),RED);
x=parentOf(parentOf(x));
}
Entry<K, V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x),BLACK);
setColor(y,BLACK);
setColor(parentOf(parentOf(x)),RED);
x=parentOf(parentOf(x));
}
插入34节点,然后染色
为什么要这么操作?我们可以将染色理解为通过颜色变化完成对黑节个数控制的操作;红色节点可以理解为黑色节点的备胎,当我们需要增加黑节节点,它变黑。
我们可以理解为这样一个思路:
虽然40节点和30节点都是红色,我们继续把40节点抛出,进行下一轮的遍历处理
我们得出进行染色的条件:父节点和叔节点是红色
染色的操作流程为
1. 将父节点和叔节点染黑
2. 将爷节点染红
3. 将循环的节点变为爷节点
当30和40都变红时,我们发现满足染色的条件:父节点和叔节点是红色,进行染色
跳出循环时,根节点变黑,相当于全路径添加一个黑色节点,平衡不受影响
说完然后,我们就考虑染色无法解决的问题
染色的条件为:父节点和叔节点是红色
如图,叔节点为为黑色,当我们进行变色操作时,节点90右路径增加一个黑色节点,叔节点无法变色,不满足第三特性(叶节点都是黑色)。这里我们就要考虑创造变色的条件==>旋转
变色的条件时叔节点为红色,目前只要92和100是连接的红色节点,那我们通过旋转让100变为红色叔节点,
这样100变成了92节点的右节点,90是91的左节点,然后染色保证平衡
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
......
}
else{
Entry<K, V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
......
}else{
//当前节点是父节点的左子树,需要右旋
if(x==leftOf(parentOf(x))){
x=parentOf(x);
rotateRight(x);
}
//此时x节点为旋转前父节点,通过右旋,父节点在插入节点的右字子树上,参考下图
setColor(parentOf(x),BLACK);
setColor(parentOf(parentOf(x)),RED);
rotateLeft(parentOf(parentOf(x)));
}
}
这里我们看一下源码如何实现左旋和右旋的,参考之前的动图
private void rotateRight(Entry<K,V> p) {
if (p != null) {
Entry<K, V> l = p.left;
p.left = l.right;
if (l.right != null) {
//更换父节点
l.right.parent = p;
}
//互换父节点
l.parent = p.parent;
if (p.parent == null) {
root = l;
} else if (p == leftOf(parentOf(p))) {
parentOf(p).left = l;
} else {
parentOf(p).right = l;
}
//p的节点换成l
p.parent = l;
//l的左边节点换成p
l.right = p;
}
}
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K, V> r = p.right;
p.right = r.left;
if (r.left != null) {
//更换父节点
r.left.parent = p;
}
//互换父节点
r.parent = p.parent;
if (p.parent == null) {
root = r;
} else if (p == leftOf(parentOf(p))) {
parentOf(p).left = r;
} else {
parentOf(p).right = r;
}
//p的节点换成l
p.parent = r;
//l的左边节点换成p
r.left = p;
}
}
如图我们插入一个35节点到34节点右子树上,我们放大影响平衡的节点
我们分析一下思路:
35节点和34节点肯定有一个节点需要变黑,才能不违背第四特性(每个红色节点的两个子节点都是黑色);
当35和34其中一个节点变黑会影响37节点的第五特性(从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点);37节点的左边增加一个黑色节点,那就需要减少一个黑色节点保证37节点的平衡,有两个思路
如图我们添加一个15节点到20节点的左子树
当我们20和15节点有一个节点必须变黑的时,10节点的右子树增加一个黑色节点,那就必须增加或者减少黑色节点保证平衡。
1.增加5节点为黑色,10节点的左右路径上各增加黑色
2.需要减少一个公共的节点黑色,保证左右路径平衡,10变红
从这个变色思路,我们得出一个规律
1.只在在影响平衡的节点上进行调整,一般是三个
2.调整不平衡节点为一个二叉搜索树,并且左右有一个节点是红色,方便染色操作
1.找出影响的节点,37-34-35
2.调整不平衡节点为一个二叉搜索树
我们去掉其他树的影响,35-34-33 可以构建成这样一个二叉搜索树
35
/ \
34 37
第一步,左旋使得34变成35节点的左子树
第三步染色
1.35和34必须有一个节点变黑,保证第四特性
2.只能35节点变黑,左子树无影响,但是其右路径37是黑色,影响了平衡,将37染红
调整完毕!!!
//如果当前节点在父节点的右边,大于父节点,在构建二叉树的取一个中间值作为父节点
if(x==rightOf(parentOf(x))){
x=parentOf(x);
rotateLeft(x);
}
如果插入节点是x是父节点的右边,根据二叉搜索树的原则,影响的三个节点大小关系
parentOf(x)x
parentOf(parentOf(x))
/
parentOf(x)
\
x
构建新的二叉树时,需要x作为父节点,需要进行调整
一般思路是这样的;x在parentOf(parentOf(x))在左子树parentOf(x)的右子树上,需要进行左右节点旋转。根据需要调整的节点在爷节点的位置,进行节点旋转,具体可以查看之前提到那个文章。
setColor(parentOf(x),BLACK);
setColor(parentOf(parentOf(x)),RED);
rotateRight(parentOf(parentOf(x)));
在源码中,是先进行调整颜色再进行旋转的,这个思路我们可以这么理解
如图插入14节点,由于14 -15-20已经满足二叉搜索的树条件,直接右旋20节点就会变成这样一个二叉树
14和15节点肯定要变黑一个,14和20肯定是15的左右子树;
为了保证红黑树的平衡,增加一个黑色节点,就需要变色一个黑色节点减少影响,由于10节点是红色,只能15变黑,让两个子树中减少一个黑色节点,即20变红。
这里为什么10是红色节点,大家可以根据第四和第五特性自己构建一个红黑树,发现只要发生旋转的节点,必定其10节点这个位置是一个红节点。感觉到了红黑树的神奇之处。
那么,可以得出一个规律,当满足构建二叉树的条件时,从最下面的节点的角度看,我们可以将父节点设置为黑色,爷节点设置为红色,再旋转就可以平衡了。
public class tree<K, V> {
private static final boolean BLACK = true;
private static final boolean RED = false;
private static int size;
/**
* 根节点
*/
private transient Entry<K, V> root;
//对比
private final Comparator<? super K> comparator;
/**
* 构造方法
*
* @param comparator
*/
public tree(Comparator<? super K> comparator) {
this.comparator = comparator;
}
public tree() {
this.comparator = null;
}
public V put(K key, V value) {
Entry<K, V> r = root;
//根节点
if (root == null) {
//判断是否为空
compare(key, key);
root = new Entry<>(key, value, null);
size = 1;
return null;
}
Entry<K, V> parent;
int cmp;
Comparator<? super K> compare = comparator;
if (compare != null) {
do {
parent = r;
cmp = compare.compare(key, r.key);
if (cmp < 0) {
r = r.left;
} else if (cmp > 0) {
r = r.right;
} else
return r.setValue(value);
} while (r != null);
} else {
if (key == null) {
throw new NullPointerException();
}
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = r;
cmp = k.compareTo(r.key);
if (cmp < 0) {
r = r.left;
} else if (cmp > 0) {
r = r.right;
} else
return r.setValue(value);
} while (r != null);
}
Entry<K, V> e = new Entry<>(key, value, parent);
if (cmp < 0) {
parent.left = e;
} else {
parent.right = e;
}
size++;
//调整树
fixAfterInsertion(e);
return null;
}
//调整树结构为红黑树
private void fixAfterInsertion(Entry<K, V> x) {
//默认插入的节点都是红色
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
//判断当前节点是父亲节点的左节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K, V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x),BLACK);
setColor(y,BLACK);
setColor(parentOf(parentOf(x)),RED);
x=parentOf(parentOf(x));
}else {
if(x==rightOf(parentOf(x))){
x=parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x),BLACK);
setColor(parentOf(parentOf(x)),RED);
rotateRight(parentOf(parentOf(x)));
}
}else{
Entry<K, V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x),BLACK);
setColor(y,BLACK);
setColor(parentOf(parentOf(x)),RED);
x=parentOf(parentOf(x));
}else{
if(x==leftOf(parentOf(x))){
x=parentOf(x);
rotateRight(x);
}
setColor(parentOf(x),BLACK);
setColor(parentOf(parentOf(x)),RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}
//左旋
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K, V> r = p.right;
p.right = r.left;
if (r.left != null) {
//更换父节点
r.left.parent = p;
}
//互换父节点
r.parent = p.parent;
if (p.parent == null) {
root = r;
} else if (p == leftOf(parentOf(p))) {
parentOf(p).left = r;
} else {
parentOf(p).right = r;
}
//p的节点换成l
p.parent = r;
//l的左边节点换成p
r.left = p;
}
}
//右旋
private void rotateRight(Entry<K,V> p) {
if (p != null) {
Entry<K, V> l = p.left;
p.left = l.right;
if (l.right != null) {
//更换父节点
l.right.parent = p;
}
//互换父节点
l.parent = p.parent;
if (p.parent == null) {
root = l;
} else if (p == leftOf(parentOf(p))) {
parentOf(p).left = l;
} else {
parentOf(p).right = l;
}
//p的节点换成l
p.parent = l;
//l的左边节点换成p
l.right = p;
}
}
private <K, V> void setColor(Entry<K, V> x, boolean color) {
if (x != null) {
x.color = color;
}
}
private <K, V> boolean colorOf(Entry<K, V> x) {
return (x == null ? BLACK : x.color);
}
private static <K, V> Entry<K, V> rightOf(Entry<K, V> x) {
return x == null ? null : x.left;
}
private static <K, V> Entry<K, V> leftOf(Entry<K, V> x) {
return x == null ? null : x.left;
}
private static <K, V> Entry<K, V> parentOf(Entry<K, V> x) {
return x == null ? null : x.parent;
}
private int compare(Object k1, Object k2) {
return comparator == null ? ((Comparable<? super K>) k1).compareTo((K) k2) :
comparator.compare((K) k1, (K) k2);
}
/**
* 1.变量
* 2.构造树
* 3.调整树
*/
//树的对象
static final class Entry<K, V> {
private K key;
private V value;
private Entry left;
private Entry right;
private Entry parent;
//默认树的节点都是黑,即null为黑
boolean color = BLACK;
public Entry() {
}
public Entry(K key, V value, Entry parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
V oldv = this.value;
this.value = value;
return oldv;
}
}
public static void main(String[] args) {
tree<Integer, Integer> tree = new tree();
tree.put(0, 0);
tree.put(-1, -1);
tree.put(1, 1);
tree.put(2, 2);
tree.put(3, 3);
tree.put(4, 5);
}
}
第一次写这么长的篇幅描述自己对源码的理解,存在错别字或者语法问题,后期,我会慢慢校验!!!