博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
史上最清晰的红黑树讲解(上)
阅读量:6657 次
发布时间:2019-06-25

本文共 5773 字,大约阅读时间需要 19 分钟。

hot3.png

本文以Java TreeMap为例,从源代码层面,结合详细的图解,剥茧抽丝地讲解红黑树(Red-Black tree)的插入,删除以及由此产生的调整过程。

总体介绍

Java TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的评判可以通过其本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator)。

TreeMap底层通过红黑树(Red-Black tree)实现,也就意味着containsKey()get()put()remove()都有着log(n)的时间复杂度。其具体算法实现参照了《算法导论》。

TreeMap_base.png

出于性能原因,TreeMap是非同步的(not synchronized),如果需要在多线程环境使用,需要程序员手动同步;或者通过如下方式将TreeMap包装成(wrapped)同步的:

SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

红黑树是一种近似平衡的二叉查找树,它能够确保任何一个节点的左右子树的高度差不会超过二者中较低那个的一陪。具体来说,红黑树是满足如下条件的二叉查找树(binary search tree):

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点必须是黑色
  3. 红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色)。
  4. 对于每个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点。

在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件3或条件4,需要通过调整使得查找树重新满足红黑树的条件。

预备知识

前文说到当查找树的结构发生改变时,红黑树的条件可能被破坏,需要通过调整使得查找树重新满足红黑树的条件。调整可以分为两类:一类是颜色调整,即改变某个节点的颜色;另一类是结构调整,集改变检索树的结构关系。结构调整过程包含两个基本操作:左旋(Rotate Left),右旋(RotateRight)

左旋

左旋的过程是将x的右子树绕x逆时针旋转,使得x的右子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。

TreeMap_rotateLeft.png

TreeMap中左旋代码如下:

//Rotate Leftprivate void rotateLeft(Entry
p) { if (p != null) { Entry
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.parent.left == p) p.parent.left = r; else p.parent.right = r; r.left = p; p.parent = r; }}

右旋

右旋的过程是将x的左子树绕x顺时针旋转,使得x的左子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。

TreeMap_rotateRight.png

TreeMap中右旋代码如下:

//Rotate Rightprivate void rotateRight(Entry
p) { if (p != null) { Entry
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.parent.right == p) p.parent.right = l; else p.parent.left = l; l.right = p; p.parent = l; }}

方法剖析

get()

get(Object key)方法根据指定的key值返回对应的value,该方法调用了getEntry(Object key)得到相应的entry,然后返回entry.value。因此getEntry()是算法的核心。算法思想是根据key的自然顺序(或者比较器顺序)对二叉查找树进行查找,直到找到满足k.compareTo(p.key) == 0entry

TreeMap_getEntry.png

具体代码如下:

//getEntry()方法final Entry
getEntry(Object key) { ...... if (key == null)//不允许key值为null throw new NullPointerException(); Comparable
k = (Comparable
) key;//使用元素的自然顺序 Entry
p = root; while (p != null) { int cmp = k.compareTo(p.key); if (cmp < 0)//向左找 p = p.left; else if (cmp > 0)//向右找 p = p.right; else return p; } return null;}

put()

put(K key, V value)方法是将指定的keyvalue对添加到map里。该方法首先会对map做一次查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似于getEntry()方法;如果没有找到则会在红黑树中插入新的entry,如果插入之后破坏了红黑树的约束,还需要进行调整(旋转,改变某些节点的颜色)。

public V put(K key, V value) {    ......    int cmp;    Entry
parent; if (key == null) throw new NullPointerException(); Comparable
k = (Comparable
) 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); Entry
e = new Entry<>(key, value, parent);//创建并插入新的entry if (cmp < 0) parent.left = e; else parent.right = e; fixAfterInsertion(e);//调整 size++; return null;}

上述代码的插入部分并不难理解:首先在红黑树上找到合适的位置,然后创建新的entry并插入(当然,新插入的节点一定是树的叶子)。难点是调整函数fixAfterInsertion(),前面已经说过,调整往往需要1.改变某些节点的颜色,2.对某些节点进行旋转。

注意:

1、根据红黑树规则4,新插入的节点一定为RED(因为如果是黑色,就不满足每条路径都有相同数量的黑色了);

TreeMap_put.png

调整函数fixAfterInsertion()的具体代码如下,其中用到了上文中提到的rotateLeft()rotateRight()函数。通过代码我们能够看到,情况2其实是落在情况3内的。情况4~情况6跟前三种情况是对称的,因此图解中并没有画出后三种情况,读者可以参考代码自行理解。

//红黑树调整函数fixAfterInsertion()private void fixAfterInsertion(Entry
x) { x.color = RED; while (x != null && x != root && x.parent.color == RED) { if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { Entry
y = rightOf(parentOf(parentOf(x))); if (colorOf(y) == RED) {//如果y为null,则视为BLACK setColor(parentOf(x), BLACK); // 情况1 setColor(y, BLACK); // 情况1 setColor(parentOf(parentOf(x)), RED); // 情况1 x = parentOf(parentOf(x)); // 情况1 } else { if (x == rightOf(parentOf(x))) { x = parentOf(x); // 情况2 rotateLeft(x); // 情况2 } setColor(parentOf(x), BLACK); // 情况3 setColor(parentOf(parentOf(x)), RED); // 情况3 rotateRight(parentOf(parentOf(x))); // 情况3 } } else { Entry
y = leftOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); // 情况4 setColor(y, BLACK); // 情况4 setColor(parentOf(parentOf(x)), RED); // 情况4 x = parentOf(parentOf(x)); // 情况4 } else { if (x == leftOf(parentOf(x))) { x = parentOf(x); // 情况5 rotateRight(x); // 情况5 } setColor(parentOf(x), BLACK); // 情况6 setColor(parentOf(parentOf(x)), RED); // 情况6 rotateLeft(parentOf(parentOf(x))); // 情况6 } } } root.color = BLACK;}

remove()

remove(Object key)的作用是删除key值对应的entry,该方法首先通过上文中提到的getEntry(Object key)方法找到key值对应的entry,然后调用deleteEntry(Entry<K,V> entry)删除对应的entry。由于删除操作会改变红黑树的结构,有可能破坏红黑树的约束,因此有可能要进行调整。

转载于:https://my.oschina.net/weiweiblog/blog/1623509

你可能感兴趣的文章
CentOS6.3 64位安装wine出错,牛人帮帮忙
查看>>
js获取textarea标签中的换行符和空格。
查看>>
国内的Maven服务器
查看>>
C# winform DataGridView 的18种常见属性
查看>>
Cygwin的安装、配置与调整
查看>>
MySQL存储过程
查看>>
做有中国特色的程序员
查看>>
JVM【第九回】:【OutOfMemoryError异常之本机直接内存溢出】
查看>>
Angular
查看>>
ANTLR Reference书摘
查看>>
我的友情链接
查看>>
我的友情链接
查看>>
《Effective C++第三版》读书笔记——构造/析构/赋值运算
查看>>
Redhat 5.3 Linux内核的升级!
查看>>
Ubuntu Mate:扩展存储到整张SD卡
查看>>
学习使用clojure(1)
查看>>
EXCEL 2010规划求解基础篇
查看>>
es学习5-slowlog
查看>>
nf_conntrack: table full, dropping packet
查看>>
Linux的五个查找命令:find,locate,whereis,which,type
查看>>