AVL树-带平衡条件的二叉查找树
一、前提理论
1. 相关概念
平衡二叉树,又称为 AVL 树。实际上就是遵循以下两个特点的二叉查找树:
- 每棵子树中的左子树和右子树的深度差不能超过 1;
- 二叉树中每棵子树都要求是平衡二叉树;
图1 平衡二叉树
AVL树的查找、插入、删除操作在平均和最坏的情况下都是O(logn),这得益于它时刻维护着二叉树的平衡。如果我们需要查找的集合本身没有顺序,在频繁查找的同时也经常的插入和删除,AVL树是不错的选择。
2. 平衡因子
平衡因子:将二叉树上节点的左子树高度减去右子树高度的值称为该节点的平衡因子BF(Balance Factor),表示的就是其左子树深度同右子树深度的差。平衡二叉树中各结点平衡因子的取值只可能是:0、1 和 -1。
3. 最小不平衡子树
距离插入节点最近的,且平衡因子的绝对值大于1的节点为根的子树。
二、平衡调整的步骤
1. 找平衡因子绝对值等于2的节点
2. 找插入新节点后失去平衡的最小不平衡子树(距离插入节点最近,平衡因子绝对值大于1的节点作为根)
3. 平衡调整
不平衡情况 | 调整方法 |
---|---|
LL型(左孩子的左子树插入节点) | R(单向右旋) |
RR型(右孩子的右子树插入节点) | L(单向左旋) |
LR型(左孩子的右子树插入节点) | LR(先左后右) |
RL型(右孩子的左子树插入节点) | RL(先右后左) |
三、不平衡的四种情况处理
把必须重新平衡的节点叫做α,由于任意节点最多有两个儿子,因此出现高度不平衡就需要α点的两棵子树的高度差2,则这种不平衡可能出现在下面4种情况中:
1. 对α的左儿子的左子树进行一次插入(LL型)–单向右旋
图2 LL型–单向右旋
若由于结点 a 的左子树为根结点的左子树上插入结点,导致结点 a 的平衡因子由 1 增至 2,致使以 a 为根结点的子树失去平衡,则只需进行一次向右的顺时针旋转,以B节点为中间支点,高的节点A右转(顺时针旋转)。
2.对α的右儿子的右子树进行一次插入(RR型)–单向左旋
图3 RR型–单向左旋
如果由于结点 a 的右子树为根结点的右子树上插入结点,导致结点 a 的平衡因子由 -1变为 -2,则以 a 为根结点的子树需要进行一次向左的逆时针旋转,以B节点为中间支点,高的节点A左转(逆时针旋转)。
3.对α的左儿子的右子树进行一次插入(LR型)–先左后右
图4 LR型–先左后右
如果由于结点 a 的左子树为根结点的右子树上插入结点,导致结点 a 平衡因子由 1 增至 2,致使以 a 为根结点的子树失去平衡,则需要进行两次旋转操作,第一次以C为中间支点将B左旋,第二次以C为中间支点将A右旋。
4.对α的右儿子的左子树进行一次插入(RL型)–先右后左
图5 RL型–先右后左
如果由于结点 a 的右子树为根结点的左子树上插入结点,导致结点 a 平衡因子由 -1 变为 -2,致使以 a 为根结点的子树失去平衡,则需要进行两次旋转(先右旋后左旋)操作,第一次以C为中间支点B右旋,第二次以C为中间支点A左旋。
四、代码实现
以下代码实现了AVL树四种平衡情况的处理,其余方法与二叉查找树的方法一样。
template<typename Comparable> class AvlTree { struct AvlNode { Comparable element; AvlNode* left; AvlNode* right; int height; AvlNode(const Comparable &ele,AvlNode*lt,AvlNode*rt,int h=0) :element{ele},left{lt},right{rt},height{h}{} AvlNode( Comparable&& ele, AvlNode* lt, AvlNode* rt, int h = 0) :element{ std::move(ele) }, left{ lt }, right{ rt }, height{ h }{} }; //返回节点t的高度,如果是nullptr则返回-1 int height(AvlNode* t)const { return t == nullptr ? -1 : t->height; } /** * 向一棵子树进行插入的内部方法 * x是要插入的项 * t为该子树的根节点 * 设置子树的新根 */ void insert(const Comparable& x, AvlNode*& t) { if (t == nullptr) t = new AvlNode{ x,nullptr,nullptr }; else if (x < t->element) insert(x, t->left); else if (x > t->element) insert(x, t->right); balance(t); } static const int ALLOWED_IMBALANCE = 1; //假设t是平衡的,或与平衡相差不超过1 void balance(AvlNode*& t) { if (t == nullptr) return; if (height(t->left) - height(t->right) > ALLOWED_IMBALANCE) if (height(t->left->left) >= height(t->left->right)) rotateWithLeftChild(t); else doubleWithLeftChild(t); else if (height(t->right) - height(t->left) > ALLOWED_IMBALANCE) if (height(t->right->right) >= height(t->right->left)) rotateWithLeftChild(t); else doubleWithLeftChild(t); t->height = max(height(t->left), height(t->right))+1; } /** * 用左儿子旋转二叉树的节点 * 这是对AVL树在情形1的一次单旋转 * 更新高度,然后设置新根 */ void rotateWithLeftChild(AvlNode*& k2) { AvlNode* k1 = k2->left; k2->left = k1->right; k1->right = k2; k2->height = max(height(k2->left), height(k2->right)) + 1; k1->height = max(height(k1->left), k2->hegiht)+1; k2 = k1; } /** * 双旋转二叉树的节点:首先左儿子和它的右儿子进行 * 然后节点k3和新的左儿子进行 * 这是对AVL树情形3的一次双旋转 * 更新高度,然后设置新根 */ void doubleWithLeftChild(AvlNode*& k3) { rotateWithRightChild(k3->left); rotateWithLeftChild(k3); } /** * 从子树实施删除的内部方法 * x是要被删除的项 * t为该子树的根节点 * 设置该子树的新根 */ void remove(const Comparable& x, AvlNode*& t) { if (t == nullptr) return; //没发现要删除的项,什么也不做 if (x < t->element) remove(x, t->left); else if (x > t->element) remove(x, t->right); else if (t->left != nullptr && t->right != nullptr) //有两个儿子 { t->element = findMin(t->right)->element; remove(t->element, t->right); } else { AvlNode* oldNode = t; t = (t->left != nullptr) ? t->left : t->right; delete oldNode; } balance(t); } };
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 易语言 —— 开山篇