15-平衡树(查找树、红黑树)
一、2-3查找树
为了保证查找树的平衡性,我们需要一些灵活性,因此在这里我们允许树中的一个结点保存多个键。确切的说,我们将一棵标准的二叉查找树中的结点称为2-结点(含有一个键和两条链),而现在我们引入3-结点,它含有两个键和三条链。2-结点和3-结点中的每条链都对应着其中保存的键所分割产生的一个区间。
1.1 2-3查找树的定义
一棵2-3查找树要么为空,要么满足满足下面两个要求:
-
2-结点:
含有一个键(及其对应的值)和两条链,左链指向的2-3树中的键,都小于该结点的键;右链指向的2-3树中的键,都大于该结点的键。
-
3-结点:
含有两个键(及其对应的值)和三条链,左链指向的2-3树中的键,都小于该结点的键;中链指向的2-3树中的键都位于该结点的两个键之间,右链指向的2-3树中的键都大于该结点的键。
1.2 查找
将二叉查找树的查找算法一般化我们就能够直接得到2-3树的查找算法。要判断一个键是否在树中,我们先将它和根结点中的键比较。如果它和其中任意一个相等,查找命中;否则我们就根据比较的结果找到指向相应区间的连接,并在其指向的子树中递归地继续查找。如果这个是空链接,查找未命中。
1.3 插入
1.3.1 向2-结点插入新键
往2-3树中插入元素和往二叉查找树中插入元素一样,首先要进行查找,然后将节点挂到未找到的节点上。2-3树之所以能够保证在最差的情况下的效率的原因在于其插入之后仍然能够保持平衡状态。如果查找后未找到的节点是一个2-结点,那么很容易,我们只需要将新的元素放到这个2-结点里面使其变成一个3-结点即可。但是如果查找的节点结束于一个3-结点,那么可能有点麻烦。
1.3.2 向一棵只含有3-结点的树中插入新键
假设2-3树只包含一个3-结点,这个结点有两个键,没有空间来插入第三个键了,最自然的方式是我们假设这个结点能存放三个元素,暂时使其变成一个4-结点,同时他包含四条链接。然后,我们将这个4-结点的中间元素提升,左边的键作为其左子结点,右边的键作为其右子结点。插入完成,变为平衡2-3查找树,树的高度从0变为1。
1.3.3 向一个父结点为2-结点的3-结点中插入新键
和上面的情况一样一样,我们也可以将新的元素插入到3-结点中,使其成为一个临时的4-结点,然后,将该结点中的中间元素提升到父结点即2-结点中,使其父结点成为一个3-结点,然后将左右结点分别挂在这个3-结点的恰当位置。
1.3.4 向一个父结点为3-结点的3-结点插入新键
当我们插入的结点是3-结点的时候,我们将该结点拆分,中间元素提升至父结点,但是此时父结点是一个3-结点,插入之后,父结点变成了4-结点,然后继续将中间元素提升至其父结点,直至遇到一个父结点是2-结点,然后将其变为3-结点,不需要继续进行拆分。
1.3.5 分解根结点
当插入结点到根结点的路径上全部是3-结点的时候,最终我们的根结点会编程一个临时的4-结点,此时,就需要将根结点拆分为两个2-结点,树的高度加1。
1.4 2-3树的性质
一棵完全平衡的2-3树具有以下性质:
- 任意空链接到根结点的路径长度都是相等的。
- 4-结点变换为3-结点时,树的高度不会发生变化,只有当根结点是临时的4-结点,分解根结点时,树高+1。
- 2-3树与普通二叉查找树最大的区别在于,普通的二叉查找树是自顶向下生长,而2-3树是自底向上生长。
二、红黑树
红黑树主要是对2-3树进行编码,红黑树背后的基本思想是用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。我们将树中的链接分为两种类型:
**红链接:**将两个2-结点连接起来构成一个3-结点; **黑链接:**则是2-3树中的普通链接。
确切的说,我们将3-结点表示为由一条左斜的红色链接(1两个2-结点其中之一是另一个的左子结点)相连的两个2-结点。这种表示法的一个优点是,我们无需修改就可以直接使用标准的二叉查找树的get方法。
2.1 红黑树的定义
红黑树是含有红黑链接并满足下列条件的二叉查找树:
- 红链接均为左链接;
- 没有任何一个结点同时和两条红链接相连;
- 该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同;
2.2 红黑树结点API
因为每个结点都只会有一条指向自己的链接(从它的父结点指向它),我们可以在之前的Node结点中添加一个布尔类型的变量color来表示链接的颜色。如果指向它的链接是红色的,那么该变量的值为true,如果链接是黑色的,那么该变量的值为false。
类名 | Node<Key,Value> |
---|---|
构造方法 | Node(Key key, Value value, Node left, Node right,boolean color):创建Node对象 |
成员变量 | 1.public Node left:记录左子结点 2.public Node right:记录右子结点 3.public Key key:存储键 4.public Value value:存储值 5.public boolean color:由其父结点指向它的链接的颜色 |
代码:
private class Node<key, Value> {
// 存储键
public Key key;
// 存储值
public Value value;
// 记录卡左子结点
public Node left;
// 记录右子结点
public Node right;
// 由其父结点指向它的链接的颜色
public boolean color;
public Node(Key key, Value value, Node left, Node right, boolean color) {
this.key = key;
this.value = value;
this.left = left;
this.right = right;
this.color = color;
}
}
2.3 平衡化
在对红黑树进行一些增删改查的操作后,很有可能会出现红色的右链接或者两条连续红色的链接,而这些都不满足红黑树的定义,所以我们需要对这些情况通过旋转进行修复,让红黑树保持平衡。
2.3.1 左旋
当某个结点的左子结点为黑色,右子结点为红色,此时需要左旋。
**前提:**当前结点为E,它的右子结点为S;
左旋过程:
- 让S的左子结点变为E的右子结点:E.right = S.left;
- 让E成为S的左子结点:S.left = E;
- 让S的color属性变为E的color属性值:S.color = E.color;
- 让E的color属性变为RED:E.color = true;
2.3.2 右旋
当某个结点的左子结点是红色,且左子结点的左子结点也是红色,需要右旋
**前提:**当前结点为S,它的左子结点为E;
右旋过程:
- 让E的右子结点成为S的左子结点:S.left = E.right;
- 让S成为E的右子结点:E.right = S;
- 让E的color变为S的color属性值:E.color = S.color;
- 让S的color为RED:S.color = true;
2.4 向单个2-结点中插入新键
一棵只含有一个键的红黑树只含有一个2-结点。插入另一个键后,我们马上就需要将他们旋转。
-
如果新键小于当前结点的键,我们只需要新增一个红色结点即可,新的红黑树和单个3-结点完全等价。
-
如果新键大于当前结点的键,那么新增的红色结点将会产生一条红色的右链接,此时我们需要通过左旋,把红色右链接变成左链接,插入操作才算完成。形成的新的红黑树依然和3-结点等价,其中含有两个键,一条红色链接。
2.5 向底部的2-结点插入新键
用和二叉查找树相同的方式向一棵红黑树中插入一个新键,会在树的底部新增一个结点(可以保证有序性),唯一区别的地方是我们会用红链接将新结点和它的父结点相连。如果它的父结点是一个2-结点,那么刚才讨论的两种方式仍然适用。
2.6 颜色反转
当一个结点的左子结点和右子结点的color都为RED时,也就是出现了临时的4-结点,此时只需要把左子结点和右子结点的颜色变为BLACK,同时让当前结点的颜色变为RED即可。
2.7 向一棵双键树(即一个3-结点)中插入新键
这种情况有可以分为三种子情况:
-
新键大于原树中的两个键
-
新键小于原树中的两个键
-
新键介于原数中两个键之间
2.8 根结点的颜色总是黑色
在结点Node对象中color属性表示的是父结点指向当前结点的连接的颜色,由于根结点不存在父结点,所以每次插入操作后,我们都需要把根结点的颜色设置为黑色。
2.9 向树底部的3-结点插入新键
假设在树的底部的一个3-结点下加入一个新的结点。前面我们所讲的3种情况都会出现。指向新结点的链接可能是3-结点的右链接(此时我们只需要转换颜色即可),或是左链接(此时我们需要进行右旋转然后再转换),或是中链接(此时需要先左旋转然后再右旋转,最后转换颜色)。颜色转换会使中间结点的颜色变红,相当于将它送入了父结点。这意味着父结点中继续插入一个新键,我们只需要使用相同的方法解决即可,直到遇到一个2-结点或者根结点为止
2.10 红黑树的API设计
类名 | RedBlack<Key,Value> |
---|---|
构造方法 | RedBlackTree():创建RedBlackTree对象 |
成员方法 | 1.private boolean isRed(Node x):判断当前结点的父指向链接是否为红色 2.private Node rotateLeft(Node h):左旋调整 3.private Node rotateRight(Node h):右旋调整 4.private void flflipColors(Node h):颜色反转,相当于完成拆分4-结点 5.public void put(Key key, Value val):在整个树上完成插入操作 6.private Node put(Node h, Key key, Value val):在指定树中,完成插入操作,并返回添加元素后新的树 7.public Value get(Key key):根据key,从树中找出对应的值 8.private Value get(Node x, Key key):从指定的树x中,找出key对应的值 9.public int size():获取树中元素的个数 |
成员变量 | 1.private Node root : 记录根结点 2.private int N:记录树中元素的个数 3.private static fifinal boolean RED:红色链接标识 4.private static fifinal boolean BLACK:黑色链接标识 |
2.11 红黑树的实现
public class RedBlackTree<Key extends Comparable<Key>, Value> {
// 记录根结点
private Node root;
// 记录树中元素的个数
private int N;
// 红色链接标识
private static final boolean RED = true;
// 黑色链接标识
private static final boolean BLACK = false;
// 判断当前结点的父指向链接是否为红色
private boolean isRed(Node x) {
//空结点默认是黑色链接
if (x == null) {
return false;
}
return x.color == RED;
}
// 左旋(条件:左黑右红)
private Node rotateLeft(Node h) {
// 1.让当前结点h的右子结点的左子节点,成为当前结点h的右子结点
// 1.1找出当前结点的右子结点
Node hRight = h.right;
// 1.2变换值
h.right = hRight.left;
// 2.让当前结点h,成为其右子结点的左子结点
hRight.left = h;
// 3.让当前结点的颜色,成为其右子结点的颜色
hRight.color = h.color;
// 4.让当前结点h的颜色,变为RED
h.color = RED;
// 5.返回变换后的结点(也就是h的右子结点)
return hRight;
}
// 右旋(条件:左红左红)
private Node rotateRight(Node h) {
// 1.让当前结点h的左子结点的右子结点,成为当前结点h的左子结点
// 1.1寻找当前结点h的左子结点
Node hLeft = h.left;
// 1.2变换值
h.left = hLeft.right;
// 2.让当前结点h,成为当前结点的左子结点的右子结点
hLeft.right = h;
// 3.让当前结点的颜色,成为其左子结点的颜色
hLeft.color = h.color;
// 4.让当前结点的颜色变为RED
h.color = RED;
// 5.返回变换后的结点(也就是h的左子结点)
return hLeft;
}
// 颜色反转,相当于完成拆分4-结点(条件:左红右红)
private void flipColors(Node h) {
// 让当前节点的颜色变为RED
h.color = RED;
// 让当前结点的左右子结点颜色变为BLACK
h.left.color = BLACK;
h.right.color = BLACK;
}
// 在整个树上完成插入操作
public void put(Key key, Value value) {
root = put(root, key, value);
root.color = BLACK;// 根结点的颜色永远为黑色的
}
// 在指定树中,完成插入操作,并返回添加元素后的新树(向树底部的3-结点插入新键)
private Node put(Node h, Key key, Value value) {
// 判断h是否为空,为空则直接返回一个红色的结点就可以
if (h == null) {
N++; // 数量+1
return new Node(key, value, null, null, RED);
}
// 不为空,比较h结点的key与传入的key的大小
int cmp = key.compareTo((Key) h.key);
if (cmp < 0) { //传入的key<当前结点的key
// 继续向左查找
h.left = put(h.left, key, value);
} else if (cmp > 0) { //传入的key>当前结点的key
// 继续向右查找
h.right = put(h.right, key, value);
} else { //传入的key=当前结点的key
// 进行值的替换
h.value = value;
}
// 进行左旋:当前结点的左子结点为黑色,右子结点为红色(左黑右红)
if (!isRed(h.left) && 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);
}
return h;
}
// 根据key,从树中找出对应的值
public Value get(Key key) {
return get(root, key);
}
// 从指定树x中,查找key对应的值
private Value get(Node x, Key key) {
// 如果x为空,则返回空
if (x == null) {
return null;
}
// 不为空,令传入的key与当结点的key进行比较
int cmp = key.compareTo(x.key);
// 传入的key>当前结点的key
if (cmp > 0) {
return get(x.right, key);
} else if (cmp < 0) { // 传入的key<当前结点的key
return get(x.left, key);
} else { // 传入的key=当前结点的key
return x.value;
}
}
// 获取树中元素个数
public int size() {
return N;
}
// 红黑树的结点
private class Node {
// 存储键
public Key key;
// 存储值
public Value value;
// 记录左子结点
public Node left;
// 记录右子结点
public Node right;
// 由其父结点指向它的链接的颜色
public boolean color;
public Node(Key key, Value value, Node left, Node right, boolean color) {
this.key = key;
this.value = value;
this.left = left;
this.right = right;
this.color = color;
}
}
}