文章目录
- 1、引入
- 2、左旋和右旋
- 3、AVL树
- 3.1 AVL 树的平衡条件
- 3.2 搜索二叉树如何删除节点
- 3.3 AVL树的平衡性被破坏的四种类型
- 3.4 AVL 树平衡性如何检查?如何调整失衡?
- 3.4.1 AVL树新增节点如何检查树的平衡性?
- 3.4.2 AVL树删除节点如何检查树的平衡性?
- 3.5 AVL 树代码实现
- 3.6 有序表和 AVL 树的关系
1、引入
如果数据库中只是按照顺序存储数据,那么找数据的时候就必须遍历,整个过程非常慢。于是在数据库中有一个重要的优化——建索引。
可以建索引的前提是被索引的字段是可排序的。
那么排序是如何加速找数据库的数据的?
数据库中的数据可能在硬盘里就是顺序存储的,但是数据库一定会在这个底层的数据库上建立一棵索引树。
索引树就是搜索二叉树(左孩子数据 < 根节点数据 < 右孩子数据)或者叫做有序树,那么给定一个字段,在有序树中就能快速得找到该数据在物理结构中的真实位置,就能快速取出数据。注意,如果只是单纯地找某个数据,那么哈希表性能更好。但是现实世界中,通常查询的不单单是某个数据,更多的是范围查询,比如年龄在某个范围或者以ABC开头的姓名这种业务,这是哈希表无法完成的,但是有序结构可以。
实际上,搜索二叉树本身就能支持索引,增删改查都可以在这棵树上完成。但是有一个瓶颈——树的高度。如果用户的数据是顺序给的,那这棵树就变成了一个链表,则这棵树就没有效率可言了。这种时候的算法性能就是用户的输入数据决定的。
于是,就要涉及到 平衡搜索二叉树 的概念了。即保证一棵树是搜索二叉树的前提下(搜索性),还要保证它的平衡性。所谓的平衡性就是希望最高的高度是 l o g N logN logN(其中 N N N是数据量)。
2、左旋和右旋
先来讲讲 左旋 和 右旋 的概念。
注意:左旋/右旋的时候一定要指明是以哪个节点为头结点进行左旋/右旋,往哪边旋就是头结点往哪边倒。
左旋和右旋并不会改变搜索二叉树的性质。
经典的有序表会有多种实现,AVL树、SB树、红黑树都有各自的实现,会有自己使用左旋和右旋的策略,但是不管是哪种平衡搜索二叉树,不管你的平衡性如何定义,让它变平衡的基本操作只有左旋和右旋。
不同的有序表的树(AVL树、SB树、红黑树、B+树等)的差别在于规定的平衡性不一样,相似点是不管是哪种平衡树都可以保证增删改查的时间复杂度为 O ( l o g N ) O(logN) O(logN),即它们在时间复杂度指标上没有区别。
3、AVL树
3.1 AVL 树的平衡条件
AVL树具有最严苛的平衡性:任意一个节点的左树高度和右树高度之差不超过2,即 ∣ H e i g h t 左 − H e i g h t 右 ∣ < 2 |Height_左 - Height_右| \lt 2 ∣Height左−Height右∣<2
3.2 搜索二叉树如何删除节点
搜索二叉树的增查改节点都比较简单,但是删除节点就稍微复杂一些,如下是搜索二叉树分情况删除节点:
AVL树的增删节点操作和搜索二叉树一样,但是区别在AVL树在增删节点后有平衡动作。
3.3 AVL树的平衡性被破坏的四种类型
- LL型:左子树的左孩子导致了失衡,只需要做一次右旋
- LR型:左子树的右子树高度过高,导致了失衡,需要进行做两次旋转,一次失衡节点的左子树左旋,一次失衡的节点右旋,也就是LR型先通过左旋变成LL型,再右旋
- RR型:右子树的右孩子导致了失衡,只需要做一次左旋
- RL型:同理LR型,先进行一次右旋,再进行一次左旋。即先通过右旋将RL变成RR型,再左旋
有个细节需要注意:失衡有可能是(LL型 && LR型) 或者 (RR型 && RL)型。比如:
假设右子树上删除了一个节点导致右子树的高度变成了5,而左子树高度仍然是7,且左子树的左右孩子的高度均为6,这时候就对于A来说就同时出现了 LL 和 LR 型违规,这时候一定要按照 LL 型来调整,即 LL & LR 默认为 LL型即可。
再举个例子:
小结
假设 X 为根节点的树失衡
- 如果是 LL 型失衡,则 X 节点进行一次右旋;
- 如果是 LR 型失衡,则 X 节点的左孩子进行一次左旋,然后 X 节点进行一次右旋;
- 如果是 RR 型失衡,则 X 节点进行一次左旋;
- 如果是 RL 型失衡,则 X 节点的右孩子进行一次右旋,然后 X 节点进行一次左旋;
- 如果是 LL & LR 型失衡,则按照 LL 型调整;
- 如果是 RR & RL 型失衡,则按照 RR 型调整。
无论是上述哪种类型的失衡,都能在 O ( 1 ) O(1) O(1) 时间复杂度内调整平衡。
3.4 AVL 树平衡性如何检查?如何调整失衡?
3.4.1 AVL树新增节点如何检查树的平衡性?
如果 X 节点挂在了树的某个具体位置,先检查 X 节点中了上述的哪种失衡类型;然后检查 X 节点的父节点是哪种失衡类型;接着检查 X 节点的父节点的父节点是哪种失衡类型,一直到头结点。即从 X 节点到根节点沿途的所有节点全部都要检查平衡性。
AVL树的调整不是只对一个节点或者只对头结点的平衡性进行检查,而是只要有节点加入,沿途的每个节点都要检查。
如果 AVL 树在加入节点之前最大高度是 l o g N logN logN,那么加入一个节点后,即便沿途到头结点的节点都检查,一共最多也就 l o g N logN logN 个节点,而每对一个节点做四种失衡类型的检查并调整的时间复杂度是 O ( 1 ) O(1) O(1),所以加入节点后对沿途的每个节点都检查调整也就是 O ( l o g N ) O(logN) O(logN) 的代价。
3.4.2 AVL树删除节点如何检查树的平衡性?
-
如果删除的 X 节点是叶子节点,则检查从删除节点开始一直到头结点沿途的所有节点全部都要检查是否平衡;
-
如果删除的 X 节点有右孩子,无左孩子,直接用右孩子替代X节点的环境,则要检查从右孩子来到的位置开始往上到头结点沿途的所有节点的平衡性;有左孩子,无右孩子同理;
-
如果删除的 X 节点既有左孩子,又有右孩子。如下:
3.5 AVL 树代码实现
public class AVLTreeMap {
//AVL树的节点
//AVL树是有序表中用到的,所以Key必须是可比较的,所以此处的 K 一定要实现为一个可比较的类
public static class AVLNode<K extends Comparable<K>, V> {
public K k;
public V v;
public AVLNode<K, V> l; //指向左孩子
public AVLNode<K, V> r; //指向右孩子
public int h; //平衡因子:高度,整棵树的高度
public AVLNode(K key, V value) {
k = key;
v = value;
h = 1;
}
}
//AVL树实现的有序表
public static class AVLTreeMap<K extends Comparable<K>, V> {
private AVLNode<K, V> root; //根节点
private int size; //节点的数量
public AVLTreeMap() {
root = null;
size = 0;
}
//右旋:对cur这棵树右旋
private AVLNode<K, V> rightRotate(AVLNode<K, V> cur) {
AVLNode<K, V> left = cur.l; //记录cur的左孩子
cur.l = left.r; //左孩子的右孩子 成为 cur的左孩子
left.r = cur; //左孩子的右孩子 变成 cur
//接管高度
//先更新 cur 的高度
cur.h = Math.max((cur.l != null ? cur.l.h : 0), (cur.r != null ? cur.r.h : 0)) + 1;
//再更新 left 的高度
left.h = Math.max((left.l != null ? left.l.h : 0), (left.r != null ? left.r.h : 0)) + 1;
return left; //以谁做头,就返回谁
}
//左旋
private AVLNode<K, V> leftRotate(AVLNode<K, V> cur) {
AVLNode<K, V> right = cur.r;
cur.r = right.l;
right.l = cur;
cur.h = Math.max((cur.l != null ? cur.l.h : 0), (cur.r != null ? cur.r.h : 0)) + 1;
right.h = Math.max((right.l != null ? right.l.h : 0), (right.r != null ? right.r.h : 0)) + 1;
return right;
}
//平衡调整:以cur为头的这棵树上进行平衡调整
private AVLNode<K, V> maintain(AVLNode<K, V> cur) {
if (cur == null) {
return null;
}
int leftHeight = cur.l != null ? cur.l.h : 0; //左树的高度
int rightHeight = cur.r != null ? cur.r.h : 0; //右树的高度
if (Math.abs(leftHeight - rightHeight) > 1) { //失衡
if (leftHeight > rightHeight) { //左树高度 > 右树高度
//左树的左树高度
int leftLeftHeight = cur.l != null && cur.l.l != null ? cur.l.l.h : 0;
//左树的右树高度
int leftRightHeight = cur.l != null && cur.l.r != null ? cur.l.r.h : 0;
//=号表示 LL & LR型,归为LL型
if (leftLeftHeight >= leftRightHeight) { //LL型 或者 既是LL又是LR型 ,进行右旋
cur = rightRotate(cur);
} else { //LR型,先cur的左孩子进行左旋,然后cur整体右旋
cur.l = leftRotate(cur.l);
cur = rightRotate(cur);
}
} else { //左树高度 < 右树高度
int rightLeftHeight = cur.r != null && cur.r.l != null ? cur.r.l.h : 0;
int rightRightHeight = cur.r != null && cur.r.r != null ? cur.r.r.h : 0;
if (rightRightHeight >= rightLeftHeight) { //RR型 或者 既是RR型又是RL型,左旋
cur = leftRotate(cur);
} else { //RL型,cur的右孩子先右旋,然后cur整体左旋
cur.r = rightRotate(cur.r);
cur = leftRotate(cur);
}
}
}
return cur; //返回调整后的头结点
}
// 找到 <=key 离 key 最近的 (与平衡性无关)
private AVLNode<K, V> findLastIndex(K key) {
AVLNode<K, V> pre = root;
AVLNode<K, V> cur = root;
while (cur != null) {
pre = cur;
if (key.compareTo(cur.k) == 0) {
break;
} else if (key.compareTo(cur.k) < 0) {
cur = cur.l;
} else {
cur = cur.r;
}
}
return pre;
}
// 找到 >= key 离 key 最近的
private AVLNode<K, V> findLastNoSmallIndex(K key) {
AVLNode<K, V> ans = null;
AVLNode<K, V> cur = root;
while (cur != null) {
if (key.compareTo(cur.k) == 0) {
ans = cur;
break;
} else if (key.compareTo(cur.k) < 0) {
ans = cur;
cur = cur.l;
} else {
cur = cur.r;
}
}
return ans;
}
private AVLNode<K, V> findLastNoBigIndex(K key) {
AVLNode<K, V> ans = null;
AVLNode<K, V> cur = root;
while (cur != null) {
if (key.compareTo(cur.k) == 0) {
ans = cur;
break;
} else if (key.compareTo(cur.k) < 0) {
cur = cur.l;
} else {
ans = cur;
cur = cur.r;
}
}
return ans;
}
// 添加节点:以cur为根节点的树上添加新的记录(key,value),返回整棵树的头结点
// 如果 key<cur.k,则新节点添加到cur的左边
// 如果 key > cur.k,则新节点添加到cur的右边
// 如果 key = cur.k,直接将cur的v值更新为value,搜索二叉树不接收重复的K
private AVLNode<K, V> add(AVLNode<K, V> cur, K key, V value) { //该方法的潜台词就是不会有相同的key插入
if (cur == null) { //空树
return new AVLNode<K, V>(key, value);
} else {
if (key.compareTo(cur.k) < 0) { //key < cur.k,去cur的左树上添加
cur.l = add(cur.l, key, value); //添加调整后可能换头,所以必须用cur.l接住该头
} else {
cur.r = add(cur.r, key, value);
}
cur.h = Math.max(cur.l != null ? cur.l.h : 0, cur.r != null ? cur.r.h : 0) + 1;
return maintain(cur); //对cur这棵树平衡性的检查,调整后可能换头,所以返回的是一个节点
}
}
// 删除节点:在cur这棵树上,删掉key所代表的节点,返回cur这棵树的新头部
private AVLNode<K, V> delete(AVLNode<K, V> cur, K key) { //潜台词是存在key所代表的节点
if (key.compareTo(cur.k) > 0) {
cur.r = delete(cur.r, key);
} else if (key.compareTo(cur.k) < 0) {
cur.l = delete(cur.l, key);
} else {
if (cur.l == null && cur.r == null) { //既无左孩子,也无右孩子
cur = null;
} else if (cur.l == null && cur.r != null) { //无左孩子,有右孩子
cur = cur.r;
} else if (cur.l != null && cur.r == null) { //无右孩子,有左孩子
cur = cur.l;
} else { //既有左孩子,也有右孩子
AVLNode<K, V> des = cur.r;
while (des.l != null) { //找到右子树上最左节点
des = des.l;
}
cur.r = delete(cur.r, des.k); //在右子树上删除 des 这个节点,并进行平衡性的调整
//用des节点替换cur节点
des.l = cur.l;
des.r = cur.r;
cur = des;
//注意:此时,cur的右子树的平衡性已经调整完毕,然后从cur开始向上的节点进行平衡性检查与调整,与上文中讲到的平衡性的调整略有不同,此处时进行了分离
}
}
if (cur != null) {
cur.h = Math.max(cur.l != null ? cur.l.h : 0, cur.r != null ? cur.r.h : 0) + 1;
}
return maintain(cur);//每次递归都会检查一遍自己,所有受到影响的节点全部都会检查一遍
}
public int size() {
return size;
}
public boolean containsKey(K key) {
if (key == null) {
return false;
}
AVLNode<K, V> lastNode = findLastIndex(key);
return lastNode != null && key.compareTo(lastNode.k) == 0 ? true : false;
}
public void put(K key, V value) {
if (key == null) {
return;
}
AVLNode<K, V> lastNode = findLastIndex(key);
if (lastNode != null && key.compareTo(lastNode.k) == 0) {
lastNode.v = value;
} else {
size++;
root = add(root, key, value);
}
}
public void remove(K key) {
if (key == null) {
return;
}
if (containsKey(key)) {
size--;
root = delete(root, key);
}
}
public V get(K key) {
if (key == null) {
return null;
}
AVLNode<K, V> lastNode = findLastIndex(key);
if (lastNode != null && key.compareTo(lastNode.k) == 0) {
return lastNode.v;
}
return null;
}
public K firstKey() {
if (root == null) {
return null;
}
AVLNode<K, V> cur = root;
while (cur.l != null) {
cur = cur.l;
}
return cur.k;
}
public K lastKey() {
if (root == null) {
return null;
}
AVLNode<K, V> cur = root;
while (cur.r != null) {
cur = cur.r;
}
return cur.k;
}
public K floorKey(K key) {
if (key == null) {
return null;
}
AVLNode<K, V> lastNoBigNode = findLastNoBigIndex(key);
return lastNoBigNode == null ? null : lastNoBigNode.k;
}
public K ceilingKey(K key) {
if (key == null) {
return null;
}
AVLNode<K, V> lastNoSmallNode = findLastNoSmallIndex(key);
return lastNoSmallNode == null ? null : lastNoSmallNode.k;
}
}
}
3.6 有序表和 AVL 树的关系
有序表是接口名,AVL树是具体的类或者说实现,AVL树可以实现有序表,Size-Blanced树、红黑树、跳表、B树和B+树也可以实现有序表,具体的实现细节不同。但是有序表应该有的功能,每种树都可以实现;有序表要求的性能,无论用哪种树实现都是 O ( l o g N ) O(logN) O(logN)。
有序表中要求的所有功能和平衡性的调整毫无关系,只要能够玩转搜索二叉树就能改出有序表要求的所有功能。任何树关于平衡性的调整都只是一个补丁。