【C++】平衡二叉搜索(AVL)树的模拟实现
创始人
2024-05-28 23:33:13
0

一、 AVL树的概念

map、multimap、set、multiset 在其文档介绍中可以发现,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N)O(N)O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
在这里插入图片描述

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

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

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

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

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

在这里插入图片描述

二、AVL树节点的定义

AVL树的节点是三叉链结构:即parent、left和right,它们分别指向当前节点的父节点、左子节点和右子节点。通过这种方式,可以在O(1)O(1)O(1)的时间内找到一个节点的父节点、左子节点和右子节点。

在这里插入图片描述

namespace AVL
{templatestruct AVLTreeNode {AVLTreeNode* _left;AVLTreeNode* _right;AVLTreeNode* _parent; //指向父节点的指针pair _kv;int _bf; // 平衡因子AVLTreeNode(const pair& kv) :_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv),_bf(0){}};templateclass AVLTree{typedef AVLTreeNode Node;public:private:Node* _root = nullptr;};
}

三、AVL树的插入

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

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子

插入在左平衡因子-1,插入在右平衡因子+1

是否继续更新的依据:parent所在子树的高度是否变化

  1. parent->_bf == 0说明之前parent->_bf1 或者 -1 说明之前parent一边高一边低,这次插入填上矮的那边,parent所在子树高度不变,不需要继续往上更新
    在这里插入图片描述

  2. parent->_bf == 1-1说明之前是parent->_bf = 0,两边一样高,现在插入一边更高了,parent所在子树高度变了,继续往上更新
    在这里插入图片描述

  3. parent->_bf == 2-2,说明之前parent->_bf == 1或者-1,现在插入严重不平衡,违反规则,就地处理–旋转

bool insert(const pair& kv)
{// 1. 先按照二叉搜索树的规则将节点插入到AVL树中// 空树直接构建根if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (kv.first > cur->_kv.first) // 大了往右边走{parent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first) // 小了往左边走{parent = cur;cur = cur->_left;}else{return false;// 相等不插入}}//开始插入cur = new Node(kv);// 新插入的节点// 小的插入左,大的插入右if (kv.first < parent->_kv.first){parent->_left = cur;cur->_parent = parent;// 三叉链,不要忘记更新父指针}else{parent->_right = cur;cur->_parent = parent;}// 2. 新节点插入后,AVL树的平衡性可能会遭到破坏// 此时需要更新平衡因子,并检测是否破坏了AVL树的平衡性while (parent) // parent为空,也就更新到根停止{// 更新平衡因子// 新增在左,parent->bf--;// 新增在右,parent->bf++;if (cur == parent->_left){parent->_bf--;}else{parent->_bf++;}//检测平衡因子if (parent->_bf == 0){break;// 无需继续更新}else if (parent->_bf == 1 || parent->_bf == -1){// 插入前parent的平衡因子是0,插入后parent的平衡因为为1 或者 - 1 ,说明以parent为根的二叉树// 的高度增加了一层,因此需要继续向上调整cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){// parent的平衡因子为-2/2,违反了AVL树的平衡性// 需要对以 parent 为根的树进行 旋转 处理// 旋转break; // 旋转完成后,原 parent 为根的子树高度降低,已经平衡,不需要再向上更新}else{assert(false); // 平衡因子异常:绝对值大于2}}return true;
}

四、AVL树的旋转

在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时可通过旋转调整树的结构,使之平衡化。

旋转的目的:

  • 让这颗子树左右高度不超过1
  • 旋转过程中继续保持是搜索树
  • 更新调整孩子节点的平衡因子
  • 让这颗子树的高度跟插入前保持一致

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

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

在这里插入图片描述

在插入前,图中AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子,图中a/b/c是高度为 h 的AVL子树)中,30左子树增加了一层,导致以60为根的二叉树不平衡

要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点的平衡因子即可。

在旋转过程中,有以下几种情况需要考虑:

  1. 30节点的右孩子可能存在,也可能不存在
  2. 60可能是根节点,也可能是子树,如果是根节点,旋转完成后,要更新根节点,如果是子树,可能是某个节点的左子树,也可能是右子树

这里举一些详细的例子进行画图,考虑各种情况,加深旋转的理解

h == 0,则a/b/c是空树:

在这里插入图片描述

h == 1:

