一、概述
1. 历史
AVL树是一种自平衡二叉搜索树,由托尔·哈斯特罗姆在1960年提出并在1962年发表。它的名字来源于发明者的名字:Adelson-Velsky和Landis,他们是苏联数学家,于1962年发表了一篇论文,详细介绍了AVL树的概念和性质。
在二叉搜索树中,如果插入的元素按照特定的顺序排列,可能会导致树变得非常不平衡,从而降低搜索、插入和删除的效率。为了解决这个问题,AVL树通过在每个节点中维护一个平衡因子来确保树的平衡。平衡因子是左子树的高度减去右子树的高度。如果平衡因子的绝对值大于等于2,则通过旋转操作来重新平衡树。
AVL树是用于存储有序数据的一种重要数据结构,它是二叉搜索树的一种改进和扩展。它不仅能提高搜索、插入和删除操作的效率,而且还能够确保树的深度始终保持在O(log n)的水平。随着计算机技术的不断发展,AVL树已经成为了许多高效算法和系统中必不可少的一种基础数据结构。
前面介绍过,如果一棵二叉搜索树长的不平衡,那么查询的效率会受到影响,如下图
通过旋转可以让树重新变得平衡,并且不会改变二叉搜索树的性质(即左边仍然小,右边仍然大)
2. 如何判断失衡
如果一个节点的左右孩子,高度差超过1,则此节点失衡,才需要旋转。
3. 处理高度
如何得到节点高度?一种方式之前做过的一道题目:求二叉树的最大深度(高度),但由于求高度是一种非常频繁的操作,因此将高度作为节点的一个属性,将来新增或删除时及时更新,默认为1
static class AVLNode {
int key;
Object value;
AVLNode left;
AVLNode right;
int height = 1; // 高度
public AVLNode(int key) {
this.key = key;
}
public AVLNode(int key, Object value) {
this.key = key;
this.value = value;
}
public AVLNode(int key, Object value, AVLNode left, AVLNode right) {
this.key = key;
this.value = value;
this.left = left;
this.right = right;
}
}
求高度
这里加入了height函数方便求节点为null时的高度
// 求节点的高度
private int height(AVLNode node) {
return node == null ? 0 : node.height;
}
更新高度
将来新增、删除、旋转时,高度都可能发生变化,需要更新。
// 更新节点高度(新增、删除、旋转)
private void updateHeight(AVLNode node) {
node.height = Integer.max(height(node.left), height(node.right)) + 1;
}
4. 何时触发失衡判断
定义平衡因子(balance factor)如下
平衡因子 = 左子树高度 - 右子树高度
当平衡因子
- bf = 0, 1, -1时,表示左右平衡
- bf > 1时,表示左边太高
- bf < -1时,表示右边太高
/**
* 衡因子 balance factor = 左子树高度 - 右子树高度
* bf = 0, 1, -1时,表示左右平衡
* bf > 1时,表示左边太高
* bf < -1时,表示右边太高
* @param node
* @return
*/
private int bf(AVLNode node) {
return height(node.left) - height(node.right);
}
当插入新节点时,或删除节点时,引起高度变化时,例如
目前此树平衡,当再插入一个4时,节点们的高度都产生了相应的变化,8节点失衡了
再比如说,下面这棵树一开始也是平衡的
当删除节点8时,节点们的高度都产生了相应的变化,6节点失衡了
5. 失衡的四种情况
LL
- 失衡节点的bf > 1,即左边更高
- 失衡节点的左孩子bf >= 0,即左孩子这边也是左边更高或等高
LR
- 失衡节点的bf > 1,即左边更高
- 失衡节点的左孩子的 bf < 0,即左孩子这边是右边更高
RL
- 失衡节点的 bf < -1,即右边更高
- 失衡节点的右孩子的bf > 0,即右孩子这边左边更高
RR
- 失衡节点的bf < -1,即右边更高
- 失衡节点的右孩子的 bf <= 0,即右孩子这边右边更高或等高
二、实现
1. 解决失衡
失衡可以通过树的旋转解决。什么是树的旋转呢?它是在不干扰元素顺序的情况下更改结构,通常用来让树的高度变得平衡。
观察下面一棵二叉搜索树,可以看到,旋转后,并未改变树的左小右大特性,但根、父、孩子节点都发生了变化
LL - 右旋
旋转前
- 红色节点,旧根(失衡节点)
- 黄色节点,旧根的左孩子,将来作为新根,旧根是它右孩子
- 绿色节点,新根的右孩子,将来要换爹作为旧根的左孩子
旋转后
代码:
/**
* 右旋
* @param red 要旋转的节点(失衡)
* @return 新的根节点
*/
private AVLNode rightRotate(AVLNode red) {
AVLNode yellow = red.left;
// AVLNode green = yellow.right;
yellow.right = red; // 上位
red.left = yellow.right; // 换爹
// 更新节点高度
updateHeight(red);
updateHeight(yellow);
return yellow;
}
RR - 左旋
旋转前
- 红色节点,旧根(失衡节点)
- 黄色节点,旧根的右孩子,将来作为新根,旧根是它左孩子
- 绿色节点,新根的左孩子,将来要换爹作为旧根的右孩子
旋转后
代码:
/**
* 左旋
* @param red 要旋转的节点(失衡)
* @return 新的根节点
*/
private AVLNode leftRotate(AVLNode red) {
AVLNode yellow = red.right;
red.right = yellow.left;
yellow.left = red;
// 更新节点高度
updateHeight(red);
updateHeight(yellow);
return yellow;
}
LR - 左右旋
指先旋转左子树,再右旋根节点(失衡),这时一次旋转并不能解决失衡
左子树旋转后 - 左旋
根右旋前
根右旋后
代码:
/**
* 左右旋
* 先左旋左子树,再右旋根节点
* @param root
* @return
*/
private AVLNode leftRightRotate(AVLNode root) {
root.left = leftRotate(root.left);
return rightRotate(root);
}
RL - 右左旋
指先右旋右子树,再左旋根节点(失衡)
右子树右旋后
根左旋前
根左旋后
代码:
/**
* 右左旋
* 先右旋右子树,再左旋根节点
* @param root
* @return
*/
private AVLNode rightLeftRotate(AVLNode root) {
root.right = rightRotate(root.right);
return leftRightRotate(root);
}
判断及调整平衡
/**
* 判断及调整平衡代码
* @param node
* @return
*/
private AVLNode balance(AVLNode node) {
if(node == null) {
return null;
}
int bf = bf(node);
if(bf > 1 && bf(node.left) >= 0) {
// LL - 右旋
return rightRotate(node);
} else if(bf > 1 && bf(node.left) < 0) {
// LR - 左右旋
return leftRightRotate(node);
} else if(bf < -1 && bf(node.right) > 0) {
// RL - 右左旋
return rightLeftRotate(node);
} else if(bf < -1 && bf(node.right) <= 0) {
// RR - 左旋
return leftRotate(node);
}
return node;
}
以上四种旋转代码里,都需要更新高度,需要更新的节点是红色、黄色,而绿色节点高度不变。
2. 新增
/**
* 新增节点
* @param key
* @param value
*/
public void put(int key, Object value) {
root = doPut(root, key, value);
}
private AVLNode doPut(AVLNode node, int key, Object value) {
// 1. 找到空位,创建新结点返回
if(node == null) {
return new AVLNode(key, value);
}
// 2. key已有,更新
if(key == node.key) {
node.value = value;
return node;
}
// 3. 继续查找
if(key < node.key) {
node.left = doPut(node.left, key, value);
} else {
node.right = doPut(node.right, key, value);
}
// 更新节点高度
updateHeight(node);
// 重新调整二叉搜索树
return balance(node);
}
3. 删除
/**
* 删除节点
* @param key
*/
public void remove(int key) {
root = doRemove(root, key);
}
private AVLNode doRemove(AVLNode node, int key) {
// 1. node == null
if(node == null) {
return null;
}
// 2. 没找到key
if(key < node.key) {
node.left = doRemove(node.left, key);
} else if(node.key < key) {
node.right = doRemove(node.right, key);
} else {
// 3. 找到key 1)没有孩子节点 2)只有一个孩子 3)有两个孩子
if(node.left == null && node.right == null) {
// 情况1 没有孩子节点
return null;
} else if(node.left == null) {
// 情况2 只有右孩子
node = node.right;
} else if(node.right == null) {
// 情况3 只有左孩子
node = node.left;
} else {
// 情况4 有两个孩子
AVLNode s = node.right;
while(s.left != null) {
s = s.left;
}
s.right = doRemove(node.right, s.key);
s.left = node.left;
node = s;
}
}
// 4. 更新高度
updateHeight(node);
// 5. 检查是否失衡
return balance(node);
}
4. 查询
/**
* 根据key查询节点的value值
* @param key
* @return
*/
public Object get(int key) {
return doGet(root, key);
}
private Object doGet(AVLNode node, int key) {
if(node == null) {
return null;
}
if(key < node.key) {
return doGet(node.left, key);
} else if(node.key < key) {
return doGet(node.right, key);
} else {
return node.value;
}
}
5. 小结
AVL树的优点:
- AVL树是一种自平衡树,保证了树的高度平衡,从而保证了树的查询和插入操作的时间复杂度均为O(log n)
- 相比于一般二叉搜索树,AVL树对查询效率的提升更为显著,因为其左右子树高度的差值不会超过1,避免了二叉搜索树退化为链表的情况,使得整棵树的高度更低
- AVL树的删除操作比较简单,只需要像插入一样旋转即可,在旋转过程中树的平衡性可以得到维护
AVL树的缺点:
- AVL树每次插入和删除节点时可能需要进行旋转操作,这个操作比较耗时,因此在一些应用中不太适用
- 在AVL树进行插入或删除操作时,为保持树的平衡需要不断进行旋转操作,在一些高并发环节和大数据量环境下,这可能会导致多余的写锁导致性能瓶颈
- AVL树的旋转操作相对较多,因此在一些应用中可能会造成较大的空间浪费。