红黑二叉查找树的个人理解以及Java实现
红黑二叉查找树是一种能够表达并实现 2-3 树 的简单数据结构。它的实现基于标准的二叉查找树。其需要扩展的功能(性质)是替换 3- 节点。
替换 3- 节点
将二叉查找树的链接分为两种类型:红链接和黑链接
红链接将两个 2- 节点链接起来构成一个 3- 节点,黑链接则是 2-3 树中的普通链接
更为确切的说,3- 节点 表示为一条左斜的红色链接(两个 2- 节点其中之一是另一个节点的左子节点)相连的两个 2- 节点
如此实现后,假设把红链接拉平,那么它和 2-3 树 其实是等价的
与 2-3 树等价的定义
- 它是一颗二叉查找树
- 它包含红黑两种链接
- 红链接均为左链接
- 没有任何一个节点同时和两条红链接相连
- 它是完美黑色平衡的,即任意空链接到根节点的路径上的黑链接数量相同
其实红链接左右均可,但是统一使用左连接可以更简单的实现
实现
颜色表示
由于每个节点都只会有一条指向自己的链接(从父节点指向它),只需要给节点添加颜色属性即可表示链接的颜色(指向自己的链接的颜色)
private static final boolean RED = true;
private static final boolean BLACK = false;
private class Node {
private Key key;
private Value value;
/**
* 左子树
*/
private Node left;
/**
* 右字数
*/
private Node right;
/**
* 节点颜色,即指向该节点的链接颜色
*/
private boolean color;
/**
* 节点总数
*/
private int size;
public Node(Key key, Value value) {
this(key, value, 1, RED);
}
public Node(Key key, Value value, boolean color) {
this(key, value, 1, color);
}
public Node(Key key, Value value, int size, boolean color) {
this.key = key;
this.value = value;
this.size = size;
this.color = color;
}
}
private boolean isRed(Node node) {
if (node == null) {
return false;
}
return node.color == RED;
}
private boolean isBlack(Node node) {
return !isRed(node);
}
为更简单的实现红黑二叉查找树的定义,有如下约定:
-
根节点永远是黑色
-
新节点为红色
-
空连接为黑色
违背定义的情况
随着节点的插入,会出现如下违背红黑二查找树定义的情况:
- 一个节点的右链接为红链接
- 连续出现两个左红连接
- 节点的左右链接同时为红链接
左旋转
应对情况:需要将一条红色的右链接转换为左连接(左黑右红)
实现原理:将两个键中较小者作为根节点变为较大者作为根节点
private Node rotateLeft(Node h) {
Node x = h.right;
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
x.size = h.size;
h.size = h.left.size + h.right.size + 1;
return x;
}
右旋转
应对情况:连续出现两个左红连接(一个节点的左子节点以及左子节点的左子节点都为红色)
实现原理:将两个键中较大者作为根节点变为较小者作为根节点
private Node rotateRight(Node h) {
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
x.size = h.size;
h.size = h.left.size + h.right.size + 1;
return x;
}
颜色转换
应对情况:两个子节点均为红色
实现原理: 子节点由红转黑,自己由黑转红
private void flipColors(Node h) {
h.color = !h.color;
h.left.color = !h.left.color;
h.right.color = !h.right.color;
}
插入
在标准二叉查找树插入的基础上修正不满足红黑二叉查找树定义的情况即可
public void put(Key key, Value value) {
root = put(root, key, value);
root.color = BLACK;
}
private Node put(Node tree, Key key, Value value) {
if (tree == null) {
return new Node(key, value);
}
int compareTo = key.compareTo(tree.key);
if (compareTo < 0) {
// 如果key小于二叉树的根节点,递归插入左子树
tree.left = put(tree.left, key, value);
} else if (compareTo > 0) {
// 如果key大于于二叉树的根节点,递归插入右子树
tree.right = put(tree.right, key, value);
} else {
tree.value = value;
}
if (isRed(tree.right) && isBlack(tree.left)) {
tree = rotateLeft(tree);
}
if (isRed(tree.left) && isRed(tree.left.left)) {
tree = rotateRight(tree);
}
if (isRed(tree.left) && isRed(tree.right)) {
flipColors(tree);
}
tree.size = size(tree.left) + size(tree.right) + 1;
return tree;
}
删除最小键
同标准的二叉查找树一样,红黑树的删除操作是最复杂的,在实现删除前,先做一个热身:删除最小键
如果最小键是红色(3- 节点),那么直接删除它就可以了;但如果最小键是黑色(2- 节点),那么就直接删除节点就会打破红黑树的平衡。所以我们需要这样做:为了保证我们不会删除一 个 2- 结点,我们沿着左链接向下进行变换,确保当前结点不是 2- 结点 (可能是3-结点,也可能是临时的 4- 结 点 )。
在沿着左链接向下 的过程中,保证以下情况之一成立:
- 如果当前结点的左子结点不是 2- 结点,结束
- 如果当前结点的左子节点是 2- 结点而它的兄弟结点不是 2- 结点,将左子结点的兄弟结点中的一个结点移动到左子结点构成一个 3-结点
- 如果当前结点的左子结点和它的兄弟结点都是 2- 结点,将左子结点、父结点中的最小键和左子结点最近的兄弟结点合并成一个 4- 结点,使父结点由 3- 结点变成一个 2- 结点或者由一个 4- 结点变成一个 3- 结点
在遍历的过程中执行这个过程,最后能够得到一个含有最小键的 3- 结点或者 4- 结点,如此就可以直接删除它。然后再回头分解所有临时的 4- 结点就完成了删除最小键的全过程。
public void deleteMin() {
if (isEmpty()) {
throw new NoSuchElementException("RedBlackBST underflow");
}
if (isBlack(root.left) && isBlack(root.right)) {
root.color = RED;
}
root = deleteMin(root);
if (!isEmpty()) {
root.color = BLACK;
}
}
private Node deleteMin(Node h) {
if (h.left == null) {
return null;
}
if (isBlack(h.left) && isBlack(h.left.left)) {
h = moveRedLeft(h);
}
h.left = deleteMin(h.left);
return balance(h);
}
/**
* 默认h为红色,且 h.left 和 h.left.left 都是黑色时,使 h.left 或者 h.left 的子树变成红色
*
* @param h
* 红黑树
* @return 变换了红黑链接后的树
*/
private Node moveRedLeft(Node h) {
flipColors(h);
if (isRed(h.right.left)) {
h.right = rotateRight(h.right);
h = rotateLeft(h);
flipColors(h);
}
return h;
}
private Node balance(Node h) {
// assert (h != null);
if (isRed(h.right)) {
h = rotateLeft(h);
}
if (isRed(h.left) && isRed(h.left.left)) {
h = rotateRight(h);
}
if (isRed(h.left) && isRed(h.right)) {
flipColors(h);
}
h.size = size(h.left) + size(h.right) + 1;
return h;
}
删除
在查找路径上进行和删除最小键相同的变换同样可以保证在查找过程中任意当前结点均不是2-结点。如果被查找的键在树的底部,我们可以直接删除它。如果不在我们需要将它和它的后继结点交换,就和 二叉查找树一样。因为当前结点必然不是2-结点,问题已经转化为在一棵根结点不是2- 结点的子树中删除最小的键,我们可以在这棵子树中使用前文所述的算 法
public void delete(Key key) {
if (!contains(key)) {
return;
}
if (isBlack(root.left) && isBlack(root.right)) {
root.color = RED;
}
root = delete(root, key);
if (!isEmpty()) {
root.color = BLACK;
}
}
/**
* 从红黑树中删除键为key的节点
*
* @param h
* 红黑树
* @param key
* 键
* @return 删除节点后的红黑树
*/
private Node delete(Node h, Key key) {
if (key.compareTo(h.key) < 0) {
if (isBlack(h.left) && isBlack(h.left.left)) {
h = moveRedLeft(h);
}
h.left = delete(h.left, key);
} else {
if (isRed(h.left)) {
h = rotateRight(h);
}
// 如果命中且右链接为空,直接删除
if (key.compareTo(h.key) == 0 && (h.right == null)) {
return null;
}
if (isBlack(h.right) && isBlack(h.right.left)) {
h = moveRedRight(h);
}
if (key.compareTo(h.key) == 0) {
Node x = min(h.right);
h.key = x.key;
h.value = x.value;
h.right = deleteMin(h.right);
} else {
h.right = delete(h.right, key);
}
}
return h;
}
/**
* 默认h为红色,且 h.right 和 h.right.left 都是黑色时,使 h.right 或者 h.right 的子树变成红色
*
* @param h
* 红黑树
* @return 变换了红黑链接后的树
*/
private Node moveRedRight(Node h) {
flipColors(h);
if (isRed(h.left.left)) {
h = rotateRight(h);
flipColors(h);
}
return h;
}
其他操作
红黑二查找树的其他操作与标准二叉查找树的操作完全一致