【进阶数据结构】平衡搜索二叉树 —— AVL树
创始人
2025-05-31 22:09:32
0

🌈感谢阅读East-sunrise学习分享——[进阶数据结构]AVL树
博主水平有限,如有差错,欢迎斧正🙏感谢有你 码字不易,若有收获,期待你的点赞关注💙我们一起进步🚀


🌈我们上一篇博客分享了搜索二叉树,在文中也铺垫了搜索二叉树的一些结构局限性
而今天分享的一种特殊的搜索二叉树——AVL树,便是一种结构优异的搜索二叉树🎄那么我们就开始吧🚀🚀🚀

目录

  • 一、AVL树的概念
  • 二、AVL树结点的定义
  • 三、AVL树的插入
  • 四、AVL树的旋转
    • 1.左单旋
    • 2.右单旋
    • 3.左右双旋
    • 4.右左双旋
  • 五、最终代码展示

一、AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下

因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度

🎄一棵AVL树可以是一棵空树,或者是一棵具有以下性质的二叉搜索树

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hDG6AbRQ-1679390790392)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320143958347.png)]

这里的平衡因子是指:右子树高度-左子树高度

注意:平衡因子只是博主分享的这种实现方法的一种自定义名字(不是必须的),除了使用平衡因子之外还有许多实现AVL树的方法

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(logN),搜索时间复杂度O(logN)


二、AVL树结点的定义

AVL树的结点我们定义了一个三叉链结构,便于后续的操作;并且在每个结点中都引入了平衡因子

template
struct AVLTreeNode
{//存储键值对的pair类pair _kv;//含有父节点的三叉链AVLTreeNode* _left;AVLTreeNode* _right;AVLTreeNode* _parent;//平衡因子int _bf;AVLTreeNode(const pair& kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}
};//AVL树
template
struct AVLTree
{typedef AVLTreeNode Node;
public://插入bool Insert(const pair& kv){}private:Node* _root = nullptr;
};

三、AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整平衡因子,若不平衡则需要旋转调整AVL树

⭕⭕当有新节点插入后我们就需要判断此时的树是否仍然平衡仍然是AVL树了


🚩插入后平衡因子的变化类型?

我们知道,假如平衡,则每个结点的平衡因子只有三种可能:-1,0,1

而插入新结点肯定会使得高度的变化,假如插入新节点后仍平衡,则父节点的平衡因子的变化有:

  • 0 --> 1
  • 0 --> -1
  • 1 --> 0
  • -1 --> 0

知道了平衡因子的变化情况后,又抛出了一个问题

🚩插入新节点影响父节点的平衡因子,那是否会影响祖先结点的平衡因子?

最简单的情况就是插入了新节点,只影响了其父结点,只需更新父节点的平衡因子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yGlce0wT-1679390790393)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320151927976.png)]

插入新节点后,改变了其父结点(8)的子树高度,所以需要更新父节点的平衡因子,但是插入之后并不会改变其祖先结点的子树高度,所以不需要往上更新平衡因子

📌因此我们可以总结出:是否持续更新平衡因子,取决于其结点的子树高度是否变化

再结合一开始的平衡因子变化情况我们可以得出插入新结点后:

  • parent -> _bf == 0 —— 说明之前parent -> _bf 是 1 或者 -1(一边高一边低)新节点刚好插入填上矮的那边,parent所在子树高度不变 —— 祖先的子树高度也不会变 —— 只需更新parent的平衡因子,不需要继续往上更新
  • parent -> _bf == 1 或 -1 —— 说明之前parent -> _bf == 0(两边一样高)新结点插入使得parent所在子树的高度变得一高一低 —— 祖先的子树高度也产生变化 —— 更新parent的平衡因子之外,还需要继续往上更新祖先结点的平衡因子
  • parent -> _bf == 2 或 -2 —— 说明本就一高一低的子树,插入新节点后造成更加不平衡,此时违反了AVL树的平衡规则 —— 就地处理 ——旋转调整

最坏的情况就是插入了新节点,直接影响到了root根结点,所以需要持续更新到root根结点的平衡因子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-inVjVnIr-1679390790394)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320154047198.png)]

