什么是红黑树?
红黑树的定义
- 每个节点或者是红色的,或者是黑色的。
- 根节点是黑色的。
- 每一个叶子节点(最后的空节点)是黑色的。
- 如果一个节点是红色的,那么他的孩子节点都是黑色的。
- 从任意一个节点到叶子节点,经过的黑色节点是一样的。
直接看到这些定义是非常难以理解的,红黑树为什么这样定义?
在算法4这本书中对于红黑树的介绍直接绕过了红黑树的基本性质,而是首先探索了另外一种平衡树,这种平衡树就是2-3树,事实上红黑树与2-3树是等价的,如果理解的红黑树与2-3树之间的等价关系,其实红黑树并不难!
什么是2-3树?
在讲解红黑树之前,首先学习2-3树,通过2-3树来理解红黑树。
2-3树满足二分搜索树的基本性质,但其节点可以存放一个元素或二个元素,每个节点有两个或者三个孩子,所以称为2-3树。通常存放一个元素有两个孩子的节点称为两节点,一个元素有三个孩子的节点称为三节点。
如图:
==注意:2-3树是一颗绝对平衡的树。==
查找元素
2-3树的查找类似二分搜索树的查找,根据元素的大小来决定查找的方向是左孩子还是右孩子。要判断一个元素是否存在,首先将待查询的元素与根节点进行比较,如果待查找的元素和其中任意一个元素相等,那就查找到了,否则根据比较的结果来选择查找的方向。
如图:
插入元素
==对于2-3树来说,添加节点永远不会添加到空的位置上==。那么添加到哪呢?
如图:
例如添加一个节点37,依然按照二分搜索树的性质来看待,由于37小于42,应该放于42的左子树中,由于42的左子树为空,那么此时新节点将会融合到添加的过程中所找到的最后一个叶子节点。
如图:
由于42本来是个2节点,但是融合后,变成了一个3节点,并且依然是平衡的。
此时再添加一个节点12,由于12小于37,应该放于37的左子树中,由于37的左子树为空,那么此时新节点将会融合到添加过程中所找到的最后一个叶子节点。
如图:
虽然再添加过程中的最后一个叶子节点是一个3节点,但是依然进行融合,暂时形成一个4节点。但是对于2-3树来说,它并没有4节点,只有2节点或3节点。对于4节点可以直接分裂成子树。
如图:
上图中,由4节点分裂成了由三个2节点组成一颗平衡的树。
此时再添加一个节点18,由于18小于37,进入37的左子树中继续比较,此时18大于12,应该放于12的右子树中,由于12的右子树为空,那么此时新节点将会融合到添加过程中所找到的最后一个叶子节点。
如图:
此时再添加一个节点6,由于6小于37,进入37的左子树中继续比较,此时6小于12,应该放于12的左子树当中,但是12的左子树为空,那么此时新节点将会融合到添加过程中所找到的最后一个叶子节点。
如图:
由于2-3中没有4节点,对于节点可以直接分裂成子树。
如图:
注意,上图中由于4节点分裂成3个2节点的子树,造成整个树并不是一颗绝对平衡的树。对于2-3树来说,如果一个叶子节点本来是一个3节点了,添加了新节点变成了4节点的话,对于这个4节点,分裂为三个2节点,并且有一个根节点,对于根节点需要跟父亲节点进行融合,对于根节点和父亲节点融合分为两种情况:
-
父节点是一个2节点
如图:
-
父节点是一个3节点
如图:
红黑树与2-3树的等价性
2-3树中的3节点的元素是并列关系,但两个黑色的节点无法表示出并列的关系。那么如何在红黑树中标识出并列的关系呢?
如图:
如上图中,可以将b节点设置为红色,表示b、c节点在2-3树中是并且的关系。也就是说,一个红色节点和它的父亲节点在2-3树中是一个3节点。
红黑树的实现
注意:在这里实现的红黑树以红色节点向左倾斜为基础的。
插入节点
在2-3树中插入一个节点,始终都是与叶子节点进行融合,那么在红黑树中红色节点代表着与父亲节点
并列的关系,所以在红黑树中添加一个节点始终都是红色的。
上图中,添加第一个节点42,由于添加一个节点始终是红色的,所以要把根节点变为黑色。
此时,再添加一个节点37,根据二分搜索树的性质,由于37<42,所以将节点37添加到根节点的左孩子。
-
情况1
注意,如果新节点大于根节点,就会将新节点插入到根节点的右孩子位置,那么此时就不符合红色节点向左倾斜的定义了,如何解决?对根节点进行左旋转操作,并且根据红色节点向左倾斜的定义,还需把37节点置位红色,42节点置位黑色。注:这里对应在2-3树中相当于把一个新的元素放于2节点中融合为3节点的操作。
-
情况2
此时再插入一个节点66,根据二分搜索树的性质,由于66大于42,放于42的右孩子中。
上图中,在2-3树中对应一个临时的4节点,在2-3树对临时的4节点所处理的方式是什么?
处理的方式是拆分成三个2节点所组成的一棵子树。
那么在红黑树中三个2节点代表着什么意思?其实就是代表这三个节点都是黑色的节点,每一个黑色的节点,如果它的左侧没有红色节点,它本身就代表一个单独的二节点,所以在这种情况下,节点根本不需要选择操作,只需要改变颜色即可,让节点42的左右孩子分别改为黑色。
注:这个样子相当于2-3树中由临时4节点所组成三个2节点的子树。
注意,在2-3树中由临时4节点所组成的三个2节点的子树,有可能会造成整颗树不是绝对平衡的,所以拆分的子树的根节点会继续向父亲节点融合。这意味着,新的根节点在红黑树中变为红色节点。
注:原来的42节点为黑色,37节点和66节点为红色,经过一系列操作之后,其实所有节点的颜色正好相反过来了,这个过程称为颜色翻转。
-
情况3
如上图,添加节点12,根据二分搜索树的性质,添加到节点37的左孩子中。这样的形状也等同于2-3树中的临时4节点。
在2-3树中需要把临时4节点拆分成三个2节点,那么在红黑树中如何解决呢?
对这颗子树的根节点进行右旋转操作,旋转后再对新的根节点变为原来根节点的颜色。
通过这样的处理之后,其实本质上12、37、42这些节点还是应该是一个临时的4节点,所以最后再把原来的根节点变为红色节点,这样就表示节点42与父亲节点37是融合在一起的。
这样就完成了整个右旋转的过程。在这个过程中,根节点变为节点37,并且临时4节点的样子变为根节点的左右孩子都为红色节点,这种情况就是颜色翻转,处理这种情况,把它变成对应2-3树中由三个2节点组成的子树的方法,就是再运行一下颜色翻转的过程。
整体代码
/**
* 描述:红黑树。
*
* Create By ZhangBiao
* 2020/5/18
*/
public class RedBlackTree, V> {
private Node root;
private int size;
private static final boolean RED = true;
private static final boolean BLACK = false;
public RedBlackTree() {
this.root = null;
this.size = 0;
}
/**
* 判断节点node的颜色
*
* @param node
* @return
*/
private boolean isRed(Node node) {
if (node == null) {
return BLACK;
}
return node.color;
}
// node x
// / \ 左旋转 / \
// T1 x ---------> node T3
// / \ / \
// T2 T3 T1 T2
private Node leftRotate(Node node) {
Node x = node.right;
//左旋转的过程
node.right = x.left;
x.left = node;
x.color = node.color;
node.color = RED;
return x;
}
// node x
// / \ 右旋转 / \
// x T2 -------> y node
// / \ / \
// y T1 T1 T2
private Node rightRotate(Node node) {
Node x = node.left;
//右旋转过程
node.left = x.right;
x.right = node;
x.color = node.color;
node.color = RED;
return x;
}
/**
* 颜色翻转
*
* @param node
*/
private void flipColors(Node node) {
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
/**
* 向红黑树中添加新的元素
*
* @param key
* @param value
*/
@Override
public void add(K key, V value) {
root = add(root, key, value);
//最终根节点为黑色节点
root.color = BLACK;
}
/**
* 向以node为根节点的红黑树中插入元素(key, value)(递归算法)并返回插入新节点后红黑树的根节点
*
* @param node
* @param key
* @param value
* @return
*/
private Node add(Node node, K key, V value) {
if (node == null) {
size++;
//默认插入红色节点
return new Node(key, value);
}
if (key.compareTo(node.key) < 0) {
node.left = add(node.left, key, value);
} else if (key.compareTo(node.key) > 0) {
node.right = add(node.right, key, value);
} else {
node.value = value;
}
if (isRed(node.right) && !isRed(node.left)) {
node = leftRotate(node);
}
if (isRed(node.left) && isRed(node.left.left)) {
node = rightRotate(node);
}
if (isRed(node.left) && isRed(node.right)) {
flipColors(node);
}
return node;
}
@Override
public V remove(K key) {
throw new IllegalArgumentException("No remove in RedBlackTree!");
}
@Override
public boolean contains(K key) {
return getNode(root, key) != null;
}
@Override
public V get(K key) {
Node node = getNode(root, key);
return node == null ? null : node.value;
}
@Override
public void set(K key, V newValue) {
Node node = getNode(root, key);
if (node == null) {
throw new IllegalArgumentException(key + " not exist!");
}
node.value = newValue;
}
@Override
public int getSize() {
return this.size;
}
@Override
public boolean isEmpty() {
return this.size == 0;
}
/**
* 返回以node为根节点的二分搜索树中key所在的节点
*
* @param node
* @param key
* @return
*/
private Node getNode(Node node, K key) {
if (node == null) {
return null;
}
if (key.equals(node.key)) {
return node;
} else if (key.compareTo(node.key) < 0) {
return getNode(node.left, key);
} else {
return getNode(node.right, key);
}
}
private class Node {
public K key;
public V value;
public Node left;
public Node right;
public boolean color;
public Node(K key, V value) {
this.key = key;
this.value = value;
this.left = null;
this.right = null;
this.color = RED;
}
}
}