🏆 作者简介,愚公搬代码 🏆《头衔》:华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,阿里云专家博主,腾讯云优秀博主,掘金优秀博主,51CTO博客专家等。 🏆《近期荣誉》:2022年CSDN博客之星TOP2,2022年华为云十佳博主等。
🏆《博客内容》:.NET、Java、Python、Go、Node、前端、IOS、Android、鸿蒙、Linux、物联网、网络安全、大数据、人工智能、U3D游戏、小程序等相关领域知识。
🏆🎉欢迎 👍点赞✍评论⭐收藏
数据结构是计算机科学中的一个重要概念,它描述了数据之间的组织方式和关系,以及对这些数据的访问和操作。常见的数据结构有:数组、链表、栈、队列、哈希表、树、堆和图。
AVL树的基本思想是通过对树的平衡因子进行调整,使得树保持平衡。平衡因子指的是左右子树的高度差,AVL树要求任何一个节点的左右子树高度差不能超过1。如果一个节点的平衡因子超过1,就需要通过旋转操作来调整树的结构,使之重新达到平衡。
AVL树的插入和删除操作都会引起树的不平衡,需要通过旋转操作来重新平衡。当节点插入到AVL树中时,需要从插入节点的父节点开始,一直到根节点,检查每个节点的平衡因子是否超过1,如果有,则需要旋转该节点,直到根节点。删除操作同理。由于AVL树保持平衡,因此任何操作的时间复杂度都是O(log n)。
AVL树的节点高度
是该节点到其最远叶子节点的路径长度,即从该节点往下到最底层节点的路径长度。对于空节点,其高度为-1。
节点的平衡因子
是其左子树的高度减去右子树的高度,因此平衡因子只可能是-1、0或1。如果一个节点的平衡因子的绝对值超过1,则该节点需要进行旋转操作,以保持AVL树的平衡性。
AVL树右旋的具体步骤如下:
右旋的示意图如下:
p p
| |
x y
/ \ right rotate / \
y C ------------> A x
/ \ \
A B B
其中,p为需要右旋的节点的父节点,x为需要右旋的节点,y为x的左子节点。
AVL树左旋是一种平衡操作,其步骤如下:
AVL树左旋后右旋的实现步骤如下:
左旋后,根据平衡因子,我们可以得出旋转后各个节点的平衡因子以及新的根节点。然后,我们需要按照步骤进行右旋操作。右旋操作的实现步骤与左旋操作类似,只是方向相反。
先来了解一下什么是AVL树:AVL树是一种自平衡二叉搜索树,它的每个节点的左子树和右子树的高度差至多为1,这就保证了它的查找、插入和删除操作的时间复杂度都是O(log n)。
当AVL树的某个节点的左右子树高度差超过1时,就需要进行旋转操作来保持平衡,而先右旋后左旋就是一种旋转操作。
具体步骤如下:
3.1 将不平衡节点的左子节点设为新的根节点。
3.2 将新的根节点的右子树设为原不平衡节点的左子树。
3.3 将原不平衡节点设为新的根节点的右子树。
4.1 将不平衡节点的右子节点设为新的根节点。
4.2 将新的根节点的左子树设为原不平衡节点的右子树。
4.3 将原不平衡节点设为新的根节点的左子树。
需要注意的是,先右旋后左旋并不一定是所有情况下都适用的,而是取决于不平衡节点的左右子树的高度差和节点的左右子节点的高度差。因此,在实际应用中,需要根据具体情况来选择合适的旋转策略。
AVL树的旋转分为左旋和右旋,旋转的目的是为了让AVL树保持平衡。
选择左旋还是右旋,需要根据树的实际情况来决定。如果某个节点的左子树比右子树高,则选择右旋;如果某个节点的右子树比左子树高,则选择左旋。
具体选择方法如下:
需要注意的是,某些情况可能需要进行双旋转,即先左旋再右旋或先右旋再左旋,才能达到平衡。
/**
* File: avl_tree.cs
* Created Time: 2022-12-23
* Author: haptear (haptear@hotmail.com)
*/
namespace hello_algo.chapter_tree;
/* AVL 树 */
class AVLTree {
public TreeNode? root; // 根节点
/* 获取节点高度 */
public int height(TreeNode? node) {
// 空节点高度为 -1 ,叶节点高度为 0
return node == null ? -1 : node.height;
}
/* 更新节点高度 */
private void updateHeight(TreeNode node) {
// 节点高度等于最高子树高度 + 1
node.height = Math.Max(height(node.left), height(node.right)) + 1;
}
/* 获取平衡因子 */
public int balanceFactor(TreeNode? node) {
// 空节点平衡因子为 0
if (node == null) return 0;
// 节点平衡因子 = 左子树高度 - 右子树高度
return height(node.left) - height(node.right);
}
/* 右旋操作 */
TreeNode? rightRotate(TreeNode? node) {
TreeNode? child = node.left;
TreeNode? grandChild = child?.right;
// 以 child 为原点,将 node 向右旋转
child.right = node;
node.left = grandChild;
// 更新节点高度
updateHeight(node);
updateHeight(child);
// 返回旋转后子树的根节点
return child;
}
/* 左旋操作 */
TreeNode? leftRotate(TreeNode? node) {
TreeNode? child = node.right;
TreeNode? grandChild = child?.left;
// 以 child 为原点,将 node 向左旋转
child.left = node;
node.right = grandChild;
// 更新节点高度
updateHeight(node);
updateHeight(child);
// 返回旋转后子树的根节点
return child;
}
/* 执行旋转操作,使该子树重新恢复平衡 */
TreeNode? rotate(TreeNode? node) {
// 获取节点 node 的平衡因子
int balanceFactorInt = balanceFactor(node);
// 左偏树
if (balanceFactorInt > 1) {
if (balanceFactor(node.left) >= 0) {
// 右旋
return rightRotate(node);
} else {
// 先左旋后右旋
node.left = leftRotate(node?.left);
return rightRotate(node);
}
}
// 右偏树
if (balanceFactorInt < -1) {
if (balanceFactor(node.right) <= 0) {
// 左旋
return leftRotate(node);
} else {
// 先右旋后左旋
node.right = rightRotate(node?.right);
return leftRotate(node);
}
}
// 平衡树,无须旋转,直接返回
return node;
}
/* 插入节点 */
public void insert(int val) {
root = insertHelper(root, val);
}
/* 递归插入节点(辅助方法) */
private TreeNode? insertHelper(TreeNode? node, int val) {
if (node == null) return new TreeNode(val);
/* 1. 查找插入位置,并插入节点 */
if (val < node.val)
node.left = insertHelper(node.left, val);
else if (val > node.val)
node.right = insertHelper(node.right, val);
else
return node; // 重复节点不插入,直接返回
updateHeight(node); // 更新节点高度
/* 2. 执行旋转操作,使该子树重新恢复平衡 */
node = rotate(node);
// 返回子树的根节点
return node;
}
/* 删除节点 */
public void remove(int val) {
root = removeHelper(root, val);
}
/* 递归删除节点(辅助方法) */
private TreeNode? removeHelper(TreeNode? node, int val) {
if (node == null) return null;
/* 1. 查找节点,并删除之 */
if (val < node.val)
node.left = removeHelper(node.left, val);
else if (val > node.val)
node.right = removeHelper(node.right, val);
else {
if (node.left == null || node.right == null) {
TreeNode? child = node.left != null ? node.left : node.right;
// 子节点数量 = 0 ,直接删除 node 并返回
if (child == null)
return null;
// 子节点数量 = 1 ,直接删除 node
else
node = child;
} else {
// 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点
TreeNode? temp = node.right;
while (temp.left != null) {
temp = temp.left;
}
node.right = removeHelper(node.right, temp.val);
node.val = temp.val;
}
}
updateHeight(node); // 更新节点高度
/* 2. 执行旋转操作,使该子树重新恢复平衡 */
node = rotate(node);
// 返回子树的根节点
return node;
}
/* 查找节点 */
public TreeNode? search(int val) {
TreeNode? cur = root;
// 循环查找,越过叶节点后跳出
while (cur != null) {
// 目标节点在 cur 的右子树中
if (cur.val < val)
cur = cur.right;
// 目标节点在 cur 的左子树中
else if (cur.val > val)
cur = cur.left;
// 找到目标节点,跳出循环
else
break;
}
// 返回目标节点
return cur;
}
}
public class avl_tree {
static void testInsert(AVLTree tree, int val) {
tree.insert(val);
Console.WriteLine("\n插入节点 " + val + " 后,AVL 树为");
PrintUtil.PrintTree(tree.root);
}
static void testRemove(AVLTree tree, int val) {
tree.remove(val);
Console.WriteLine("\n删除节点 " + val + " 后,AVL 树为");
PrintUtil.PrintTree(tree.root);
}
[Test]
public void Test() {
/* 初始化空 AVL 树 */
AVLTree avlTree = new AVLTree();
/* 插入节点 */
// 请关注插入节点后,AVL 树是如何保持平衡的
testInsert(avlTree, 1);
testInsert(avlTree, 2);
testInsert(avlTree, 3);
testInsert(avlTree, 4);
testInsert(avlTree, 5);
testInsert(avlTree, 8);
testInsert(avlTree, 7);
testInsert(avlTree, 9);
testInsert(avlTree, 10);
testInsert(avlTree, 6);
/* 插入重复节点 */
testInsert(avlTree, 7);
/* 删除节点 */
// 请关注删除节点后,AVL 树是如何保持平衡的
testRemove(avlTree, 8); // 删除度为 0 的节点
testRemove(avlTree, 5); // 删除度为 1 的节点
testRemove(avlTree, 4); // 删除度为 2 的节点
/* 查询节点 */
TreeNode? node = avlTree.search(7);
Console.WriteLine("\n查找到的节点对象为 " + node + ",节点值 = " + node?.val);
}
}
优点:
缺点:
AVL树可以应用于需要高效的数据插入和查询的场景。例如,在数据库中,AVL树常常被用来存储索引数据,以便快速地查找和访问表中的数据;在编译器中,AVL树通常被用来实现符号表,以便快速地查找和访问变量和函数等标识符信息;在路由算法中,AVL树常常被用来维护路由表,以便快速地查找和访问路由信息。总之,需要高效地进行数据插入和查询的场景都可以考虑使用AVL树。