一. 平衡树
- 前面我们学过二叉查找树,如果像之前那样依次按照9,8,7,6,5,4,3,2,1的顺序插入数据的话,可能会得到下图的树,这样我们如果要查找1这个元素效率非常低,因为需要查找所有元素最后才能找到1.效率低的原因在于这个是并不平衡,而是全部都是右分支,如果我们有一种方法,能够不受插入数据的影响,让生成的树都像完全二叉树那样,那么即使在糟糕的情况下效果依旧很好。

1.1 2-3查找树
- 为了解决上述问题,我允许树种的结点可以保存多个键,确切的说我们将一棵标准的二叉树中的结点称为2-结点(含有一个键和两条链),现在我们引入3-结点,它含有两个键和三条链。2-结点和3-结点中的每条链都对应这着其中分割所产生的区间。
1.1.1 2-3查找树的定义
- 一棵2-3查找树要么为空,要么满足下面两个要求:
- 2-结点:含有一个键(及其对应的值)和两条链,左链接指向2-3树中的键都小于该结点,右连接指向的2-3树中的键都大于该结点。
- 3-结点:含有两个键(及其对应的值)和三条链,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。

1.1.2 2-3树的查找
- 将二叉查找树的查找算法一般化我们就能够直接得到2-3树的查找算法。要判断一个键是否在树中,我们先将它和根结点中的键比较。如果它和其中任意一个相等,查找命中;否则我们就根据比较结果找到指向的相应区间的链接,并在其指向的子树中递归地继续查找。如果这个是空链接,查找未命中。

1.1.3 插入
- 向2-结点中插入新键:如果查找后未找到的结点是个2-结点,那么很容易,我们只需要将这个2-结点变为3-结点就行。

- 向一棵只含有一个3-结点的树中插入新键:假如2-3树只包含一个3-结点,这个结点有两个键,没有空间来插入第三个键了,最自然的方式是我们假设这个结点能存放三个元素,暂时使其变成一个4-结点,同时它包含四条链接。然后,将这个4-结点的中间元素提升,左边的键作为其左子节点,右边的键作为其右子节点。插入完成,变为平衡2-3查找树,树的高度从0变为1;

- 向一个父结点未2-结点的3-结点中插入新的元素:将新的元素插入到3-结点中,使其成为临时的4-结点,然后,将该结点中的中间元素提升到父结点即2-结点中,使其父结点成为一个3-结点,然后将左右结点分别挂在这个3-结点的恰当位置。

- 向一个父结点为3-结点的3-结点中插入新键:当我们向一个3-结点插入的时候,我们将该结点拆分,中间元素提升至父结点,又因为父结点是一个3-结点,所以父结点被临时提升为一个4-结点,然后继续拆分,直到遇到父结点为2-结点的,将其变为3-结点不需要进行拆分。

- 分解根结点:当插入的结点到根节点的路径上全部是3-结点时,将根节点临时变为一个4-结点,然后将根节点拆分为两个2-结点,树的高度+1

1.1.4 2-3树的性质
- 通过对2-3树插入操作的分析,我们发现在插入的时候,2-3树需要做一些局部的变换来保持2-3树的平衡。一颗完全平衡的2-3树具有以下性质:
- 任意空连接到根结点的路径长度都是相等的
- 4-结点变为3-结点时,树的高度不会发生变化,只有当根节点是临时的4-结点,分解根结点时,树高+1
- 2-3树与普通二叉查找树最大的区别在于,普通的二叉查找树是自顶向下生长的,而2-3树是自底向上生长的。
1.2 红黑树
- 红黑树对2-3进行编码,红黑树背后的基本思想是用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。我们将树中的链接分为两种类型:
- 红链接:将两个2-结点连接起来构成一个3-结点。
- 黑链接:是2-3树中的普通链接。
- 在红黑树中我们将3-结点表示为由一条左斜的红色链接(两个2-结点其中之一是另一个左子结点)相连的两个2-结点。这种表示方法的一个优点是,我们无需修改就可以直接使用标准的二叉查找树的get方法。

1.2.1 红黑树的定义
- 红黑树是含有红黑链接并满足下列条件的二叉查找树:
- 红链接均为左连接
- 没有任何一个结点同时和两条红结点相连
- 该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同。

1.2.2 红黑树结点API
- 因为每个结点都只会有一条指向自己的链接(从父结点指向它),我们可以在之前的Node结点中添加一个布尔类型的变量color来表示链接的颜色。如果指向它的链接是红色的,那么该变量的值为true,如果链接是黑色的,那么该变量的值为false;


1.2.3 平衡化
- 在对红黑树进行一些增删改查的操作后,很有可能会出现红色和右连接或者两条连续的红色的链接,而这些都不满足红黑树的定义,所以我们需要对这些情况通过旋转进行修复,让红黑树保持平衡
1.2.3.1 左旋
- 当某个结点的左子节点为黑色,右子节点为红色,此时需要左旋
- 左旋过程:
- 前提:当前结点为h,它的右子节点为x
- 让x的左子结点变为h的右子节点:h.right=x.left
- 让h成为x的左子节点:x.left=h
- 让h的color属性变为x的color属性值:x.color=h.color
- 让h的color属性变为RED: h.color=true

