平衡查找树
1、2-3查找树
定义:
一棵2-3查找树或为一棵空树。或又以下结点组成:
- 2-结点:含有一个键和两条连接
- 3-结点:含有两个键和三条连接
1.1查找
和标准的二叉查找树类似
1.2插入
有以下几种情况:
1、向2-结点中插入新键
- 把这个2-结点替换为一个3-结点
2、向一棵只含有一个3-结点的树中插入新键
- 先将新键存入该结点,成为一个4-结点**(新建4-结点)**
- 转换为3个2-结点**(分解)**
3、向一个父结点为2-结点的3-结点插入新键
- 新建4-结点
- 分解:将中键插入到父结点中
4、向一个父结点为3-结点的3-结点中插入新键
- 新建4-结点
- 分解:将中键插入到父结点中
- 此时父结点变为一个4-结点
- 递归对父结点进行同样的操作。
1.3局部变换
插入算法的变换是局部的:
- 除了相关的结点和链接之外不会修改或者检查树的其他部分
1.4全局性质
局部变换不会影响树的全局的有序性
和平衡性
:
- 任意空链接到根结点的路径长度都是相等的。
标准的二叉查找树是由上向下生长的,2-3树是由下向上生长的。
在一棵大小为N
的2-3树中,查找和插入的操作访问的结点必然不超过lgN
个。
2、红黑二叉查找树
2-3查找树性能很好,但是转换调整操作需要移动对象,太过复杂麻烦。
**红黑二叉查找树(红黑树)**就是用标准的二叉查找树和一些额外的信息来表示2-3树。
- 红链接:将两个2-结点连接起来构成一个3-结点。
- 黑链接:就是普通的链接。
2.1定义
红黑树就是满足以下条件的二叉查找树:
- 红链接均为左链接
- 没有任何一个结点同时和两条红链接相连
- 是完美黑色平的,即任意空链接到根结点的路径上的黑链接数量都是相等的。
我们将指向结点自己的链接的颜色存储在自己上。
2.2旋转
旋转操作可以改变红链接的指向。
左旋
目的:将一条红色右链接转化为一条红色左链接。
操作:
- 红色右链接连接着一个子结点和一个父结点,
- 只需要将子结点变为父结点,父结点变为左子结点。
右旋
和左旋相反。
2.3插入
有以下几种情况:
1、向单个2-结点插入新键
- 如果新键小于老键:直接在左新增一个红色结点
- 如果新键大于老键:在右新增一个红色结点,再左旋。
2、向树底的2-结点插入新键
- 同上。
3、向一棵双键树(即一个3-结点)插入新键
分3种情况:
- 如果新键大于原树的两个键:在父结点的右新建一个红色结点,将两条红链接变成黑色。
- 如果新键小于原树的两个键:在子结点的左新建一个红色结点,将上层的红链接右旋,得到第一种情况。
- 如果新键介于原树的两个键之间:在子结点的右新建一个红色结点,将下层的红链接左旋,得到第二种情况。
4、向树底的3-结点插入新键
- 同上
2.4颜色转换
- 将子结点的颜色由红变黑
- 将父结点的颜色由黑变红
但是,每次插入之后要将树的根结点置为黑色。
2.5将红链接在树中向上传递
在沿着插入点到根结点的路径上向上移动时,在经过的每个结点中顺序完成以下操作,就能完成插入操作:
- 如果右子结点是红色的,而左子结点是黑色的,进行左旋。
- 如果左子结点是红色的,且它的左子结点也是红色的,进行右旋。
- 如果左右子结点都是红色的,就进行颜色转换。
2.6实现
/**
* 红黑树
* @Author: AZhu
* @Date: 2021/3/5 23:30
*/
public class RedBlackBST<Key extends Comparable<Key>, Value> {
private static final boolean RED = true;
private static final boolean BLACK = false;
private Node root;//根结点
private class Node {
Key key;//键
Value value;//值
Node left, right;//左右子树
int N;//以该节点为根的树中的结点总数
boolean color;//指向自己的链接的颜色
public Node(Key key, Value value, int n, boolean color) {
this.key = key;
this.value = value;
this.N = n;
this.color = color;
}
}
private boolean isRed(Node x) {
if (x==null) return false;
return x.color == RED;
}
/**
* 左旋
* @param h
* @return
*/
private Node rotateLeft(Node h) {
Node x = h.right;
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = size(x.left) + size(x.right) + 1;
return x;
}
/**
* 右旋
* @param h
* @return
*/
private Node rotateRight(Node h) {
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = size(x.left) + size(x.right) + 1;
return x;
}
/**
* 返回树的结点数目
* @return
*/
public int size() {
return size(root);
}
private int size(Node x) {
if (x == null) {
return 0;
} else {
return x.N;
}
}
/**
* 颜色转换
* @param h
*/
private void flipColor(Node h) {
h.color = RED;
h.left.color = BLACK;
h.right.color = BLACK;
}
/**
* 查找key对应的value
* @param key
* @return
*/
public Value get(Key key) {
return get(root, key);
}
private 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;//找到了
}
}
/**
* 插入或更新一对键值对
* @param key
* @param value
*/
public void put(Key key, Value value) {
root = put(root, key, value);
root.color = BLACK;
}
private Node put(Node h, Key key, Value value) {
if (h==null) return new Node(key, value, 1, RED);//新建结点
int cmp = key.compareTo(h.key);//比较
if (cmp < 0) {
h.left = put(h.left, key, value);//到左子树
} else if (cmp > 0) {
h.right = put(h.right, key, value);//到右子树
} else {
h.value = value;//更新
}
//沿路径返回调整
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)) {
flipColor(h);
}
//增加路径上的每一个结点的N值
h.N = size(h.left) + size(h.right) + 1;
return h;
}
}
2.7分析
所有基于红黑树的符号表实现都能保证操作的运行时间为对数级别。
一棵大小为N
的红黑树的高度不会超过2lgN
。
红黑树是平衡的,因此查找比二叉查找树更快。
红黑树的有序性相关操作与二叉查找树一致,并且在最坏情况下的运行时间也是对数级别的。
- 因此,对于大小可能上亿的表,仍能保证在几十次比较之内完成这些操作。
简单的符号表实现的成本总结:
算法 | 最坏情况 | 平均情况 | 是否高效的支持有序性相关的操作 | ||
---|---|---|---|---|---|
查找 | 插入 | 查找 | 插入 | ||
顺序查找 | N | N | N/2 | N | 否 |
二分查找 | lgN | 2N | lgN | N | 是 |
二叉查找树 | N | N | 1.39lgN | 1.39lgN | 是 |
红黑树 | 2lgN | 2lgN | 1.00lgN | 1.00lgN | 是 |
3答疑
Q1:为什么不存在红色右链接和4-结点?
A1:因为这样可以大大减少代码量。
Q2:为什么不在Node类型中用一个数组来表示2-结点和3-结点?
A2:这样的话就是B树了,并且数组带来的额外开销太大了。