2-3树
1. 2-3树的定义
一颗2-3查找树或为一颗空树,或由以下节点组成:
- 2-节点,含有一个键(及其对应的值)和两条链接,左链接指向的2-3树中的键都小于该节点,右链接指向的2-3树中的键大于该节点。
- 3-节点,含有两个键(及其对应的值)和三条链接,左连接指向的2-3树中的键都小于该节点,中链接指向的2-3树中的键都位于该节点的两个键之间,右链接指向的2-3树中的键大于该节点。
一颗完美平衡的2-3查找树中的所有空连接到根节点的距离都是相同的。
2. 2-3树的查找
将二叉查找树的查找算法一般化就能够直接得到2-3树的查找算法。要判断一个键是否存在树中,我们先将它和根节点中的键进行比较。如果它和其中任意一个相等,查找命中;否则我们就根据比较的结果找到指向相应区间的链接,并在其指向的子树中递归地继续查找。如果这是个空链接,查找未命中。
3. 向2-3树中插入新键
要在2-3树中插入一个新节点,可以和二叉树一样先进行一次未命中的查找,然后把新节点挂在树的底部。但是这样的话无法保持完美平衡性。我们使用2-3树的主要原因就在于它能够在插入后继续保持平衡。
3.1 向2-节点中插入新键
只要把这个2-节点替换成一个3-节点,将要插入的键保存在其中即可。
3.2 向一颗只含有一个3-节点的树中插入新键
3.3 向一个父节点为2-节点的3-节点中插入新键
先构建一个临时的4-节点并将其分解,将中键移动至原来的父节点中,形成一个新的3-节点。
3.4 向一个父节点为3-节点的3-节点中插入新键
推广到一般情况,一直向上不断分解临时的4-节点并将中键插入到更高层的父节点,直到遇到一个2-节点并将它替换为一个不需要继续分解的3-节点,或者是到达3-节点的根。
如果从插入节点到根节点的路径上全都是3-节点,我们的根节点最终变成一个临时的4-节点。此时可以按照向一颗只有一个3-节点的树中插入新键的方法处理这个问题。将临时4-节点分解为3个2-节点,使得树高加1。请注意,这次最后的变换仍然保持了树的完美平衡性,因为它变换的是根节点。
插入新键总结
- 总是先将新键融合进查找结束的节点,如果是2-节点,直接保存到其中;
- 如果是3-节点,先构建成一个临时的4-节点,然后对父节点分解为3个2节点,将中键与原父节点融合;
- 若原父节点是2-节点,则融合为一个新的3-节点,3-节点符合2-3树定义;
- 若源父节点是3-节点,融合成的是一个4-节点,4-节点需要分解,回到步骤2,以此类推,不断向上分解,直到遇到一个2-节点,将它融合后一个新的3-节点;
- 若一直向上分解到根节点,根节点是一个3-节点(根节点融合成临时的4-节点),则需要将根节点分解成3个2-节点,树高加1。
红黑树
红黑二叉查找树背后的思想是用标准的二叉查找树(完全由2-节点构成)和一些额外的信息(替换3-节点)来表示2-3树。将树中的链接分为两种类型:红链接将两个2-节点连接起来构成一个3-节点,黑链接则是2-3树中的普通链接。确切地说,我们将3-节点表示为由一条左斜的红色链接相连的两个2-节点。这种表示法的一个优点是,无需修改就可以直接使用标准二叉树的get()方法。对于2-3树,只需要对节点进行转换,就可以立即派生出一颗对应的二叉查找树。我们将这种表示2-3树的二叉查找树称为红黑二叉查找树。所有基于红黑树的符号表实现都能保证操作的运行时间未对数级别。
1.红黑树的性质
- 每个节点或者是红色的,或者是黑色的;
- 根节点是黑色的;
- 每个叶子节点(最后的空节点)是黑色的;
- 如果 一个节点是红色的,那么它的孩子节点都是黑色的;
- 从任意一个节点到叶子节点,经过的黑色节点是一样的(红黑树以保持黑平衡的二叉树)。
红黑树既是二叉查找树,也是2-3树,它综合了二叉查找树中简洁高效的查找方法和2-3树中高效的平生插入算法。
2.红黑树与2-3树的一一对应关系
3.红黑树的节点表示
代码表示
// 定义颜色的两个常量
private static final boolean RED = true;
private static final boolean BLACK = false;
private class Node {
private K key;
private V value;
private Node left;
private Node right;
private boolean color; // 节点颜色
public Node(K key, V value) {
this.key = key;
this.value = value;
this.left = null;
this.right = null;
this.color = RED; // 添加节点时,默认是红节点,相当于2-3树的添加时的融合操作
}
}
/**
* 定义一个辅助方法,判断节点的颜色
*/
private boolean isRed(Node node) {
if (node == null)
return BLACK;
return node.color;
}
4.节点的旋转
4.1 左旋转
红黑树插入一个新键,必定是红色的节点,因为在2-3树中插入一个新键总是先融合到已有的节点,这里红节点即代表节点要进行融合操作。
若新键插入到已有节点(假设是黑色的)的右侧,形成一个右红链接,则需要进行性左旋转没确保红链接保持是左连接。
/**
* h x
* / \ 左旋转 / \
* T1 X -----------> h T3
* / \ / \
* T2 T3 T1 T2
*
*/
private Node leftRotate(Node h) {
Node x = h.right;
// 左旋转
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
return x;
}
4.2 右旋转
若需要插入节点A,经查找要插入到节点E的左侧,假设E节点是红色的,E节点的父节点S节点是黑色的,即E、S节点在2-3树中是一个3节点,此时A、E、S形成一个临时的4-节点,需要拆分成3个2节点。故需要对S节点的左链接进行右旋转,E经过右旋转后变成根节点,颜色要与原根节点保持一致(S节点颜色是黑色的),S节点变为E节点的右孩子,在E节点还未向上融合之前,A、E、S还是一个临时4-节点,所以S节点需要置为红色,代表与E还是融合在一起的。如图所示。
代码实现
/**
* h x
* / \ 右旋转 / \
* x T2 -----------> y h
* / \ / \
* y T1 T1 T2
*
*/
private Node rightRotate(Node h) {
Node x = h.left;
// 右旋转
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
return x;
}
5.颜色的翻转
接4.2,A、E、S拆分成3个2-节点之后,E这个2-节点需要向上与其父节点进行融合(2-3的性质),融合的2-节点必须是红色的,但是此时E节点是黑色的,与此同时A、S这两个节点是红色的,故可以对A、E、S这三个节点进行颜色的翻转,即E(黑->红),A、S(红->黑)。
代码实现
/**
* 颜色翻转
*/
private void filpColor(Node node) {
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
6.向红黑树中插入新键
红黑树插入新键可以等价于向2-3树的2-节点或者3-节点插入新键。
6.1 向2-节点插入新键
- 若插在2-节点的左侧,则直接插入;
- 若插在2-节点的右侧,则需要进行一次左旋转。
6.2 向3-节点插入新键
向一个3-节点插入新键有三种情况,如上图所示:
- 新键所在位置为根节点的左节点的右节点,需要依次进行左旋转->右旋转->颜色翻转;
- 新键所在位置为根节点的左节点的左节点,需要依次进行右旋转->颜色翻转;
- 新键所在位置为根节点的右节点,只需要进行颜色翻转。
可以看出三种情况可以公用一个流程链,最坏的情况①需要进行三个步骤,才能正确的添加新键。
代码实现
/**
* 向以node为根节点的红黑树中插入元素,递归算法
* 返回插入新节点后红黑树的根
*/
private Node addNode(Node node, K key, V value) {
if (node == null) {
size++;
return new Node(key, value);
}
if (key.compareTo(node.key) < 0)
node.left = addNode(node.left, key, value);
else if (key.compareTo(node.key) > 0)
node.right = addNode(node.right, key, value);
else
node.value = value;
// 如果h的右节点是红色的,左节点不是红色的,需要进行性左旋转
if (isRed(node.right) && !isRed(node.left))
node = leftRotate(node);
// 如果h的左节点是红色的,h的左节点的左节点也是红色的,需要进行性右旋转
if (isRed(node.left) && isRed(node.left.left))
node = rightRotate(node);
// 如果h的左右节点都是红色的,进行颜色翻转
if (isRed(node.left) && isRed(node.right))
filpColor(node);
return node;
}
7.红黑树的完整实现
public class RBT, V> {
private static final boolean RED = true;
private static final boolean BLACK = false;
private class Node {
private K key;
private V value;
private Node left;
private Node right;
private boolean color; // 节点颜色
public Node(K key, V value) {
this.key = key;
this.value = value;
this.left = null;
this.right = null;
this.color = RED; // 添加节点时,默认是红节点,相当于2-3树的添加时的融合操作
}
}
private Node root;
private int size;
public RBT() {
root = null;
size = 0;
}
public int getSize() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
/**
* 判断节点的颜色
*/
private boolean isRed(Node node) {
if (node == null)
return BLACK;
return node.color;
}
/**
* h x
* / \ 左旋转 / \
* T1 X -----------> h T3
* / \ / \
* T2 T3 T1 T2
*
*/
private Node leftRotate(Node h) {
Node x = h.right;
// 左旋转
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
return x;
}
/**
* h x
* / \ 右旋转 / \
* x T2 -----------> y h
* / \ / \
* y T1 T1 T2
*
*/
private Node rightRotate(Node h) {
Node x = h.left;
// 右旋转
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
return x;
}
/**
* 颜色翻转
*/
private void filpColor(Node node) {
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
/**
* 向红黑树中添加新的元素
*/
public void addNode(K key, V value) {
root = addNode(root, key, value);
root.color = BLACK; // 最终根节点为黑色节点
}
/**
* 向以node为根节点的红黑树中插入元素,递归算法
* 返回插入新节点后红黑树的根
*/
private Node addNode(Node node, K key, V value) {
if (node == null) {
size++;
return new Node(key, value);
}
if (key.compareTo(node.key) < 0)
node.left = addNode(node.left, key, value);
else if (key.compareTo(node.key) > 0)
node.right = addNode(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))
filpColor(node);
return node;
}
/**
* 返回以node为根节点的红黑树中,key所在的节点
* 与二分搜索树一致
*/
public Node getNode(Node node, K key) {
if (node == null)
return null;
if (key.equals(node.key))
return node;
if (key.compareTo(node.key) < 0)
return getNode(node.left, key);
else
return getNode(node.right, key);
}
public boolean contains(K key) {
return getNode(root, key) != null;
}
public void set(K key, V newValue) {
Node node = getNode(root, key);
if (node == null)
throw new IllegalArgumentException(key + "doesn't exist");
node.value = newValue;
}
// 返回以node为根的二分搜索树的最小值所在的节点
private Node minimum(Node node){
if(node.left == null)
return node;
return minimum(node.left);
}
// 删除掉以node为根的二分搜索树中的最小节点
// 返回删除节点后新的二分搜索树的根
private Node removeMin(Node node){
if (node.left == null) {
Node rightNode = node.right;
node.right = null; // 删除根节点
size--;
return rightNode;
}
removeMin(node.left);
return node;
}
// 从二分搜索树中删除键为key的节点
public V remove(K key) {
Node node = getNode(root, key);
if (node != null) {
root = remove(root, key);
return node.value;
}
return null;
}
private Node remove(Node node, K key){
if (node == null)
return null;
if (key.compareTo(node.key) < 0) {
node.left = remove(node.left, key);
return node;
} else if (key.compareTo(node.key) > 0) {
node.right = remove(node.right, key);
return node;
} else {
// 左节点为空
if (node.left == null) {
Node rightNode = node.right;
node.right = null; // 删除根节点
size--;
return rightNode;
}
// 右节点为空
if (node.right == null) {
Node leftNode = node.left;
node.left = null;
size--;
return leftNode;
}
// 待删除节点左右子树均不为空的情况
// 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
// 用这个节点顶替待删除节点的位置
Node successor = minimum(node.right);
successor.right = removeMin(node.right);
successor.left = node.left;
node.left = node.right = null;
return successor;
}
}
}