走得最慢的人,只要他不丧失目标,也比漫无目的地徘徊的人走得快。
什么是2-3树?
如下图所示:
这就是一个2-3树。一颗2-3树应该是这样的:
- 满足二叉树的基本性质
- 节点可以存放一个或两个元素
从上面图片可以看出,2-3已经不是一颗二叉树了,在树中有两种不同的节点,放一个元素的节点叫二节点,也就是说这个节点有两个孩子,而放两个元素的节点叫做三节点,他有三个孩子。
正是因为每个节点有2个或3个孩子,所以才叫2-3树。
所谓的2-3树满足二叉树的基本性质是指:在2-3树中,2节点的左孩子小于根节点,右孩子大于根节点;3节点的左孩子小于根节点的第一个元素,中间孩子的大小在两个根节点元素之间,右孩子元素大于根节点的第二个元素。
以刚才的图为例:
- 2节点 :左孩子 < a < 右孩子
- 3节点 :左孩子 < b < 中间孩子 < c < 右孩子
2-3树的绝对平衡
2-3树是一颗绝对平衡的树,所谓的绝对平衡是指在2-3树中从根节点到任意叶子结点所经过的节点数是一样的。
为了保持2-3树的绝对平衡,在元素插入时有以下特征:
- 如果插入位置节点的父节点的左右孩子都为空,则就和父节点进行融合
- 如果和一个三节点的节点融合,则可以先融合成四节点,四节点在拆分为以中间大小元素为根,较小元素为中间大小元素的左孩子,较大元素为中间大小元素的右孩子二叉树。若这颗二叉树的根不为2-3树的根,这需要将这个二叉树的根向上融合
这两个特征描述起来确实不好理解。
举个栗子:
下面演示2-3树的建立过程,将包含所有情况:
42 是第一个插入的元素,作为2-3树的根节点。
现在要将37插入到这颗2-3书中,根据第一个特征:如果插入位置节点的父节点的左右孩子都为空,则就和父节点进行融合,根据大小值,37应该插入到42的左孩子位置,但是42的左右孩子此时均为空,所以应该进行融合,此时 37 和 42 组成了一个3节点。如下图:
此时又需要将12 插入2-3树:
同理,12应该插入到37左孩子位置,但37节点左右孩子均为空,因此需要进行融合,此时就是和一个3节点进行融合,可以先融合成一个4节点
由于2-3树中只能是2节点或3节点,所以还需要将4节点拆分出来,拆分成以中间大小元素为根,较小元素为中间大小元素的左孩子,较大元素为中间大小元素的右孩子二叉树。即:
再插入一个18:
同样根据特征一,应该和12融合:
接着插入6:
那么6应该和12 18 所在的三节点融合,构成一个临时的四节点:
此时需要将4节点拆分:
需要注意的是到这一步依然没有结束,还需要将拆分后的二叉树的根节点,也就是12继续向上融合,和37融合:
接着插入11:
插入5:
OK,2-3树正是用这种方式来保证自己的绝对平衡,大家可以检查一下,的确每次插入完毕之后,2-3树依然是绝对平衡的,也就是说:从根节点到任意叶子结点所经过的节点数是一样的。
2-3树和红黑树的等价性
红黑树是每个节点都带有颜色属性的二分搜索树,在满足二分搜索树的基本性质的同时,还具有以下性质:
- 每个节点或者是红色的,或者是黑色的
- 根节点是黑色的
- 每一个叶子节点(最后的空节点)是黑色的
- 如果一个节点是红色的,那么他的孩子节点都是黑色的
- 从任意节点出发,到达叶子结点所经过的黑色节点个数是一样的
需要注意的是,在红黑树中,所谓的叶子结点指的是最后的空节点。
根据上面所讲的2-3树,其实红黑树与2-3树是等价的,他们具有这样的等价关系:
这张图片展示了2-3树中的2节点和3节点在红黑树中所对应的节点,其中:
红黑树中的红色节点表示和双亲结点是融合的关系,在图片中b节点是红色的,就表示和b的双亲结点c相融合。
有了这样的对应关系,我们可以轻易的将一颗2-3树转化为红黑树,或将一颗红黑树转化为2-3树。
举个栗子:
将下面的2-3树转化为红黑树:
转化后的结果应该是这样的:
怎么样?在了解了2-3树以后,其实红黑树也不难!
细心的读者可能已经发现了一个细节,也就是红黑树严格意义上讲,并不是一颗平衡的二叉树!请看以33为根节点的左右子树,其中左子树的高度为3,右子树的高度为1,高度之差为2,已经大于1了。
但红黑树是保持黑平衡的二叉树,这是由红黑树的最后一个性质得来的: 从任意节点出发,到达叶子结点所经过的黑色节点个数是一样的 ,由于2-3树和红黑树的等价关系,其实红黑树的某些性质就是由2-3树的性质得来的,比如刚才这条红黑树的性质正是由2-3树的绝对平衡得来的。
红黑树(左倾红黑树)中插入新元素
在红黑树中插入新元素有下面两种情况:
- 向2节点中插入元素,形成3节点
- 向3节点中插入元素,暂时形成4节点
由于插入一个元素一定是要与2节点或3节点进行融合的,所以在红黑树中插入元素的颜色永远是红色的。
其次,红黑树中的根节点一定是黑色的。
下面就分别对插入的几种情况进行分析:
向2节点中插入元素
- 插入元素小于根节点元素
如向以42为根的红黑树中插入37
这种情况最为简单,之间插入在42的左孩子即可:
2. 插入元素大于根节点元素
如向以37为根的红黑树中插入42
同样我们直接插入到37的右孩子位置:
由于我们实现的是左倾红黑树,因此我们还需将这颗树以37为根进行一次左旋转:
用T1,T2,T3分别表示37的左子树,42的左子树和右子树,左旋转:
左旋转后还应维护结点的颜色,左旋转前,37为根,旋转后42为根,因此旋转后42的颜色应该为旋转前根节点37的颜色(之所以42不直接赋为黑色是应为37不一定是整个红黑树的根,37也可能是红色,表示37与37的双亲是融合的),而左旋后37的元素一定是红色的表示与42融合。
向3节点插入新元素
- 插入的元素大于3结点中的其他元素
如:将66插入到下面的3节点中
显然66应该作为42的右孩子,在2-3树中对应了临时的四节点:
在2-3树中,临时的四节点要被拆分成一个二叉树的形状,并且根节点要向上融合,因此 37 和 66 应该对应为黑色节点,而42为红色节点,向上融合。
此时这个红黑树的颜色发生了翻转,因此这个操作也叫颜色翻转。
- 插入的元素小于3结点中的其他元素
如:将12 插入这个3节点中
显然12应该作为37的左孩子:
此时也对应2-3树中的4节点,同样需要将4节点拆分:
此时4节点的拆分,对应在红黑树中的操作为右旋转,同样我们用T1,T2分别表示x的右子树和node的右子树。
有旋转后是这样的:
和第一种情况一样,x的颜色是原来根节点node的颜色,node的颜色是红色表示和x融合:
最后在进行一次颜色翻转就好了:
3. 插入元素大小位于3节点元素之间
如:将40插入到下面的3节点中
显然40应该插入到37的右孩子上:
然后以 37 为根,做一次左旋转得到:
此时已经转化为第二种情况了。
因此向3节点中插入元素可以总结为以下过程:
Java实现红黑树
package cn.boom.tree;
public class RBTree<T extends Comparable<T>> {
private static final boolean RED = true;
private static final boolean BLACK = false;
private Node root;
private int size;
private class Node {
private T data;
private Node left;
private Node right;
private boolean colour;
public Node() {
this.data = null;
this.left = null;
this.right = null;
this.colour = RED; //默认为红色节点
}
public Node(T data) {
this.data = data;
this.left = null;
this.right = null;
this.colour = RED;
}
}
public RBTree() {
root = null;
size = 0;
}
/**
* 获取树中元素个数
*
* @return
*/
public int getSize() {
return size;
}
/**
* 树是否为空
*
* @return
*/
public boolean isEmpty() {
return size == 0;
}
// 判断节点node的颜色
private boolean getColour(Node node){
if(node == null)//空节点为黑色
return BLACK;
return node.colour;
}
// node x
// / \ 左旋转 / \
// T1 x ---------> node T3
// / \ / \
// T2 T3 T1 T2
private Node leftRotate(Node node) {
Node x = node.right;
Node T2 = x.left;
x.left = node;
node.right = T2;
//维护颜色
x.colour = node.colour;
node.colour = RED;
return x;
}
// node x
// / \ 右旋转 / \
// x T2 -------> y node
// / \ / \
// y T1 T1 T2
private Node rightRotate(Node node) {
Node x = node.left;
Node T1 = x.right;
x.right = node;
node.left = T1;
//维护颜色
x.colour = node.colour;
node.colour = RED;
return x;
}
//颜色翻转
private void flipColors(Node node) {
node.colour = RED;
node.left.colour = node.right.colour = BLACK;
}
/**
* 向红黑树中插入元素
*
* @param elem
*/
public void add(T elem) {
root = add(root, elem);
size++;
//维护根节点的颜色
root.colour = BLACK;
}
private Node add(Node parent, T e) {
if (parent == null) {
return new Node(e);
}
if (e.compareTo(parent.data) > 0) { //忽略重复元素
parent.right = add(parent.right, e);
} else if (e.compareTo(parent.data) < 0) {
parent.left = add(parent.left, e);
}
if (getColour(parent.right) == RED && getColour(parent.left) != RED) {
parent = leftRotate(parent);
}
if (getColour(parent.left) == RED && getColour(parent.left.left) == RED) {
parent = rightRotate(parent);
}
if (getColour(parent.left) == RED && getColour(parent.right) == RED) {
flipColors(parent);
}
return parent;
}
/**
* 元素e是否存在
*
* @param e
* @return
*/
public boolean contains(T e) {
return contains(root, e);
}
private boolean contains(Node parent, T e) {
if (parent != null) {
if (e.compareTo(parent.data) == 0) {
return true;
} else if (e.compareTo(parent.data) > 0) {
return contains(parent.right, e);
} else if (e.compareTo(parent.data) < 0) {
return contains(parent.left, e);
}
}
return false;
}