在这里插入图片描述

h == 2的情况已经有很多种了,随着h的增加情况会越来越复杂

在这里插入图片描述

看图写代码:

在这里插入图片描述


void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;// 30的右变成60的左parent->_left = subLR;if (subLR != nullptr) // 30的右不为空,更新_parent指针{subLR->_parent = parent;}Node* ppNode = parent->_parent;// 60变成30的右parent = subL->_right;parent->_parent = subL;//不要忘记更新parent的父指针if (_root == parent) // parent就是根//if (ppNode == nullptr) //也可以使用这个判断条件{_root = subL;_root->_parent = nullptr;}else // parent是左或右子树{// parent是左就把subL链接到左,是右就链接到右if (parent == ppNode->_left){ppNode->_left = subL;}else{ppNode->_right = subL;}subL->_parent = ppNode;// 同样不要忘记更新subL的父指针}// 最后更新parent和subL的平衡因子parent->_bf = subL->_bf = 0;			
}
  1. 新节点插入较高右右右子树的右右右侧—右右:左单旋

在这里插入图片描述
左单旋实现及情况考虑可参考右单旋

h == 0的情况:

在这里插入图片描述

h == 1:

在这里插入图片描述

void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;// 60的左变成30的右parent->_right = subRL;// 更新subRL的父指针if (subRL){subRL->_parent = parent;}Node* ppNode = parent->_parent;// 30变成60的左subR->_left = parent;parent->_parent = subR;//if (_root == parent)if (ppNode == nullptr){_root = subR;_root->_parent = nullptr;}else{if (parent == ppNode->_left){ppNode->_left = subR;}else{ppNode->_right = subR;}subR->_parent = ppNode;}parent->_bf = subR->_bf = 0;
}

像下图的情况简单的单旋已经不能正确调整平衡,需要使用双旋(不同轴点的单旋):

在这里插入图片描述

  1. 新节点插入较高左左左子树的右右右侧—左右:先左单旋再右单旋

a/d是高度为 h 的AVL树
b/c是高度为 h - 1 的AVL树
在这里插入图片描述

h == 0:

在这里插入图片描述

h == 1:

在这里插入图片描述

看图写代码:

void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;// 对30左单旋,对90右单旋RotateL(parent->_left);RotateR(parent);// 最后更新平衡因子if (bf == 0) // subLR自己是新增{parent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf == -1) // 在subLR的左新增{parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1) // 在subLR的右新增{parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else{assert(false);// 异常处理}
}
  1. 新节点插入较高右右右子树的左左左侧—右左:先右单旋再左单旋

在这里插入图片描述

h == 0:

在这里插入图片描述

h == 1:

在这里插入图片描述

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

总结:
假如以 parent 为根的子树不平衡,即 parent 的平衡因子为 2 或者 -2 ,分以下情况考虑:

  1. parent 的平衡因子为 2,说明 parent 的右子树高,设 parent 的右子树的根为 subR
    • 当 subR 的平衡因子为 1 时,执行左单旋
    • 当 subR 的平衡因子为 -1 时,执行右左双旋
  2. parent 的平衡因子为 -2 ,说明 parent 的左子树高,设 parent的左子树的根为 subL
    • 当 subL 的平衡因子为 -1 是,执行右单旋
    • 当 subL 的平衡因子为 1 时,执行左右双旋

旋转完成后,原 parent 为根的子树高度降低,已经平衡,不需要再向上更新。

insert 时平衡因子检测的整体代码:

while (parent) // parent为空,也就更新到根停止
{// 更新平衡因子// 新增在左,parent->bf--;// 新增在右,parent->bf++;if (cur == parent->_left){parent->_bf--;}else{parent->_bf++;}//检测if (parent->_bf == 0){break;// 无需继续更新}else if (parent->_bf == 1 || parent->_bf == -1){// 插入前parent的平衡因子是0,插入后parent的平衡因为为1 或者 - 1 ,说明以parent为根的二叉树// 的高度增加了一层,因此需要继续向上调整cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){// parent的平衡因子为-2/2,违反了AVL树的平衡性// 需要对以 parent 为根的树进行 旋转 处理if (parent->_bf == -2 && cur->_bf == -1) // 右单旋{RotateR(parent);}else if (parent->_bf == 2 && cur->_bf == 1) // 左单旋{RotateL(parent);}else if (parent->_bf == -2 && cur->_bf == 1) // 左右双旋{RotateLR(parent);}else if (parent->_bf == 2 && cur->_bf == -1) // 右左双旋{RotateRL(parent);}else{assert(false);// 平衡因子异常}break; // 旋转完成后,原 parent 为根的子树高度降低,已经平衡,不需要再向上更新}else{assert(false); // 平衡因子异常:绝对值大于2}
}