💭更新结点的平衡因子时,假若我们需要持续向上更新平衡因子,一开始我们更新的是最下面的parent结点,更新后则可向上迭代,直到parent为空就停止

✏️代码实现

bool Insert(const pair& kv){//空if (_root == nullptr){_root = new Node(kv);return true;}//非空Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}elsereturn false;}//插入cur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}//调整平衡因子while (parent){if (cur == parent->_right)parent->_bf++;elseparent->_bf--;if (parent->_bf == 0)break;else if (parent->_bf == -1 || parent->_bf == 1){cur = parent;parent = parent->_parent;}else if (parent->_bf == -2 || parent->_bf == 2){//旋转调整}elseassert(false);}return true;}

四、AVL树的旋转

🌏如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。

因此旋转的要求即是:

  1. 旋转后仍保持二叉搜索树的结构
  2. 旋转后整棵树保持平衡,平衡因子不超过1

而根据节点插入位置的不同,AVL树的旋转分为四种:

1.左单旋

1️⃣新节点插入较高右子树的右侧 —— 左单旋

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ewTMX0mf-1679390790394)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320191813189.png)]

此处我们给出左单旋过程的抽象图📌

我们发现,当parent的平衡因子是2,cur是1时,便进行左单旋 ——> 将cur的左子树给parent的右子树,然后将parent及其子树一整棵树变为cur的左子树

左单旋真就如此吗?不信我们可以画出具象图看看

当 h = 0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JOtMtrcS-1679390790394)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320192229670.png)]

当 h = 1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TNDYGHPL-1679390790394)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320192802162.png)]

当 h = 2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o56wtp0U-1679390790395)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320193527342.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ndiplxh7-1679390790395)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320194200461.png)]

💭有的兄弟看到这就有疑问,为什么h = 2时,子树c一定就得是z的模样呢?
因为假如子树c是x或y的模样,插入新节点时并不会引发节点30的旋转,那样最多只是变成以节点60为parent的树进行左单旋,那就和h = 1是同样的情况了💤因此以上的情况,其实是笼盖了所有需要进行左单旋的子情况了🚩然后以上的情况可能是某棵树的子树

最后我们发现,所有需要进行左单旋的情况,最后的操作都是如一开始所说

✏️代码实现(对照图更清晰易懂)

void RotateL(Node* parent)
{Node* subR = parent->_right;//parent的右孩子Node* subRL = subR->_left;//parent的右孩子的左孩子//旋转后subR的左孩子作为parent的右孩子parent->_right = subRL;//subR的左孩子有可能为空也有可能存在//如果存在则需要更新父子关系if (subRL)subRL->_parent = parent;//subR的左孩子变为以parent为根的子树结构//同时更新父子关系subR->_left = parent;parent->_parent = subR;//parent也可能只是一棵子树的根,其pparent可能为空也可能存在Node* pparent = parent->_parent;if (pparent){//如果pparent不为空,则说明parent是一棵子树//可能是存在于其父节点的左子树or右子树if (parent == pparent->_left)pparent->_left = subR;elsepparent->_right = subR;subR->_parent = pparent;}else{//若pparent为空,则说明parent是整棵树的根节点//旋转后根节点已经换人了需要更新_root = subR;subR->_parent = nullptr;}//最后更新平衡因子parent->_bf = subR->_bf = 0;
}

📌看完以上的代码实现,发现旋转的代码实现起来也有许多细节需要注意啊…

因为旋转后也要保持一棵正常的树的结构,因此那些父子链接关系也需要正确更新

2.右单旋

2️⃣新节点插入较高左子树的左侧 - 右单旋

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0rczBlb6-1679390790395)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320224253056.png)]

✏️实现及情况考虑可参考左单旋

void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;Node* pparent = parent->_parent;subL->_right = parent;parent->_parent = subL;if (pparent){if (parent == pparent->_left)pparent->_left = subL;elsepparent->_right = subL;subL->_parent = pparent;}else{_root = subL;subL->_parent = nullptr;}subL->_bf = parent->_bf = 0;
}

3.左右双旋

