一、啥是平衡二叉树和 AVL 树
咱先来说说平衡二叉树。简单来讲,平衡二叉树就是一种特殊的二叉树,它的每个节点的左右子树高度差不会太大,这样能保证树的结构相对“匀称”,查找、插入和删除操作的效率就会比较高。
而 AVL 树就是平衡二叉树的一种具体实现,它得名于两位发明者的名字。在 AVL 树里,每个节点都有一个平衡因子,这个平衡因子就是该节点的左子树高度减去右子树高度的值。平衡因子只能是 -1、0 或者 1,要是超出这个范围,树就不平衡啦,这时候就得进行一些操作来让树重新平衡。
举个例子,假如有这么一个简单的 AVL 树:
// Java 代码示例
// 定义 AVL 树节点类
class AVLNode {
int key;
int height;
AVLNode left;
AVLNode right;
AVLNode(int key) {
this.key = key;
this.height = 1; // 初始化高度为 1
}
}
在这个例子里,AVLNode 类就是用来表示 AVL 树的节点,key 是节点的值,height 是节点的高度,left 和 right 分别是左右子节点。
二、旋转操作是咋回事
1. 左旋
左旋是一种让树重新平衡的操作。当树的右子树比左子树高太多的时候,就可能需要左旋。咱来具体看看左旋是怎么操作的。
假设我们有一个节点 A,它的右子节点是 B。左旋就是把 B 提上来作为新的根节点,A 变成 B 的左子节点,同时 B 原来的左子节点变成 A 的右子节点。
下面是 Java 代码实现:
// Java 代码示例:左旋操作
AVLNode leftRotate(AVLNode z) {
AVLNode y = z.right;
AVLNode T2 = y.left;
// 执行旋转
y.left = z;
z.right = T2;
// 更新高度
z.height = Math.max(height(z.left), height(z.right)) + 1;
y.height = Math.max(height(y.left), height(y.right)) + 1;
return y;
}
在这个代码里,z 就是要进行左旋的节点,先把 z 的右子节点 y 保存起来,再把 y 的左子节点 T2 保存起来。然后进行旋转操作,最后更新节点的高度,返回新的根节点 y。
2. 右旋
右旋和左旋正好相反,当树的左子树比右子树高太多的时候,就可能需要右旋。
假设我们有一个节点 A,它的左子节点是 B。右旋就是把 B 提上来作为新的根节点,A 变成 B 的右子节点,同时 B 原来的右子节点变成 A 的左子节点。
Java 代码实现如下:
// Java 代码示例:右旋操作
AVLNode rightRotate(AVLNode y) {
AVLNode x = y.left;
AVLNode T2 = x.right;
// 执行旋转
x.right = y;
y.left = T2;
// 更新高度
y.height = Math.max(height(y.left), height(y.right)) + 1;
x.height = Math.max(height(x.left), height(x.right)) + 1;
return x;
}
这里的 y 是要进行右旋的节点,和左旋类似,先保存相关节点,再进行旋转和高度更新。
3. 双旋
有时候,单纯的左旋或者右旋并不能让树重新平衡,这时候就需要双旋。双旋其实就是先进行一次左旋或者右旋,再进行一次相反方向的旋转。
双旋有两种情况,一种是先左旋再右旋(左 - 右双旋),另一种是先右旋再左旋(右 - 左双旋)。
先看左 - 右双旋的 Java 代码:
// Java 代码示例:左 - 右双旋
AVLNode leftRightRotate(AVLNode z) {
z.left = leftRotate(z.left);
return rightRotate(z);
}
这里先对 z 的左子节点进行左旋,再对 z 进行右旋。
再看右 - 左双旋的 Java 代码:
// Java 代码示例:右 - 左双旋
AVLNode rightLeftRotate(AVLNode z) {
z.right = rightRotate(z.right);
return leftRotate(z);
}
这里先对 z 的右子节点进行右旋,再对 z 进行左旋。
三、平衡因子的维护逻辑
在插入或者删除节点之后,AVL 树的平衡因子可能会发生变化,这时候就需要维护平衡因子,保证树的平衡。
下面是插入节点并维护平衡因子的 Java 代码:
// Java 代码示例:插入节点并维护平衡
AVLNode insert(AVLNode node, int key) {
// 插入节点,和普通二叉搜索树插入一样
if (node == null)
return (new AVLNode(key));
if (key < node.key)
node.left = insert(node.left, key);
else if (key > node.key)
node.right = insert(node.right, key);
else // 不允许重复键
return node;
// 更新节点的高度
node.height = 1 + Math.max(height(node.left), height(node.right));
// 获取平衡因子
int balance = getBalance(node);
// 左左情况
if (balance > 1 && key < node.left.key)
return rightRotate(node);
// 右右情况
if (balance < -1 && key > node.right.key)
return leftRotate(node);
// 左右情况
if (balance > 1 && key > node.left.key) {
node.left = leftRotate(node.left);
return rightRotate(node);
}
// 右左情况
if (balance < -1 && key < node.right.key) {
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}
在这个代码里,首先按照普通二叉搜索树的方式插入节点,然后更新节点的高度,计算平衡因子。根据平衡因子的情况,判断是哪种不平衡的情况,然后进行相应的旋转操作。
四、应用场景
AVL 树在很多地方都有应用,比如数据库的索引。在数据库中,经常需要进行查找、插入和删除操作,AVL 树的平衡特性可以保证这些操作的时间复杂度是 O(log n),效率比较高。
再比如文件系统的目录结构管理。文件系统需要快速地查找和定位文件,AVL 树可以帮助实现高效的文件查找。
五、技术优缺点
优点
- 高效的操作:由于 AVL 树是平衡的,所以查找、插入和删除操作的时间复杂度都是 O(log n),在数据量比较大的时候,效率比普通的二叉搜索树高很多。
- 结构稳定:AVL 树通过旋转操作和平衡因子的维护,能保证树的结构始终保持相对平衡,不会出现极端的情况。
缺点
- 实现复杂:AVL 树的实现需要考虑旋转操作和平衡因子的维护,代码比较复杂,开发和维护的成本相对较高。
- 插入和删除效率低:每次插入或者删除节点都可能需要进行旋转操作来维护平衡,这会增加操作的时间开销,尤其是在频繁插入和删除的场景下,性能可能不如一些其他的数据结构。
六、注意事项
- 在实现 AVL 树的时候,要注意旋转操作的顺序和平衡因子的计算,一旦出错,可能会导致树的结构混乱,影响操作的正确性。
- 在频繁插入和删除的场景下,要考虑 AVL 树的性能问题,可以根据具体情况选择其他更合适的数据结构。
七、文章总结
AVL 树是一种非常重要的平衡二叉树,它通过旋转操作(左旋、右旋和双旋)和平衡因子的维护,保证了树的平衡,从而提高了查找、插入和删除操作的效率。虽然 AVL 树有一些缺点,比如实现复杂、插入和删除效率低等,但在很多场景下,它仍然是一种非常有用的数据结构。在实际应用中,我们要根据具体的需求和场景,合理选择是否使用 AVL 树。
评论