算法与数据结构——AVL树(平衡二叉搜索树)
AVL树
在“二叉搜索树”章节提到,在多次插入和删除操作后,二叉搜索树可能退化为链表。在这种情况下,所有操作的时间复杂度将从O(logn)劣化为O(n)。
如下图,经过两次删除节点操作,这棵二叉搜索树便会退化为链表
再例如,下图所示的完美二叉树中插入两个节点后,树将严重向左倾斜,查找操作的时间复杂度也随之劣化。
1962 年 G. M. Adelson‑Velsky 和 E. M. Landis 在 论 文 “An algorithm for the organization of information”中提出了 AVL 树。AVL树能够确保在持续添加和删除节点后不会退化,从而使得各种操作的时间复杂度保持在O(logn)级别。
AVL树常见术语#
AVL树既是二叉搜索树,也是平衡二叉树,同时满足这两类二叉树的所有性质,因此是一种平衡二叉搜索树(balanced binary search tree)。
节点高度#
由于AVL树的相关操作需要获取节点高度,因此我们需要为节点类添加height
变量:
/*AVL 树节点类*/ struct TreeNode{ int val{}; int height = 0; TreeNode *left{}; TreeNode *right{}; TreeNode() = default; explicit TreeNode(int x) :val(x){} };
“节点高度”是指从该节点到它的最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为0,而空节点的高度为-1。我们将创建两个工具函数,分别用于获取和更新节点的高度:
/*获取节点高度*/ int AVLTree::height(TreeNode *node){ return node == nullptr ? -1 : node->height; } /*更新节点高度*/ void AVLTree::updateHeight(TreeNode *node){ // 节点高度等于最高子树高度 + 1 node->height = max(height(node->left), height(node->right)) + 1; }
节点平衡因子#
节点的平衡因子(balance factor)定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为0。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用:
/*获取平衡因子*/ int AVLTree::balanceFactor(TreeNode *node){ // 空节点 平衡因子为0 if (node == nullptr) return 0; // 节点平衡因子 = 左子树高度 - 右子树高度 return height(node->left) - height(node->right); }
设平衡因子为f,则一棵AVL树的任意节点的平衡因子都满足 -1 < = f <= 1。
AVL树旋转#
AVL树的特点在于“旋转”操作,它能够在不影响二叉树的中序遍历序列的前提下,使失衡节点重新回复平衡。换句话说,**旋转操作既能保持“二叉搜索树”的性质,也能使树重新变成“平衡二叉树”。
我们将平衡因子绝对值 > 1 的节点称为“失衡节点”。根据节点失衡的情况不同,旋转操作分为四种:右旋、左旋、先右旋后左旋、先左旋后右旋。
右旋#
如图所示,节点下方为平衡因子。从底往顶看,二叉树中首个失衡节点时“节点3”。我们关注以该失衡节点为根节点的子树,将该节点记为node
,其左子节点记为child
,执行“右旋操作”。完成右旋后,子树恢复平衡,并且仍然保持二叉搜索树的性质。
另外当child
节点有右子节点时(记为grand_child
),需要在右旋中添加一步:将grand_child
作为node
的左子结点。
“右旋”是一种形象化的说法,实际上需要通过修改节点指针来实现。
/*右旋操作*/ TreeNode* AVLTree::rightRotate(TreeNode *node){ TreeNode *child = node->left; TreeNode * grand_child = child->right; // 以child为原点,将 node 向右旋转 child->right = node; node->left = grand_child; // 更新节点高度 updateHeight(node); updateHeight(child); // 返回旋转后子树的根节点 return child; }
左旋#
相应地,如果考虑上述失衡二叉树的“镜像”,则需要执行下图所示的“左旋”操作。
同理,当节点child
有左子结点(记为grand_child
)时,需要在左旋中添加一步grand_child
作为node
的右子节点。
观察发现,左旋和右旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的。基于对称性,我们只需要将右旋代码啊的所有left
替换为right
,将所有的right
替换为left
,即可得到左旋的实现代码:
/*左旋操作*/ TreeNode *AVLTree::leftRotate(TreeNode *node){ TreeNode *child = node->right; TreeNode *grand_child = child->left; // 以child为原点 将 node 向左旋转 child->left = node; node->right = grand_child; // 更新节点高度 updateHeight(node); updateHeight(child); // 返回旋转后子树的根节点 return child; }
先左旋后右旋#
下图中的失衡节点3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先对child
执行“左旋”,再对node
执行“右旋”。
先右旋后左旋#
对于上述失衡二叉树的镜像情况,需要先对child
执行“右旋”,再对node
执行“左旋”操作。
旋转的选择#
下面展示了四种失衡情况与上述案例逐个对应,分别需要采用右旋、先左旋后右旋、先右旋后左旋、左旋的操作。
如下表所示,我们通过判断失衡节点的平衡因子以及较高一侧子节点的平衡因子的正负号,来确定失衡节点属于那种情况
失衡节点的平衡因子 | 子节点的平衡因子 | 应采用的旋转方法 |
---|---|---|
> 1 (左偏树) | ≥ 0 | 右旋 |
> 1 (左偏树) | < 0 | 先左旋后右旋 |
< -1 (右偏树) | ≤ 0 | 左旋 |
< -1 (右偏树) | > 0 | 先右旋后左旋 |
为便于使用,我们将旋转操作封装成一个函数。有了这个函数,我们就能对各种失衡情况进行旋转,使失衡节点重新恢复平衡。
/*执行旋转操作,使该子树重新恢复平衡*/ TreeNode *AVLTree::rotate(TreeNode *node){ // 获取节点 node 的平衡因子 int _balanceFactor = balanceFactor(node); // 左偏树 if (_balanceFactor > 1){ if (balanceFactor(node->left) < 0){ // 先左旋child再右旋node node->left = leftRotate(node->left); return rightRotate(node); } else{ // 直接右旋 return rightRotate(node); } } // 右偏树 if (_balanceFactor < -1){ if (balanceFactor(node->right) > 0){ // 先右旋child 再左旋node node->right = rightRotate(node->right); return leftRotate(node); } else { // 直接左旋 return leftRotate(node); } } // 平衡树,无需旋转,直接返回 return node; }
AVL树常用操作#
插入节点#
AVL树的节点插入操作与二叉搜索树在主体上类似。唯一的区别在于,在AVL树中插入节点后,从该节点到根节点的路径上可能会出现一系列失衡节点。因此,我们需要从这个节点开始,自底向上执行旋转操作,使其所有失衡节点恢复平衡。
/*递归插入节点(辅助方法)*/ TreeNode *AVLTree::insertHelper(TreeNode *node, int val){ if (node == nullptr){ return new TreeNode(val); } if (node->val < val){ node->right = insertHelper(node->right, val); } else if (node->val > val){ node->left = insertHelper(node->left, val); } else // 重复节点 直接返回 return node; updateHeight(node); // 更新节点高度 // 执行旋转操作,使该子树重新恢复平衡 node = rotate(node); // 返回子树根节点 return node; }
删除节点#
类似地,在二叉搜索树的删除节点方法的基础上,需要从底至顶执行旋转操作,使所有失衡节点恢复平衡。
/*删除节点*/ void AVLTree::remove(int val){ root = removeHelper(root, val); } /*递归删除节点(辅助方法)*/ TreeNode *AVLTree::removeHelper(TreeNode *node, int val){ if (node == nullptr){ return nullptr; } /*查找节点并删除*/ if (node->val < val){ node->right = removeHelper(node->right, val); } else if (node->val > val){ node->left = removeHelper(node->left, val); } else{ // 子节点数量为0 或 1 if (node->left == nullptr || node->right == nullptr){ TreeNode *child = node->left == nullptr ? node->right : node->left; delete node; node = child; } // 子节点数量为 2 else{ // 找到中序遍历的后一个节点(右子树的最左边元素) TreeNode *tmp = node->right; while (tmp->left != nullptr){ tmp = tmp->left; } int tmpVal = tmp->val; // 递归删除 (返回值是根节点)(删除右子树的最左边元素返回值就是右子树根节点) node->right = removeHelper(node->right, tmpVal); // 再覆盖值(相当于交换后删除) node->val = tmpVal; } } updateHeight(node); // 更新每次遇到的节点高度 // 执行旋转操作,使该子树重新恢复平衡 node = rotate(node); // 返回子树的根节点 return node; }
查找节点#
AVL树的节点查找操作与二叉搜索树一致。
AVL树典型应用#
- 组织和存储大型数据,适用于高频查找、低频增删的场景。
- 用于构建数据库中的索引系统。
- 红黑树也是一种常见的平衡二叉搜索树。相较于AVL树,红黑树的平衡条件更加宽松,插入与删除节点所需的旋转操作更少,节点增删操作的平均效率更高。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)