1.2.3.1 右旋
- 当某个结点的左子结点是红色,并且左子结点的左子结点也是红色,需要右旋
- 右旋过程
- 前提:当前结点为h,它的左子结点为x
- 让x的右子节点成为h的左子结点:h.left=x.right
- 让h成为x的右子结点: x.right=h
- 让x的color变为h的color属性值:x.color=h.color
- 让h的color为RED

1.2.4 向单个2-结点中红插入新键
- 一棵只含有一个键的红黑树只含有一个2-结点。插入另一个键后,我们马上就需要将他们旋转。
- 如果新键小于当前结点的键,我们只需要增加一个红色结点即可,新的红黑树和单个3-结点完全等价。

- 如果新键大于当前节点的键,那么新增的红色结点将会产生一条红色的右链接,此时我们需要通过左旋,把红色右连接变为左连接,插入操作才算完成。形成的新的红黑树依然和3-结点等价,其中含有两个键,一个红色链接。

1.2.5 向底部的2-结点插入新键
- 用和二叉查找树相同的方式向一个棵红黑树中插入一个新键,会在树的底部新增一个结点(可以确保有序性),唯一区别的地方是我们会用红链接将新结点和它的父结点相连。如果它的父结点是一个2-结点,那么刚才讨论的两种方式仍然适用。

1.2.6 颜色反转
- 当一个结点的左子结点和右子结点的color都是RED时,也就是出现了临时的4-结点,此时只需要把左子结点和右子节点的颜色变为BLACK,同时让当前结点的颜色变为RED即可。

1.2.7 向一棵双键树(即一个3-结点)中插入新键
- 这种情况可以分为三种情况:
- 新键大于原树中的两个键:

- 新键小于原树中的两个键

- 新键介于原数中的两个键之间

1.2.8 根结点的颜色总是黑色
- 因为颜色是表示父结点指向该结点的链接的颜色,根结点没有父结点,所以每次插入操作之后都将父结点的颜色置为黑色。
1.2.9 向树底部的3-结点插入新键
- 假设在树的底部的一个3-结点下加入一个新的结点。前面的三种情况都会出现。指向新结点的链接可能是3-结点的右连接(此时只需要转换颜色即可),或是左链接(此时我们需要进行右旋转然后在转换),或是中结点(此时需要先左旋转然后再右旋转,最后转换颜色)。颜色转换会使中间结点的颜色变红,相当于将它送入了父结点。这意味着父结点中继续插入一个新键,我们只需要使用相同的方法解决即可,直到遇到一个2-结点或者根结点为止。

1.2.10 红黑树的API设计

1.2.11 红黑树代码实现
package com.tiger.study.DataStructure.Tree;
import com.sun.org.apache.regexp.internal.RE;
import javax.xml.soap.Node;
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;
public int size() {
return N;
}
private boolean isRed(Node x) {
if (x == null) {
return false;
}
return x.color == RED;
}
private Node rotateLeft(Node h) {
Node x = h.right;
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
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;
return x;
}
private void flipColors(Node h) {
h.color = RED;
h.left.color = BLACK;
h.right.color = BLACK;
}
public void put(Key key, Value value) {
root = put(root, key, value);
root.color = BLACK;
}
private Node put(Node h, Key key, Value val) {
if (h == null) {
N++;
return new Node(key, val, null, null, RED);
}
int cmp = key.compareTo(h.key);
if (cmp < 0) {
h.left = put(h.left, key, val);
} else if (cmp >0) {
h.right = put(h.right, key, val);
} else {
h.value = val;
}
if (isRed(h.right) && !isRed(h.left)) {
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;
}
public Value get(Key key) {
return get(root, key);
}
public Value get(Node x, Key key) {
if (x == null) {
return null;
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
return get(x.left, key);
} else if (cmp > 0) {
return get(x.right, key);
} else {
return x.value;
}
}
public class Node {
public Key key;
private 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;
}
}
}
二. B-树
- B树是一种树状数据结构,它能够存储数据,对其进行排序并以O(logn)的时间复杂度进行查找、顺序读取、插入和删除等。
2.1 B树的特性
- B树允许一个结点中包含多个key,可以是3个、4个、5个甚至更多,并不确定,需要看具体的实现。现在我们选择要给参数M,来构造一个B树,我们可以把它称作是:M阶B树,那么该树有以下特点:
- 每个结点最多有M-1个key,并且以升序排列
- 每个结点最多能有M个子结点
- B结点至少有两个子结点

2.2 B树存储数据
- 若参数M选择为5,那么每个结点最多包含4个键值对,我们以5阶B树为例:

三. B+树
- B+树是对B树的一种变形树,它与B树的差异在于:
- 非叶子结点仅仅具有索引作用,也就是说非叶子结点只存储key,不存储value
- 树的所有叶子节点构成一个有序链表,可以按照key排序的次序遍历全部数据
3.1 B+树存储数据
- 选择参数M为5,每个结点最多包含4个键值对,我们以5阶B+树为例:

3.2 B+树和B树对比
- B+树优点:
- 由于B+树在非叶子节点上不包含真正的数据,当作索引使用,因此在内存相同的情况下,能够存放更多的key。
- B+树的叶子结点是相连的,因此对整棵树的遍历只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树需要进行每层遍历。
- B树的优点
- 由于B树的每一个节点都包含key和value,因此我们根据key查找value时,只需要找到key所在的位置,就能找到value,但B+树只有叶子结点存储数据,索引每一次查找,都必须一次一次,一直找到树的最大深度处,也就是叶子结点的深度,才能找到value。