本文目录
- 一、基本概念
- 红黑树的定义
- NIL叶节点的讨论
- 引用值为null
- 引用值为特殊节点NIL
- 红黑树和AVL树
- 二、查询节点
- 三、插入节点
- 插入节点的颜色
- 插入逻辑
- fixup逻辑
- fixup原则
- fixup原理
- fixupAfterInsert源码
- 四、红黑树源码
- 源码
- 测试
- 分析
系列目录
一、基本概念
1、红黑树的定义
红黑树:首先是一颗二叉查找树,其次对于树中的任意一个节点,都满足以下5个性质
- 节点颜色不是红色就是黑色;
- 根节点黑色;
- 叶节点黑色(NIL节点);(这条性质需要讨论一下)
- 红节点只能有黑孩子;
- 对于所有的叶节点,其高度路径上都拥有相同数量的黑色节点;
二叉查找树的定义,见第三篇博客《二叉查找树》。
2.NIL节点
红黑树的五个性质中,NIL节点的规定一直不好理解。
NIL节点:如果一个节点没有子节点或父节点,则该节点相应的指针属性的值为NIL,我们把这些NIL视为指向红黑树叶节点的指针(外部节点),而把带关键字的节点视为树的内部节点。
在树的一般定义中,如果一个节点没有孩子,那么这个节点就被视为叶节点。
但是在红黑树中不一样:
- A.引用值为null:如果一个节点的孩子或父亲值为null,那么这个值为null的孩子或父亲就被视为NIL叶节点;
- B.引用值为特殊节点NIL:或者你也可以理解成有一个特殊的节点NIL,如果一个节点没有孩子或父亲(值为null),那么就把孩子节点或父节点的引用指向一个特殊的节点,这个特殊的节点就叫NIL。
以上两种理解都可以实现红黑树,其中引用值为null是JDK中TreeMap的实现采用的方式,而引用值为特殊节点NIL是《算法导论》中讲解红黑树采用的方式。
A.引用值为null
这也是本文借鉴JDK中TreeMap的源码而采用的实现方式。
在具体的代码实现中,逻辑是这样的:如果节点为空,则返回黑色,否则返回节点的颜色。
实现代码如下:
private static final boolean RED = true;
private static final boolean BLACK = false;
/**
* 返回节点颜色:如果是节点为null(NIL节点),则返回黑色,否则返回节点颜色
* @param node
* @return RED=true,BLACK=false,节点默认为红色
*/
private boolean color(Node<K, V> node) {
return Objects.isNull(node) ? BLACK : node.color;
}
B.引用值为特殊节点NIL
当然你也可以定义一个黑色的静态节点NIL,默认赋值给class Node的parent、left、right属性,或者在构造函数中赋值给parent、left、right。
《算法导论》中图示如下:
3.红黑树和AVL树
红黑树和AVL树一样,都属于自平衡二叉树。
AVL树通过左右孩子的高度差不大于1来保证该节点的平衡,进而保证整棵树的平衡;红黑树则通过要求所有叶节点的高度路径上拥有相同数量的黑色节点来保证树的平衡。
这也意味着,AVL树追求更严格的平衡,左右子树的高度差不大于1;红黑树追求近似平衡,确保没有一条路径会比其他路径长出2倍。
所以在拥有相同数量的节点的情况下,height(AVL tree) <= height(red-black tree),由此可知:
- 相同节点数量下,AVL树的高度通常更小,拥有更好的查询性能,但是写操作会导致更多的balance操作,因此AVL树更适合查多写少的场景;
- 相同节点数量下,red-black tree的高度会更大,更适合写多查少的场景;
二、查询节点
根据红黑树的定义,红黑树首先是一颗二叉查找树,查询操作不会破坏红黑树已经存在的组织结构,所以红黑树的查询操作和二叉查找树的查询操作是一致的。
查询逻辑:从根结点出发
- 如果查找key小于节点key,则向左走;
- 如果查找key大于节点key,则向右走;
- 如果查找key等于节点key,则返回该节点;
- 如果查找到最后一个不为null的节点还没有找到相等key,那么返回null;
实现代码如下,关注以下selectNode方法的实现,因为select、insert、remove的实现也依赖该方法:
/**
* select/insert/remove都需要用到这个方法
* 1.如果是空树,则返回null
* 2.如果树中包含key节点,则返回该节点
* 3.如果树中没有key节点,则返回最后一次查找的节点
* 这里不需要判断key是否为null,这个判断放在select/insert/remove调用selectNode之前,保证调用selectNode不会传入null
* @param key
* @return
*/
private Node<K, V> selectNode(K key) {
Node<K, V> current = null;
Node<K, V> find = this.root;
while (Objects.nonNull(find)) {
current = find;
int compare = key.compareTo(find.key);
if (compare < 0)
find = find.leftChild;
else if (compare > 0)
find = find.rightChild;
else
break;
}
return current;
}
public V select(K key) {
if (Objects.isNull(key))
throw new NullPointerException();
Node<K, V> node = selectNode(key);
if (Objects.nonNull(node) && key.equals(node.key))
return node.value;
return null;
}
三、插入节点
1.插入节点的颜色
红黑树的性质1:每个节点不是红色就是黑色。
那新插入的节点应该是什么颜色?
- 如果是黑色:那只要发生插入操作,就一定会违反红黑树的性质5(每个叶子节点的高度路径上拥有相同数量的黑色节点);
- 如果是红色:
- 第一次插入会违反红黑树的性质2(根节点黑色),但只需要修改this.root.color = BLACK即可修复;
- 有可能违反红黑树性质4(红节点只能有黑孩子)出现相邻的双红节点;
从插入操作一定违反性质5和可能违反性质4的代价来做取舍,显然新插入的节点是红色,这样只有部分场景需要fixup,而不是每次插入都要fixup。