3️⃣新节点插入较高左子树的右侧 - 先左单旋再右单旋

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Mn250NT-1679390790395)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320225421511.png)]

左右双旋我们可以复用上面的左单旋和右单旋的代码🚩但是需要注意的是,左右双旋完各个节点的平衡因子有不同的情况,正是因为左右双旋会因为新节点插入的位置不同而影响不同的旋转结果,因此我们总结出了以下三种情况:

  1. h = 0 —— 节点60即是新插入节点
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fkFAFXcU-1679390790396)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320230931222.png)]

  2. 新节点插入在b
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1pptemfj-1679390790396)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320231609130.png)]

  3. 新节点插入在c
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JGZNUPir-1679390790396)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320231628170.png)]

综上所述,当我们在实现左右双旋时的最后,可根据插入新节点后节点60的平衡因子大小,来确定不同的情况

void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);//更新平衡因子if (bf == 1) //新增在sublr右子树{parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (bf == -1) //新增在sublr左子树{subL->_bf = 0;parent->_bf = 1;subLR->_bf = 0;}else //本身就是新增{parent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}
}

4.右左双旋

4️⃣新节点插入较高右子树的左侧——先右单旋再左单旋

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RGitbiDD-1679390790396)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230320232205457.png)]

✏️实现及情况考虑可参考左右双旋

void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 1){subR->_bf = 0;parent->_bf = -1;subRL->_bf = 0;}else if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else if(bf == 0){parent->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}
}

五、最终代码展示

