1. 平衡树
学习过二叉查找树,发现它的查询效率比单纯的链表和数组的查询效率要高很多。
大部分情况下确实是这样的,但不幸的是,在最坏情况下,二叉查找树的性能还是很糟糕。
例如我们一次往二叉树中插入9,8,7,6,5,4,3,2,1这9个数据,那么最终构造出来的树长得是下面这个样子
我们会发现,如果我们要查询1这个元素,查询的效率依旧很低,效率低的原因在于这个树并不平衡,全都是向左分支,如果我们有一种方法能够不受插入数据的影响,让生成的树都像完全二叉树那样,那么即使在最坏情况下,查找的效率依旧会很好。
2. 2-3查找树
一颗2-3查找树要么为空,要么满足下面两个要求:
- 2-节点
- 含有一个键(及其对应的值)和两条链,
- 左链接指向2-3树中的键都小于该节点,
- 右链接指向的2-3树中的键都大于该节点
- 含有一个键(及其对应的值)和两条链,
- 3-节点
- 含有两个键(及其对应的值)和三条链,
- 左链接指向2-3树中的键都小于该节点,
- 中链接指向的2-3树中的键都位于该节点的两个键之间,
- 右链接指向的2-3树中的键都大于该节点
- 含有两个键(及其对应的值)和三条链,
2.1 查找
将二叉查找树的查找算法一般化,我们就能够直接得到2-3树的查找算法。
要判断一个键是否在树中,我们先将它和根节点中的键比较。
如果它和其中一个键相等,查找命中;否则我们就根据比较的结果找到指向相应区间的链接,并在其指向的子树中递归地继续查找。如果这个是空链接,查找未命中。
2.2 插入
2.2.1 向2-节点中插入新键
往2-3树种插入元素和往二叉查找树中插入元素一样,首先要进行查找,任何及那个节点挂到未找到的节点上。2-3树之所以能够保证在最差的情况下的效率的原因在于其插入之后仍然能够保持平衡状态。
如果查找后未找到的节点是一个2-节点,那么很容易,我们只需要将新的元素放到这个2-节点里面使其变成一个3-节点即可。但是如果查找的节点结束于一个3-节点,那么可能有点麻烦
2.2.2 向一颗只含一个3-节点的树中插入新键
假设2-3树只包含一个3-节点,这个节点有两个节点,没有空间来插入第三个键了,最自然的方式是它们假设这个节点能存放3个元素,暂时使其变成一个4-节点,同时它包含四条链接。然后,我们将这个4-节点的中间元素提升,左边的键作为其左子节点,右边的键作为其右子节点。插入完成,变为平衡2-3查找树,树的高度从0变为1
2.2.3 向一个父节点为2-节点的3-节点中插入新键
和上面的情况一样,我们也可以将新的元素插入到3-节点中,使其成员一个临时的4-节点,然后将该节点的中间元素提升到父节点即2-节点中,使其父节点成为一个3-节点,然后将左右节点分别挂在这个3-节点的恰当位置
2.2.4 向一个父节点为3-节点的3-节点中插入新键
当我们插入的节点是3-节点的时候,我们将该节点拆分,中间元素提升至父节点,但是此时父节点是一个3-节点,插入之后,父节点变成了4-节点,然后继续将中间元素提升至其父节点,直至遇到一个父节点是2-节点,然后将其变为3-节点,不需要继续进行拆分
2.2.5 分解根节点
当插入节点到根节点的路径上全部都是3-节点的时候,最终我们的根节点会变成一个临时的4-节点,此时,就需要将根节点拆分为2个2-节点,树的高度如下
2.4 2-3树的性质
通过对2-3树插入操作的分析,我们发现在插入的时候,2-3树需要做一些局部的变换来保持2-3树的平衡
一颗完全平衡的2-3树具有以下性质:
- 任意空链接到根节点的路径长度都是相等的。
- 4-节点变换为3-节点时,树的高度不会发生变化,只有当根节点时临时的4-节点,分解根节点时,树的高度才会+1
- 2-3树于普通二叉查找树的最大的区别在于,普通的二叉查找树是自定向下生长,而2-3树是自底向上生长
2.5 2-3树的实现
直接实现2-3树比较复杂,因为:
- 需要处理不同的节点类型,非常繁琐;
- 需要多次比较操作来将节点下移
- 需要上移来拆分4-节点
- 拆分4-节点的情况有很多种
2-3查找树实现起来比较比较复杂,在某些情况插入后的平衡操作可能会使得效率降低。但是2-3查找树作为一种比较重要的概念和思路对于红黑树、B树和B-树非常重要
3. 红黑树
3.1 红黑树的概述
- 平衡树中的一种,基于二叉树,实现思想来自于2-3树
在2-3树的实现原理中,可以看到2-3树能保证在插入元素后,树依然保持平衡状态。
它的最坏情况下所有子节点都是2-节点,树的高度为lgN,
相比于我们普通的二叉查找树,最坏情况下树的高度为N,确实保证了最坏情况下的时间复杂度,
但是2-3树实现起来过于复杂,所以下面将学习**基于2-3树思想实现的红黑树**
红黑树主要是对2-3树进行编码,红黑树背后的基本思想使用标准的二叉查找树(完全由2-节点构成)和一些额外的信息(替换3-节点)来表示2-3树。将红黑树中的链接分为两种类型:
- 红链接:
- 将两个2-节点连接起来构成一个3-节点
- 黑链接:
- 2-3树中的普通链接
确切的说,我们将3-节点表示为由一条左斜的红色链接(两个2-节点其中之一是另一个左子节点)相连的两个2-节点。这种表示法的一个优点是:我们无需修改就可以直接使用标准的二叉查找树的get方法
3.2 红黑树的定义
红黑树是含有红黑链接并满足下列条件的二叉查找树:
- 红链接均为左链接
- 没有任何一个节点同时和两条红链接相连
- 概述是完美黑色平衡的,即任意空链接到根节点的路径上的黑链接数量相同
下面是红黑树于2-3树的对应关系:
3.3 红黑树的节点API
因为每个节点都只会由一条指向自己的链接(从它的父节点指向它),我们可以在之前的Node节点中添加一个布尔类型的遍历color来表示链接的颜色。
如果指向它的链接是红色的,那么该变量的值为true,如果链接是黑色的,那么该变量的值为false
3.3.1 节点类API设计
类名 | 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:由其父节点指向它的链接的颜色 |
3.4 平衡化
在对红黑树进行一些增删查改的操作后,很有可能会出现红色的有链接或者两条连续红色的链接,而这些都不符合红黑树的定义(详看1.2),所以我们需要对这些情况通过旋转来进行修复,让红黑树保持平衡。
3.4.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;
3.4.2 右旋
在某个节点的左子节点是红色,且左子节点也是红色,需要节点
**前提:**当前节点为h,它的左子节点为x
右旋过程:
- 让x的右子节点成为h的左子节点:h.left = x.right;
- 让h成为x的右子节点:x.right = h;
- 让x的color变为h的color属性值:x.color = h.color;
- 让h的color为RED
3.5 插入
3.5.1 向单个2-节点中插入新键
一颗只含有一个键的红黑树只含有一个2-节点。插入另一个键后,我们马上就需要将它们旋转。
- 如果新键小于当前节点的键,我们只需要新增一个红色节点即可,新的红黑树和单个3-节点完全等价。
- 如果新键大于当前节点的键,那么新增的红色节点将会产生一条红色的有链接,此时我们需要通过左旋,把红色右链接变成做左链接,插入操作才算完成。形成的新红黑树依然和3-节点等价,其中含有两个键,一条红色链接。
3.5.2 向底部的2-节点插入新键
用和二叉查找树相同的方向向一颗红黑树中插入一个新键,会在树的底部新增一个节点(可以保证有序性),唯一区别的地方是我们会用红链接将新节点和它的父节点相连。如果它的父节点是一个2-节点,那么刚才讨论的两种方式仍然适用。
3.5.3 颜色反转
当一个节点的左子节点和右子节点的color都为RED时,也就是出现了临时的4-节点,此时直选哟把左子节点和右子节点的颜色变为BLACK,同时让当前节点的颜色变为RED即可。
3.5.4 向一颗双键树(3-节点)中插入新键
这种情况还可以有三种子情况:
-
新键大于原树种的两个键:
-
新键小于原树中的两个键:
-
新键介于原树种两个键之间:
3.5.5 根节点的颜色总是黑色
之前介绍节点API的时候,在节点Node对象中color属性表示的是父节点指向当前节点的连接的颜色,由于根节点不存在父节点,所以每次插入操作后,我们都需要把根节点的颜色设置为黑色。
3.5.6 向树底的3-节点插入新键
假设在树的底部的一个3-节点下加入一个新的节点(前面三种子情况都出现了)。指向新节点的链接可能是3-节点的右链接(此时转换颜色),或是左连接(此时需要右旋后再转换颜色),或是中连接(此时先左旋再右旋左后转换颜色)。
颜色转换会使中间节点的颜色变红,相当于将它送入了父节点。这意味着父节点中继续插入一个新键,我们只需要适用相同的方法解决即可,直到遇到一个2-节点或者根节点为止
3.6 红黑树的API设计
3.7 红黑树的实现
package com.renexdemo.tree;
// 红黑树
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 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;
}
}
// 初始化红黑树
public RedBlackTree() {
}
// 链接是否为红色
private boolean isRed(Node x){
if (x == null){
return false;
}
return x.color == RED;
}
// 树的节点个数
public int size(){
return N;
}
// 左旋
private Node rotateLeft(Node h){
// 获取h节点的右子节点,表示为x
Node x = h.right;
// 让x节点的左子节点成为h节点的右子节点
h.right = x.left;
// 让h成为x节点的左子节点
x.left = h;
// 让x节点的color属性等于h节点的color属性
x.color = h.color;
// 让h节点的color属性变为红色
h.color = RED;
return x;
}
// 右旋
private Node rotateRight(Node h){
// 获取h节点的左子节点,表示为x
Node x = h.left;
// 让x节点的右子节点成为h节点的左子节点
h.left = x.right;
// 让h成为x节点的右子节点
x.right = h;
// 让x节点的color属性等于h节点的color属性
x.color = h.color;
// 让h节点的color属性变为红色
h.color = RED;
return x;
}
//颜色反转
private void flipColors(Node h){
// 当前节点变为红色
h.color = RED;
// 左子节点和右子节点变为黑色
h.right.color = BLACK;
h.left.color = BLACK;
}
// 添加节点
public void put(Key key,Value val){
root = put(root,key,val);
root.color = BLACK;// 根节点的颜色总是黑色的
}
// 指定节点处添加节点
private Node put(Node h,Key key,Value val){
// 判断h是否为空
if (h == null) {
N++;
return new Node(key,val,null,null,RED);
}
// 比较h节点的键和k的大小
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;
}
// 进行左旋;当当前节点节点h的左子节点为黑色,右子节点为红色,需要左旋
if (isRed(h.right) && !isRed(h.left)){
h = rotateLeft(h);
}
// 进行右旋:当当前节点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);
}
// 获得指定节点处对应键的值
private Value get(Node x,Key key){
if (x==null){
return null;
}
// 比较x的键和key的大小
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;
}
}
}
4. 前置文章
- 浅入数据结构 “堆” - 实现和理论
- 开始熟悉 “二叉树” 的数据结构
- 队列 和 符号表 两种数据结构的实现
- 队列的进阶结构-优先队列
5. ES8 如何使用?
快来看看这篇好文章吧~~!!
😊👉(全篇详细讲解)ElasticSearch8.7 搭配 SpringDataElasticSearch5.1 的使用