AVL树的整体代码:AVL树的简单模拟实现


五、AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  1. 验证其为二叉搜索树
    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    每个节点子树高度差的绝对值不超过1(注意节点中如果有平衡因子,还需验证节点的平衡因子是否计算正确
int Height(Node* root)
{if (root == nullptr)return 0;int lh = Height(root->_left);int rh = Height(root->_right);return lh > rh ? lh + 1 : rh + 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){std::cout << root->_kv.first << " 平衡因子异常" << std::endl;return false;}return abs(rightHeight - leftHeight) < 2&& IsBalance(root->_left)&& IsBalance(root->_right);
}

六、AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2(N)log_2 (N)log2​(N)。

但是如果要对AVL树做一些结构修改的操 作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。


相关内容

热门资讯

求经典台词和经典旁白 求经典台词和经典旁白谁有霹雳布袋戏里的经典对白和经典旁白啊?朋友,你尝过失去的滋味吗? 很多人在即将...
小王子第二章主要内容概括 小王子第二章主要内容概括小王子第二章主要内容概括小王子第二章主要内容概括
爱情睡醒了第15集里刘小贝和项... 爱情睡醒了第15集里刘小贝和项天骐跳舞时唱的那首歌是什么谢谢开始找舞伴的时候是林俊杰的《背对背拥抱》...
世界是什么?世界是什么概念?可... 世界是什么?世界是什么概念?可以干什么?物质的和意识的 除了我们生活的地方 比方说山 河 公路 ...
全职猎人中小杰和奇牙拿一集被抓 全职猎人中小杰和奇牙拿一集被抓动画片是第五十九集,五十八集被发现,五十九被带回基地,六十逃走
“不周山”意思是什么 “不周山”意思是什么快快快快......一座山,神话里被共工撞倒了。
《揭秘》一元一分15张跑得快群... 一元一分麻将群加群主微【ab120590】【tj525555】 【mj120590】等风也等你。喜欢...
玩家必看手机正规红中麻将群@2... 好运连连,全网推荐:(ab120590)(mj120590)【tj525555】-Q号:(QQ443...
始作俑者15张跑的快群@24小... 微信一元麻将群群主微【ab120590】 【tj525555】【mj120590】一元一分群内结算,...
《重大通知》24小时一元红中麻... 加V【ab120590】【tj525555】【mj120590】红中癞子、跑得快,等等,加不上微信就...
盘点一下正规一块红中麻将群@2... 一元一分麻将群加群主微:微【ab120590】 【mj120590】【tj525555】喜欢手机上打...
(免押金)上下分一元一分麻将群... 微【ab120590】 【mj120590】【tj525555】专业麻将群三年房费全网最低,APP苹...
[解读]正规红中麻将跑的快@群... 微信一元麻将群群主微【ab120590】 【tj525555】【mj120590】一元一分群内结算,...
《普及一下》全天24小时红中... 微【ab120590】 【mj120590】【tj525555】专业麻将群三年房费全网最低,APP苹...
优酷视频一元一分正规红中麻将... 好运连连,全网推荐:(ab120590)(mj120590)【tj525555】-Q号:(QQ443...
《火爆》加入附近红中麻将群@(... 群主微【ab120590】 【mj120590】【tj525555】免带押进群,群内跑包包赔支持验证...
《字节跳动》哪里有一元一分红中... 1.进群方式-[ab120590]或者《mj120590》【tj525555】--QQ(QQ4434...
全网普及红中癞子麻将群@202... 好运连连,全网推荐:(ab120590)(mj120590)【tj525555】-Q号:(QQ443...
「独家解读」一元一分麻将群哪里... 1.进群方式《ab120590》或者《mj120590》《tj525555》--QQ(4434063...
通知24小时不熄火跑的快群@2... 1.进群方式《ab120590》或者《mj120590》《tj525555》--QQ(4434063...