template
struct AVLTreeNode
{pair _kv;AVLTreeNode* _left;AVLTreeNode* _right;AVLTreeNode* _parent;int _bf;AVLTreeNode(const pair& kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}
};template
struct AVLTree
{typedef AVLTreeNode Node;
public:AVLTree():_root(nullptr){}bool Insert(const pair& kv){//空if (_root == nullptr){_root = new Node(kv);return true;}//非空Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}elsereturn false;}//插入cur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}//调整平衡因子while (parent){if (cur == parent->_right)parent->_bf++;elseparent->_bf--;if (parent->_bf == 0)break;else if (parent->_bf == -1 || parent->_bf == 1){cur = parent;parent = parent->_parent;}else if (parent->_bf == -2 || parent->_bf == 2){//旋转调整if (parent->_bf == 2 && cur->_bf == 1)RotateL(parent);else if (parent->_bf == -2 && cur->_bf == -1)RotateR(parent);else if (parent->_bf == -2 && cur->_bf == 1)RotateLR(parent);else if (parent->_bf == 2 && cur->_bf == -1)RotateRL(parent);elseassert(false);break;}elseassert(false);}return true;}void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;Node* pparent = parent->_parent;subR->_left = parent;parent->_parent = subR;if (pparent){if (parent == pparent->_left)pparent->_left = subR;elsepparent->_right = subR;subR->_parent = pparent;}else{_root = subR;subR->_parent = nullptr;}parent->_bf = subR->_bf = 0;}void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;Node* pparent = parent->_parent;subL->_right = parent;parent->_parent = subL;if (pparent){if (parent == pparent->_left)pparent->_left = subL;elsepparent->_right = subL;subL->_parent = pparent;}else{_root = subL;subL->_parent = nullptr;}subL->_bf = parent->_bf = 0;}void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);if (bf == 1) //新增在sublr右子树{parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (bf == -1) //新增在sublr左子树{subL->_bf = 0;parent->_bf = 1;subLR->_bf = 0;}else if (bf == 0) //本身就是新增{parent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else{assert(false);}}void RotateRL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 1){subR->_bf = 0;parent->_bf = -1;subRL->_bf = 0;}else if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else if(bf == 0){parent->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}else{assert(false);}}void Inorder(){_Inorder(_root);}void _Inorder(Node* root){if (root == nullptr)return;_Inorder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_Inorder(root->_right);}int Height(Node* root){if (root == nullptr)return 0;int hl = Height(root->_left);int hr = Height(root->_right);return hl > hr ? hl + 1 : hr + 1;}bool IsBalance(){return IsBalance(_root);}bool IsBalance(Node* root){if (root == nullptr)return true;int leftHeight = Height(root->_left);int rightHeight = Height(root->_right);if (rightHeight - leftHeight != root->_bf){cout << "平衡因子异常" << endl;return false;}return abs(rightHeight - leftHeight) < 2&& IsBalance(root->_left)&& IsBalance(root->_right);}private:Node* _root = nullptr;
};

🌈🌈写在最后 我们今天的学习分享之旅就到此结束了
🎈感谢能耐心地阅读到此
🎈码字不易,感谢三连
🎈关注博主,我们一起学习、一起进步

相关内容

热门资讯

走进小城看消费丨江西资溪:低碳...   夏日时节下午4点,江西省抚州市资溪县大觉山景区漂流终点依然热闹。来自南昌的游客余鑫漂流结束后没有...
【中原晨会0625】市场分析专... 来源:市场资讯 (来源:中原证券研究所) 本期重点研报目录 【中原策略】市场分析:电子半导体领涨 ...
南向资金连买4日!低费率+可月... 6月25日早盘,港股红利资产震荡整理。截至11时14分,港股红利低波ETF招商(520550)下跌0...
618成交破百万!紫荆花用一套... 一年一度的618年中大促,是消费市场的晴雨表,也是品牌间最激烈的角力场。当各大品牌在直播间里铆足了劲...
原创 黄... 2026年6月25日的国际金价已经从前期的5500美元高点跌到4200美元下方,累计跌幅超过22%,...
英伟达CEO:Vera Rub... 截至9:38,中证半导体材料设备主题指数(931743)涨2.36%创新高;权重股中,中微公司涨3....
再被催债16亿!“钢铁大王”戴... 澎湃新闻记者 贺梨萍 因“铁本事件”入狱五年的戴国芳重返钢铁行业,但他并没有完成从阶下囚再到“钢铁大...
周三原油价格下跌 随着美国和伊朗在和平谈判中取得进展,越来越多的油轮公开穿越霍尔木兹海峡,原油在战时的价格上涨已经蒸发...
这种蛋白是大脑衰老的开关 这种蛋白是大脑衰老的开关 清晨,假设一位五十岁左右的王女士发现自己常常把手机放在熟悉的抽屉里又找不到...
信通院牵头算力Token出海生... 盘面上,截至11:04,中证科创创业50指数(931643)涨1.68%,创历史新高;权重股中,芯原...
海外 774 亿营收背后:日本... 文 | 游戏价值论 6月23日,彭博社报道了腾讯正在围绕出售多家日本游戏工作室少数股权开展谈判,包...
餐饮“抢人”大战:把店开到公交... 作者 |餐饮老板内参 内参君 医院、公交站、演唱会…餐饮品牌,正在无孔不入 在北京儿童医院,肯德基...
快讯 | 外资扫货!陈翊庭:港... 港交所行政总裁陈翊庭在接受《中国证券报》专访时指出,国际资本对中国资产的看法已彻底扭转,布局中国市场...
2777.77元!A股“股王”... 25日早盘,昨天创下历史新高的A股“股王”联讯仪器,今天上午继续走强,盘中股价再度刷新历史新高。 截...
原创 今... 欧洲自己的媒体直接下结论,欧盟衰退躲不掉,内部分裂拦不住,现在就连欧洲顶尖工业巨头,都偷偷在用中国的...
黄仁勋股东大会放言:本轮AI基... 在当地时间6月24日的英伟达(NVDA.O)2026年度股东大会上,股东批准了该公司全部10名董事会...
国际油价大跌 新华社消息, 纽约原油期货主力合约价格24日盘中跌破每桶70美元,为伊朗战事爆发以来首次。 市场分析...
马云带队插秧,什么信号? 一场别开生面的“务农”,让外界看到了一个不一样的阿里巴巴。 近日,阿里巴巴合伙人、高德董事长刘振飞在...
全球最大产能,最高丰度达99.... 本文转自【科技日报】; 6月23日,高丰度硼-10同位素技术暨产业化成果发布会在山东省东营市举办,全...
黄金大跳水!金饰克价年内暴跌近... 25日,现货黄金盘中震荡,截至发稿,报3985.070美元/盎司,跌0.17%。 当地时间24日,...