探索数据结构:AVL树的分析与实现
✨✨ 欢迎大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:数据结构与算法
贝蒂的主页:Betty’s blog
1. AVL树的介绍
在前面我们学习二叉搜索树时知道,在数据有序或接近有序时二叉搜索树会退化为单边树,查找时相当于在单链表中查找,效率低下。
为了解决这个问题,1962 年,两位俄罗斯数学家G.M.Adelson-Velskii和E.M.Landis提出了一种新方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。而这颗新的树我们平衡二叉搜索(排序)树,简称AVL
树。其具有以下特点:
AVL
树本质就是一颗二叉搜索树。AVL
树左右两棵子树的高度差的绝对值(平衡因子)不超过1。AVL
树的左右两棵子树也是一颗AVL
树。
其中特别注意:空树也是一颗AVL
树,并且由于AVL
树也是二叉搜索树,所以AVL
树的中序遍历是有序的。
AVL
树通过特殊的构造有效的避免了二叉搜索树在数据有序或接近有序时退化为单边树的情况。这使得AVL
树在查找、插入和删除操作上,能够保持较为稳定的时间复杂度,而不会因为数据的特殊分布导致性能急剧下降。
2. AVL树的功能
以下是常见的AVL树的功能:
AVL
树的插入。AVL
树的查找。AVL
树的删除。
3. AVL树的结构
3.1. AVL树的节点
AVL
树的节点本质与二叉搜索树的节点差不多,所以肯定有三个成员变量:左子树_left
,右子树_right
,键值_val
,并且键值我们采用KV
模型的形式。而为了后续调整我们还需一个平衡因子_bf
(默认为右子树的高度-左子树的高度)以及一个父节点_parent
。当然为了适配不同的类型,我们可以定义一个模版.。
template<class K,class V>
struct AVLTreeNode
{AVLTreeNode(const pair<K, V>& val = pair<K, V>()):_kv(val),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}pair<K, V> _kv;//kv模型int _bf;//平衡因子AVLTreeNode* _left;//左子树AVLTreeNode* _right;//右子树AVLTreeNode* _parent;//指向父节点
};
3.2. AVL树
然后我们就可以通过节点来定义AVL
树,并将根节点初始化为空。
template<class K,class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public://...具体功能
private:Node* _root = nullptr;//根节点
};
4. AVL树的初始化与销毁
4.1. 构造函数/拷贝构造/赋值重载
首先我们直接定义一个无参的构造函数,因为我们在定义拷贝构造之后编译器就不会在生成默认的构造函数了。
AVLTree()
{}
之后我们可以利用递归来实现一个拷贝构造函数。
AVLTree(const AVLTree& t)
{_root = copy(t._root);
}
Node* copy(Node* root)
{// 如果传入的根节点为空,直接返回空指针,表示拷贝结束if (root == nullptr)return nullptr;// 为新树创建一个与原始根节点值相同的新节点Node* newnode = new Node(root->_kv);// 递归地拷贝原始根节点的左子树,并将结果赋给新节点的左指针newnode->_left = copy(root->_left);// 递归地拷贝原始根节点的右子树,并将结果赋给新节点的右指针newnode->_right = copy(root->_right);// 新节点的父节点默认为空newnode->_parent = nullptr;// 如果新节点的左子节点存在,设置其父节点为新节点if (newnode->_left){newnode->_left->_parent = newnode;}// 如果新节点的右子节点存在,设置其父节点为新节点if (newnode->_right){newnode->_right->_parent = newnode;}// 返回新树的根节点指针return newnode;
}
最后我们通过一个简单的方式实现赋值重载——通过形参调用拷贝构造出一个临时变量,然后交换this
所指向的变量,这样原本this
所指向的对象出了作用域就会销毁,间接实现了实现赋值重载。
AVLTree<K,V>& operator=(const AVLTree <K,V> t)
{//赋值重载this->swap(_root, t._root);return *this;
}
4.2. 析构函数
析构函数需要借助递归释放所有节点,而为了方便我们传参我们可以定义子函数来帮助我们解决。
~AVLTree()
{Destroy(_root);
}
void Destroy(Node*& root)
{if (root == nullptr){return;}//递归销毁左子树Destroy(root->_left);//递归销毁右子树Destroy(root->_right);//销毁根节点delete root;root = nullptr;
}
5. AVL树的功能实现
5.1. AVL树的旋转
我们知道AVL
树左右子树的高度差绝对值要保证不超过1,也就是说AVL
树的平衡因子_bf
只能取-1
,0
,1
三个值。而无论插入还是删除都可能破坏原有的结构,导致AVL
树失衡。为了重新平衡AVL
树,我们需要重新对其调整。
首先我们可以将AVL
树被破坏的情形可以抽象成以下四种场景,其中红色字体代表该节点的平衡因子,蓝色节点代表破坏AVL
树平衡的节点。
5.1.1. 右单旋
首先第一种情况就是破坏AVL
树平衡的节点位于较高左子树的左侧。
对于这种情况我们需要右单旋的方式,设失衡节点为_parent
,其左子树节点为subL
,而左子树的右子树为subLR
。其调整规则如下:
- 将
subLR
链接到parent
的左边。- 将
parent
链接到subL
的右边。- 将
parent
与subL
的平衡因子置为0。
void RotateR(Node*&parent)
{// 获取父节点的左子节点Node* subL = parent->_left;// 获取父节点左子节点的右子节点Node* subLR = subL->_right;// 1.将subLR链接到parent的左边。parent->_left = subLR;// 如果父节点左子节点的右子节点存在,设置其父节点为当前父节点if (subLR){subLR->_parent = parent;}// 获取父节点的父节点Node* ppNode = parent->_parent;// 2.将parent链接到subL的右边。subL->_right = parent;//parent的父节点指向subLparent->_parent = subL;// 如果父节点存在父节点if (ppNode){// 如果父节点是其父亲节点的左子节点if (ppNode->_left == parent){// 将父节点的父亲节点的左子节点设置为当前父节点的左子节点ppNode->_left = subL;}// 如果父节点是其父亲节点的右子节点else{// 将父节点的父亲节点的右子节点设置为当前父节点的左子节点ppNode->_right = subL;}// 设置父节点左子节点的父节点为父节点的父节点subL->_parent = ppNode;}// 如果父节点为根节点else{// 将根节点设置为父节点的左子节点_root = subL;// 将父节点左子节点的父节点设置为空subL->_parent = nullptr;}//3.将parent与subL的平衡因子置为0。subL->_bf = parent->_bf = 0;//改变parent节点的指向,方便erase中的node返回parent = subL;
}
其中需要特别注意的就是:判断父节点**_parent**
到底是根节点,还是某个节点的子节点。
5.1.2. 左单旋
第二种情况就是破坏AVL
树平衡的节点位于较高右子树的右侧。
对于这种情况我们需要左单旋的方式,设失衡节点为_parent
,其右子树节点为subR
,而有右子树的左子树为subRL
。其调整规则如下:
- 将
subRL
链接到parent
的右边。- 将
parent
链接到subR
的左边。- 将
parent
与subR
的平衡因子置为0。
void RotateL(Node* &parent)
{// 获取父节点的右子节点Node* subR = parent->_right;// 获取父节点右子节点的左子节点Node* subRL = subR->_left;//1.将subRL链接到parent的右边。parent->_right = subRL;// 如果父节点右子节点的左子节点存在,设置其父节点为当前父节点if (subRL){subRL->_parent = parent;}// 获取父节点的父节点Node* ppNode = parent->_parent;//2.将parent链接到subR的左边。subR->_left = parent;// 设置父节点的父节点为右子节点parent->_parent = subR;// 如果父节点存在父节点if (ppNode){// 如果父节点是其父亲节点的左子节点if (ppNode->_left == parent){// 将父节点的父亲节点的左子节点设置为当前父节点的右子节点ppNode->_left = subR;}// 如果父节点是其父亲节点的右子节点else{// 将父节点的父亲节点的右子节点设置为当前父节点的右子节点ppNode->_right = subR;}// 设置父节点右子节点的父节点为父节点的父节点subR->_parent = ppNode;}// 如果父节点为根节点else{// 将根节点设置为父节点的右子节点_root = subR;// 将父节点右子节点的父节点设置为空subR->_parent = nullptr;}// 3.将parent与subR的平衡因子置为0。subR->_bf = parent->_bf = 0;//改变parent节点的指向,方便erase中的node返回parent = subR;
}
其中需要特别注意的就是:判断父节点**_parent**
到底是根节点,还是某个节点的子节点。
5.1.3. 先左单旋,再右单旋
第三种情况就是破坏AVL
树平衡的节点位于较高左子树的右侧。
对于这种情况我们需要先左单旋,再右单旋的方式,设失衡节点为_parent
,其左子树节点为subL
,而有左子树的右子树为subLR
。其调整规则如下:
- 先对
subL
进行左单旋。- 在对
parent
进行右单旋。- 调整平衡因子。
但是平衡因子的调整又可以分别三种情况:
- 当
subLR = -1
时,如上图所示,调整后subL = 0
,subLR = 0
,parent = 1
。 - 当
subLR = 0
,即h = 0
时:
比如依次插入三个节点,10,5,6,如下图所示:
调整后subL = 0
,subLR = 0
,parent = 0
。
- 当
subLR = 1
时,具体情况如下图:
调整后subL = -1
,subLR = 0
,parent = 0
。
最后我们可以总结一下:
- 当
subLR = 0
时:调整为subLR = 0,subL = 0,parent = 0
。- 当
subLR = -1
时:调整为subLR = 0,subL = 0,parent = 1
。- 当
subLR = 1
时:调整为subLR = 0,subL = -1,parent = 0
。
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;//先左旋RotateL(parent->_left);//再右旋RotateR(parent);//1.情况1if (bf == 0){parent->_bf = subL->_bf = subLR->_bf = 0;}//2.情况2else if (bf == -1){parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}//3.情况3else if (bf == 1){parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else{//走到这里说明AVL树有问题assert(false);}
}
5.1.4. 先右单旋,再左单旋
最后一种情况就是破坏AVL
树平衡的节点位于较高右子树的左侧。
对于这种情况我们需要先右单旋,再左单旋的方式,设失衡节点为_parent
,其右子树节点为subR
,而有右子树的左子树为subRL
。其调整规则如下:
- 先对
subR
进行右单旋。- 在对
parent
进行左单旋。- 调整平衡因子。
同理,平衡因子的调整又可以分别三种情况:
- 当
subRL = -1
时,如上图所示,调整后subR = 1
,subRL = 0
,parent = 0
。 - 当
subRL = 0
,即h = 0
时:
比如依次插入三个节点,5,10,6,如下图所示:
调整后subR = 0
,subRL = 0
,parent = 0
。
3.当subRL = 1
时,具体情况如下图:
调整后subR = 0
,subRL = 0
,parent = -1
。
最后我们可以总结一下:
- 当
subRL = 0
时:调整为subRL = 0,subR = 0,parent = 0
。- 当
subRL = -1
时:调整为subRL = 0,subR = 1,parent = 0
。- 当
subRL = 1
时:调整为subRL = 0,subL = 0,parent = -1
。
void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;//先右单旋Rotate(parent->_right);Rotate(parent);//1.情况一if (bf == 0){parent->_bf = subRL->_bf = subR->_bf = 0;}//2.情况二else if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}//3.情况三else if (bf == 1){parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else{//走到这里说明AVL树出错assert(false);}
}
5.2. AVL树的插入
向AVL
树进行插入,首先是先找到需要插入的位置,这个逻辑与二叉搜索树类似,这里就不在赘述。找到之后对父节点的平衡因子进行更新,更新平衡因子就可以分为以下三种大的情况:
- 父节点平衡因子为 0,整体高度不变,不需要再向上调整平衡因子。
- 父节点平衡因子为1或-1,整体高度改变,需要再向上调整平衡因子。
- 父节点平衡因子为2或-2,平衡被破坏,需要进行旋转。
平衡被破坏同样可以分为四种情况,与旋转的方式想对应,其中蓝色为插入节点。首先第一种往较高左子树的左侧插入,此时parent = -2
,subL = -1
,进行右单旋。
第二种往较高右子树的右侧插入,此时parent = 2
,subL = 1
,进行左单旋。
第三种往较高左子树的右侧插入,此时parent = -2
,subL = 1
,先左旋再右旋。
第四种往较高右子树的右侧插入,先右旋再左旋,此时parent = 2
,subR = -1
,先右旋再左旋。
最后我们可以归纳总结出:
- 父节点平衡因子为 0,整体高度不变,不需要再向上调整平衡因子。
- 父节点平衡因子为1或-1,整体高度改变,需要再向上调整平衡因子。
- 父节点平衡因子为2或-2,平衡被破坏,需要进行旋转。设父节点为
parent
,插入方向子节点为cur
。
parent = -2
且cur = -1
进行右单旋。parent = 2
且cur = 1
进行左单旋。parent = -2
且cur = 1
进行先进行左单旋,再进行右单旋。parent = 2
且cur = -1
进行先进行右单旋,再进行左单旋。
bool insert(const pair<K, V>& kv){// 如果根节点为空,创建新节点作为根节点并设置平衡因子为 0,然后返回插入成功if (_root == nullptr){_root = new Node(kv);_root->_bf = 0;return true;}// 初始化父节点为空,当前节点为根节点Node* parent = nullptr;Node* cur = _root;// 查找插入位置while (cur){// 小于当前节点,往左子树if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}// 大于当前节点,往右子树else if (kv.first > cur->_kv.first){parent = cur;cur = cur->_right;}// 键已存在,返回插入失败else{return false;}}// 找到插入位置,创建新节点cur = new Node(kv);// 根据键值大小确定新节点在父节点的位置if (kv.first > parent->_kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;// 更新平衡因子while (parent){// 如果插入在左边,父节点的平衡因子减 1if (cur == parent->_left){parent->_bf--;}// 如果插入在右边,父节点的平衡因子加 1else{parent->_bf++;}// 如果父节点平衡因子为 0,整体高度不变,不需要再向上调整if (parent->_bf == 0){break;}// 平衡因子为 1 或 -1,需要继续向上调整else if (parent->_bf == 1 || parent->_bf == -1){cur = parent;parent = parent->_parent;}// 平衡因子为 -2 或 2,需要进行旋转操作以保持平衡else if (parent->_bf == -2 || parent->_bf == 2){// 右单旋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);}break;}// 插入之前 AVL 树就已经不平衡了,断言else{assert(false);}}return true;}
5.3. AVL树的查找
AVL
树的查找逻辑就与二叉搜索树一样了,这里就不在赘述。
Node* Find(const K& val)
{Node* cur = _root;while (cur){if (cur->_kv.first > val){//左子树中查找cur = cur->_left;}else if (cur->_kv.first < val){//右子树中查找cur = cur->_right;}else{//找到了return cur;}}return nullptr;
}
5.4. AVL树的删除
AVL
树的删除逻辑其实整体与二叉搜索树的删除逻辑类似,可以分为三种情况讨论:删除节点的左子树节点为空
,删除节点的右子树节点为空,删除节点的左右子树都不为空。其中删除节点的左右子树都不为空可以经过伪删除法,即寻找到左子树的最右节点即左子树的最大值,或者是右子树的最左节点即右子树的最小值。然后赋值,转换为在其左子树或者右子树删除节点。而我们的AVL
树本质是为了保持平衡,所以尽量选择删除子树较高的一方。
因为删除节点的左右子树都不为空的情况一定会被转换另外两种情况,所以我们只需要讨论删除节点的左子树节点为空,删除节点的右子树节点为空这两种情况。
当然这里在上面基础上再重新分类可分为:
- 删除节点在父节点的左侧:
当节点数少于三个时,删除后父节点平衡因子为1或0时,正常删除。
当父节点parent
的右子树的左右子树高度相等,且删除后父节点平衡因子为2时,进行左单旋。
当父节点parent
的右子树的右子树高度大于其左子树高度,且删除后父节点平衡因子为2时,进行左单旋。
当父节点parent
的右子树的右子树高度小于其左子树高度,且删除后父节点平衡因子为2时,先进行右单旋,再进行左单旋。
- 删除节点在父节点右侧:
当节点数少于三个时,删除后父节点平衡因子为-1或0时,正常删除。
当父节点parent
的左子树的左右子树高度相等,且删除后父节点平衡因子为-2时,进行右单旋。
当父节点parent
的左子树的左子树高度大于其右子树高度,且删除后父节点平衡因子为-2时,进行右单旋。
当父节点parent
的左子树的左子树高度小于其右子树高度,且删除后父节点平衡因子为-2时,先进行左单旋,再进行右单旋。
最后我们可以总结出以下结论:
- 当删除节点在父节点的左侧时
- 父节点的平衡因子为2时,如果根节点的右子树的右子树高度大于等于其左子树高度,进行左单旋,否则进行先右旋再左旋。
- 父节点的平衡因子不为2时,正常删除。
- 当删除节点在父节点的右侧时
- 父节点的平衡因子为-2时,如果根节点的左子树的左子树高度大于等于其右子树高度,进行右单旋,否则进行先左旋再右旋。
- 父节点的平衡因子不为-2时,正常删除。
//求高度
int Hegiht(Node* root)
{if (root == nullptr)return 0;int leftHegiht = Hegiht(root->_left);//先计算左子树高度int rightHegiht = Hegiht(root->_right);//再计算右子树高度return leftHegiht > rightHegiht ? leftHegiht + 1 : rightHegiht + 1;
}
void erase(const K& val)
{//递归删除_root = _erase(_root, val);
}
Node* _erase(Node* node, const K& val)
{// 如果当前节点为空,直接返回空指针if (node == nullptr){return nullptr;}// 如果当前节点的键值大于要删除的键值,在左子树中删除if (node->_kv.first > val){node->_left = _erase(node->_left, val);//更新节点的父节点if (node->_left)node->_left->_parent = node;// 调整节点的平衡因子node->_bf = Hegiht(node->_right) - Hegiht(node->_left);int bf = node->_bf;//情况一删除节点都在左边if (bf == 2){int rightHegiht = Hegiht(node->_right->_right);int leftHegiht = Hegiht(node->_right->_left);if (rightHegiht >= leftHegiht){RotateL(node);}else{RotateRL(node);}}}// 如果当前节点的键值小于要删除的键值,在右子树中删除else if (node->_kv.first < val){node->_right = _erase(node->_right, val);//更新节点的父节点if(node->_right)node->_right->_parent = node;// 调整节点平衡因子node->_bf = Hegiht(node->_right) - Hegiht(node->_left);int bf = node->_bf;//情况二删除节点都在右边if (bf == -2){int rightHegiht = Hegiht(node->_left->_right);int leftHegiht = Hegiht(node->_left->_left);if (leftHegiht >= rightHegiht){RotateR(node);}else{RotateLR(node);}}}// 找到要删除的节点else{// 如果节点有两个孩子if (node->_left != nullptr && node->_right != nullptr){// 如果左子树高度大于等于右子树高度if (Hegiht(node->_left) >= Hegiht(node->_right)){// 找到左子树中的最大节点Node* prev = node->_left;while (prev->_right)prev = prev->_right;int target = prev->_kv.first;// 将当前节点的值替换为左子树中的最大节点的值node->_kv = prev->_kv;// 在左子树中删除该最大节点node->_left = _erase(node->_left,target);}else{// 找到右子树中的最小节点Node* post = node->_right;while (post->_left)post = post->_left;// 将当前节点的值替换为右子树中的最小节点的值int target = post->_kv.first;node ->_kv = post->_kv ;// 在右子树中删除该最小节点node->_right = _erase(node->_right, target);}}// 如果节点最多有一个孩子else{// 如果有左孩子if (node->_left != nullptr){// 保存左孩子指针Node* left = node->_left;// 删除当前节点delete node;// 返回左孩子指针return left;}// 如果有右孩子else if (node->_right != nullptr){// 保存右孩子指针Node* right = node->_right;// 删除当前节点delete node;// 返回右孩子指针return right;}// 如果没有孩子else{// 返回空指针delete node;return nullptr;}}}// 返回调整后的节点指针return node;
}
6. 判断是否为AVL树
在判断一棵树是否为 AVL 树时,其核心在于检查每一个节点的左右子树高度差的绝对值是否小于 1 。由于需要对整棵树的所有子树进行这样的判断,所以采用递归的方法比较合适的。
具体实现时,定义一个函数来进行判断。然后,分别递归地获取左子树和右子树的高度。接着,进行条件判断。如果左子树或右子树中存在高度标记为 -1 的情况(意味着该子树不平衡),或者当前节点左右子树高度差的绝对值大于 1 ,那么就将当前子树的高度标记为 -1 并返回。如果当前节点的子树都是平衡的,那么就返回最高的高度。
最后,通过一个总函数来调用这个递归函数,并根据返回结果是否大于 0 来确定整棵树是否为 AVL 树。如果大于 0 ,则表示整棵树是AVL
树;否则,表示不是AVL
树。
//判断是否平衡
bool isBalanced()
{return _isBalanced(_root) >= 0;
}
int _isBalanced(Node* root)
{if (root == nullptr){return 0;}//左平衡高度int left_isBalanced = _isBalanced(root->_left);//右平衡高度int right_isBalanced = _isBalanced(root->_right);//如果右平衡-左平衡则返回-1,并且防止两边同时返回-1相减等于0的情况,需要单独判断if (left_isBalanced == -1 || right_isBalanced == -1 || abs(right_isBalanced - left_isBalanced) > 1){return -1;}//返回最高高度return max(left_isBalanced, right_isBalanced) + 1;
}
7. 复杂度分析
下是对上述AVL 树代码中主要操作的时间复杂度和空间复杂度分析:
时间复杂度:
insert
操作:平均和最坏情况下的时间复杂度均为 O ( log n ) O(\log n) O(logn)。在插入过程中,通过不断调整平衡因子和进行旋转操作来保持树的平衡,每次调整和旋转的操作时间都是常数级,而查找插入位置的过程类似于二叉搜索树,时间复杂度为
O ( log n ) O(\log n) O(logn)。Find
操作:平均和最坏情况下的时间复杂度均为 O ( log n ) O(\log n) O(logn)。与二叉搜索树的查找过程类似,每次比较都将搜索范围缩小一半。erase
操作:平均和最坏情况下的时间复杂度均为 O ( log n ) O(\log n) O(logn)。删除节点时需要查找节点位置,然后进行调整和可能的旋转操作,时间复杂度类似于插入操作。
空间复杂度:
insert
操作:空间复杂度为 O ( 1 ) O(1) O(1)。在插入过程中,主要的额外空间消耗在于创建新节点以及一些临时变量来存储指针和平衡因子等信息,这些都是常数级的空间消耗。Find
操作:空间复杂度为 O ( 1 ) O(1) O(1)。在查找过程中,仅使用了一些固定数量的指针和临时变量,没有额外的大规模空间分配。erase
操作:空间复杂度为 O ( 1 ) O(1) O(1)。删除操作中,主要的空间消耗在于临时变量和指针的存储,没有动态分配大规模的额外空间。总的来说,上述各个操作的主要空间复杂度都为 O ( 1 ) O(1) O(1),整个 AVL 树的空间复杂度主要取决于树中节点的数量,即 O ( n ) O(n) O(n),其中 n n n 是节点的数量。
8. 源码
#pragma once
#include<utility>
#include<assert.h>
template<class K,class V>
struct AVLTreeNode
{AVLTreeNode(const pair<K, V>& val = pair<K, V>()):_kv(val),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}pair<K, V> _kv;//kv模型int _bf;//平衡因子AVLTreeNode* _left;//左子树AVLTreeNode* _right;//右子树AVLTreeNode* _parent;//指向父节点
};
template<class K,class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public://...具体功能AVLTree(){}AVLTree(const AVLTree& t){_root = copy(t._root);}AVLTree<K,V>& operator=(const AVLTree <K,V> t){//赋值重载this->swap(_root, t._root);return *this;}void RotateR(Node*&parent){// 获取父节点的左子节点Node* subL = parent->_left;// 获取父节点左子节点的右子节点Node* subLR = subL->_right;// 1.将subLR链接到parent的左边。parent->_left = subLR;// 如果父节点左子节点的右子节点存在,设置其父节点为当前父节点if (subLR){subLR->_parent = parent;}// 获取父节点的父节点Node* ppNode = parent->_parent;// 2.将parent链接到subL的右边。subL->_right = parent;//parent的父节点指向subLparent->_parent = subL;// 如果父节点存在父节点if (ppNode){// 如果父节点是其父亲节点的左子节点if (ppNode->_left == parent){// 将父节点的父亲节点的左子节点设置为当前父节点的左子节点ppNode->_left = subL;}// 如果父节点是其父亲节点的右子节点else{// 将父节点的父亲节点的右子节点设置为当前父节点的左子节点ppNode->_right = subL;}// 设置父节点左子节点的父节点为父节点的父节点subL->_parent = ppNode;}// 如果父节点为根节点else{// 将根节点设置为父节点的左子节点_root = subL;// 将父节点左子节点的父节点设置为空subL->_parent = nullptr;}//3.将parent与subL的平衡因子置为0。subL->_bf = parent->_bf = 0;//改变parent节点的指向,方便erase中的node返回parent = subL;}void RotateL(Node* &parent){// 获取父节点的右子节点Node* subR = parent->_right;// 获取父节点右子节点的左子节点Node* subRL = subR->_left;//1.将subRL链接到parent的右边。parent->_right = subRL;// 如果父节点右子节点的左子节点存在,设置其父节点为当前父节点if (subRL){subRL->_parent = parent;}// 获取父节点的父节点Node* ppNode = parent->_parent;//2.将parent链接到subR的左边。subR->_left = parent;// 设置父节点的父节点为右子节点parent->_parent = subR;// 如果父节点存在父节点if (ppNode){// 如果父节点是其父亲节点的左子节点if (ppNode->_left == parent){// 将父节点的父亲节点的左子节点设置为当前父节点的右子节点ppNode->_left = subR;}// 如果父节点是其父亲节点的右子节点else{// 将父节点的父亲节点的右子节点设置为当前父节点的右子节点ppNode->_right = subR;}// 设置父节点右子节点的父节点为父节点的父节点subR->_parent = ppNode;}// 如果父节点为根节点else{// 将根节点设置为父节点的右子节点_root = subR;// 将父节点右子节点的父节点设置为空subR->_parent = nullptr;}// 3.将parent与subR的平衡因子置为0。subR->_bf = parent->_bf = 0;//改变parent节点的指向,方便erase中的node返回parent = subR;}void RotateLR(Node*&parent){Node* subL = parent->_left;Node* subLR = subL->_right;//先左旋RotateL(parent->_left);//再右旋RotateR(parent);int bf = subLR->_bf;//1.情况1if (bf == 0){parent->_bf = subL->_bf = subLR->_bf = 0;}//2.情况2else if (bf == -1){parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}//3.情况3else if (bf == 1){parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else{//走到这里说明AVL树有问题assert(false);}}void RotateRL(Node*&parent){Node* subR = parent->_right;Node* subRL = subR->_left;//先右单旋RotateR(parent->_right);//再左单旋RotateL(parent);int bf = subRL->_bf;//1.情况一if (bf == 0){parent->_bf = subRL->_bf = subR->_bf = 0;}//2.情况二else if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}//3.情况三else if (bf == 1){parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else{//走到这里说明AVL树出错assert(false);}}bool insert(const pair<K, V>& kv){// 如果根节点为空,创建新节点作为根节点并设置平衡因子为 0,然后返回插入成功if (_root == nullptr){_root = new Node(kv);_root->_bf = 0;return true;}// 初始化父节点为空,当前节点为根节点Node* parent = nullptr;Node* cur = _root;// 查找插入位置while (cur){// 小于当前节点,往左子树if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}// 大于当前节点,往右子树else if (kv.first > cur->_kv.first){parent = cur;cur = cur->_right;}// 键已存在,返回插入失败else{return false;}}// 找到插入位置,创建新节点cur = new Node(kv);// 根据键值大小确定新节点在父节点的位置if (kv.first > parent->_kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;// 更新平衡因子while (parent){// 如果插入在左边,父节点的平衡因子减 1if (cur == parent->_left){parent->_bf--;}// 如果插入在右边,父节点的平衡因子加 1else{parent->_bf++;}// 如果父节点平衡因子为 0,整体高度不变,不需要再向上调整if (parent->_bf == 0){break;}// 平衡因子为 1 或 -1,需要继续向上调整else if (parent->_bf == 1 || parent->_bf == -1){cur = parent;parent = parent->_parent;}// 平衡因子为 -2 或 2,需要进行旋转操作以保持平衡else if (parent->_bf == -2 || parent->_bf == 2){// 右单旋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);}break;}// 插入之前 AVL 树就已经不平衡了,断言else{assert(false);}}return true;}Node* Find(const K& val){Node* cur = _root;while (cur){if (cur->_kv.first > val){//左子树中查找cur = cur->_left;}else if (cur->_kv.first < val){//右子树中查找cur = cur->_right;}else{//找到了return cur;}}return nullptr;}//求高度int Hegiht(Node* root){if (root == nullptr)return 0;int leftHegiht = Hegiht(root->_left);//先计算左子树高度int rightHegiht = Hegiht(root->_right);//再计算右子树高度return leftHegiht > rightHegiht ? leftHegiht + 1 : rightHegiht + 1;}void erase(const K& val){//递归删除_root = _erase(_root, val);}//判断是否平衡bool isBalanced(){return _isBalanced(_root) >= 0;}~AVLTree(){Destroy(_root);}private:void Destroy(Node* root){if (root == nullptr){return;}//cout << root->_kv.first << " ";//递归销毁左子树Destroy(root->_left);//递归销毁右子树Destroy(root->_right);//销毁根节点delete root;//root = nullptr;}Node* _erase(Node* node, const K& val){// 如果当前节点为空,直接返回空指针if (node == nullptr){return nullptr;}// 如果当前节点的键值大于要删除的键值,在左子树中删除if (node->_kv.first > val){node->_left = _erase(node->_left, val);//更新节点的父节点if (node->_left)node->_left->_parent = node;// 调整节点的平衡因子node->_bf = Hegiht(node->_right) - Hegiht(node->_left);int bf = node->_bf;//情况一删除节点都在左边if (bf == 2){int rightHegiht = Hegiht(node->_right->_right);int leftHegiht = Hegiht(node->_right->_left);if (rightHegiht >= leftHegiht){RotateL(node);}else{RotateRL(node);}}}// 如果当前节点的键值小于要删除的键值,在右子树中删除else if (node->_kv.first < val){node->_right = _erase(node->_right, val);//更新节点的父节点if(node->_right)node->_right->_parent = node;// 调整节点平衡因子node->_bf = Hegiht(node->_right) - Hegiht(node->_left);int bf = node->_bf;//情况二删除节点都在右边if (bf == -2){int rightHegiht = Hegiht(node->_left->_right);int leftHegiht = Hegiht(node->_left->_left);if (leftHegiht >= rightHegiht){RotateR(node);}else{RotateLR(node);}}}// 找到要删除的节点else{// 如果节点有两个孩子if (node->_left != nullptr && node->_right != nullptr){// 如果左子树高度大于等于右子树高度if (Hegiht(node->_left) >= Hegiht(node->_right)){// 找到左子树中的最大节点Node* prev = node->_left;while (prev->_right)prev = prev->_right;int target = prev->_kv.first;// 将当前节点的值替换为左子树中的最大节点的值node->_kv = prev->_kv;// 在左子树中删除该最大节点node->_left = _erase(node->_left,target);}else{// 找到右子树中的最小节点Node* post = node->_right;while (post->_left)post = post->_left;// 将当前节点的值替换为右子树中的最小节点的值int target = post->_kv.first;node ->_kv = post->_kv ;// 在右子树中删除该最小节点node->_right = _erase(node->_right, target);}}// 如果节点最多有一个孩子else{// 如果有左孩子if (node->_left != nullptr){// 保存左孩子指针Node* left = node->_left;// 删除当前节点delete node;// 返回左孩子指针return left;}// 如果有右孩子else if (node->_right != nullptr){// 保存右孩子指针Node* right = node->_right;// 删除当前节点delete node;// 返回右孩子指针return right;}// 如果没有孩子else{// 返回空指针delete node;return nullptr;}}}// 返回调整后的节点指针return node;}int _isBalanced(Node* root){if (root == nullptr){return 0;}//左平衡高度int left_isBalanced = _isBalanced(root->_left);//右平衡高度int right_isBalanced = _isBalanced(root->_right);//如果右平衡-左平衡则返回-1,并且防止两边同时返回-1相减等于0的情况,需要单独判断if (left_isBalanced == -1 || right_isBalanced == -1 || abs(right_isBalanced - left_isBalanced) > 1){return -1;}//返回最高高度return max(left_isBalanced, right_isBalanced) + 1;}Node* copy(Node* root){// 如果传入的根节点为空,直接返回空指针,表示拷贝结束if (root == nullptr)return nullptr;// 为新树创建一个与原始根节点值相同的新节点Node* newnode = new Node(root->_kv);// 递归地拷贝原始根节点的左子树,并将结果赋给新节点的左指针newnode->_left = copy(root->_left);// 递归地拷贝原始根节点的右子树,并将结果赋给新节点的右指针newnode->_right = copy(root->_right);// 新节点的父节点默认为空newnode->_parent = nullptr;// 如果新节点的左子节点存在,设置其父节点为新节点if (newnode->_left){newnode->_left->_parent = newnode;}// 如果新节点的右子节点存在,设置其父节点为新节点if (newnode->_right){newnode->_right->_parent = newnode;}// 返回新树的根节点指针return newnode;}Node* _root = nullptr;//根节点
};}// 如果有右孩子else if (node->_right != nullptr){// 保存右孩子指针Node* right = node->_right;// 删除当前节点delete node;// 返回右孩子指针return right;}// 如果没有孩子else{// 返回空指针delete node;return nullptr;}}}// 返回调整后的节点指针return node;}int _isBalanced(Node* root){if (root == nullptr){return 0;}//左平衡高度int left_isBalanced = _isBalanced(root->_left);//右平衡高度int right_isBalanced = _isBalanced(root->_right);//如果右平衡-左平衡则返回-1,并且防止两边同时返回-1相减等于0的情况,需要单独判断if (left_isBalanced == -1 || right_isBalanced == -1 || abs(right_isBalanced - left_isBalanced) > 1){return -1;}//返回最高高度return max(left_isBalanced, right_isBalanced) + 1;}Node* copy(Node* root){// 如果传入的根节点为空,直接返回空指针,表示拷贝结束if (root == nullptr)return nullptr;// 为新树创建一个与原始根节点值相同的新节点Node* newnode = new Node(root->_kv);// 递归地拷贝原始根节点的左子树,并将结果赋给新节点的左指针newnode->_left = copy(root->_left);// 递归地拷贝原始根节点的右子树,并将结果赋给新节点的右指针newnode->_right = copy(root->_right);// 新节点的父节点默认为空newnode->_parent = nullptr;// 如果新节点的左子节点存在,设置其父节点为新节点if (newnode->_left){newnode->_left->_parent = newnode;}// 如果新节点的右子节点存在,设置其父节点为新节点if (newnode->_right){newnode->_right->_parent = newnode;}// 返回新树的根节点指针return newnode;}Node* _root = nullptr;//根节点
};
相关文章:
探索数据结构:AVL树的分析与实现
✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:数据结构与算法 贝蒂的主页:Betty’s blog 1. AVL树的介绍 在前面我们学习二叉搜索树时知道,在数据有序…...
使用 C++ 实现简单的插件系统
使用 C 实现简单的插件系统 在现代软件开发中,插件系统是一种常见的架构模式,它允许开发者在不修改主程序的情况下,扩展应用程序的功能。通过插件,用户可以根据需要添加或移除功能模块,从而提高软件的灵活性和可维护性…...
使用Python创建省份城市地图选择器
在这篇博客中,我们将探讨如何使用Python创建一个简单而实用的省份城市地图选择器。这个项目不仅能帮助我们学习Python的基础知识,还能让我们了解如何处理JSON数据和集成网页浏览器到桌面应用程序中。 C:\pythoncode\new\geographicgooglemap.py 全部代码…...
【Java 数据结构】Stack和Queue介绍
Stack和Queue StackStack是什么Stack的使用构造方法常用方法 栈的模拟实现初始化和基本方法入栈出栈查看栈顶 栈的应用链栈的简单介绍 QueueQueue是什么Queue的使用队列的模拟实现初始化入队出队查看队头元素 循环队列循环队列的定义及其注意点循环队列的实现初始化和基本方法获…...
Docker基本语法
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、更新yum镜像仓库(一)查看本地yum镜像源地址(二)设置docker的镜像仓库(1)安装必要工具…...
uniapp 对于scroll-view滑动和页面滑动的联动处理
需求 遇到一个需求 解决方案 这个时候可以做一个内页面滑动判断 <!-- scroll-y 做true或者false的判断是否滑动 --> <view class"u-menu-wrap" style"background-color: #fff;"><scroll-view :scroll-y"data.isGo" scroll-wit…...
opencv基础的图像操作
1.读取图像,显示图像,保存图像 #图像读取、显示与保存 import numpy as np import cv2 imgcv2.imread(./src/1.jpg) #读取 cv2.imshow("img",img) #显示 cv2.imwrite("./src/2.jpg",img) #保存 cv2.waitKey(0) #让程序进入主循环(让…...
Java | Leetcode Java题解之第337题打家劫舍III
题目: 题解: class Solution {public int rob(TreeNode root) {int[] rootStatus dfs(root);return Math.max(rootStatus[0], rootStatus[1]);}public int[] dfs(TreeNode node) {if (node null) {return new int[]{0, 0};}int[] l dfs(node.left);i…...
本地查看的Git远程仓库分支与远程仓库分支数量不一致
说明:一次,在IDEA中想切换到某分支,但是查看Remote没有找到要切换的分支,但是打开GitLab,查看远程仓库,是有这个分支的。 解决:1)在IDEA的Git中,点下面Fatch获取一下远程…...
opencv-python实战项目九:基于拉普拉斯金字塔的图像融合
文章目录 一,简介:二,拉普拉斯金字塔介绍:三,算法实现步骤3.1 构建融合拉普拉斯金字塔3.2 融合后的拉普拉斯金字塔复原: 四,整体代码实现:五,效果: 一&#x…...
浅谈JDK
JDK(Java Development Kit) JDK是Java开发工具包,是Java编程语言的核心软件开发工具。 JDK包含了一系列用于开发、编译和运行Java应用程序的工具和资源。其中包括: 1.Java编译器(javac):用于将Java源代码编译成字节…...
爬虫案例3——爬取彩票双色球数据
简介:个人学习分享,如有错误,欢迎批评指正 任务:从500彩票网中爬取双色球数据 目标网页地址:https://datachart.500.com/ssq/ 一、思路和过程 目标网页具体内容如下: 我们的任务是将上图中…...
C++ | Leetcode C++题解之第337题打家劫舍III
题目: 题解: struct SubtreeStatus {int selected;int notSelected; };class Solution { public:SubtreeStatus dfs(TreeNode* node) {if (!node) {return {0, 0};}auto l dfs(node->left);auto r dfs(node->right);int selected node->val…...
软件架构设计师-UML知识导图
软件架构设计师-UML知识导图,包含如下内容: 结构化设计,包含结构化设计的概念、结构化设计的主要内容、概要设计、详细设计及模块设计原则;UML是什么:介绍UML是什么;UML的结构:构造块、公共机制…...
在使用transformers和pytorch时出现的版本冲突的问题
在使用transformers和torch库的时候,出现了以下问题: 1、OSError: [WinError 126] 找不到指定的模块。 Error loading "D:\Program Files\anaconda3\envs\testenv\Lib\site-packages\torch\lib\fbgemm.dll" or one of its dependencies. 2、…...
uniapp粘贴板地址识别
1: 插件安装 主要是依靠 address-parse 这个插件: 官网 收货地址自动识别 支持pc、h5、微信小程序 - DCloud 插件市场 // 首先需要引入插件 npm install address-parse --save 2:html部分 <view class""><view class&quo…...
C语言 | Leetcode C语言题解之第335题路径交叉
题目: 题解: bool isSelfCrossing(int* distance, int distanceSize){if (distance NULL || distanceSize < 4) {return false;}for (int i 3; i < distanceSize; i) {if ((distance[i] > distance[i - 2]) && (distance[i - 1] &l…...
TypeScript学习第十三篇 - 泛型
在编译期间不确定变量的类型,在调用时,由开发者指定具体的类型。 1. 如何给arg参数和函数指定类型? function identity(arg){return arg; }identity(1) identity(jack) identity(true) identity([]) identity(null)定义的时候,无…...
工业智能网关在汽车制造企业的应用价值及功能-天拓四方
随着工业互联网的飞速发展,工业智能网关作为连接物理世界与数字世界的桥梁,正逐渐成为制造业数字化转型的核心组件。本文将以一家汽车制造企业的实际使用案例为蓝本,深入解析工业智能网关在实际应用中的价值、功能及其实操性。 一、背景与挑…...
LLM - 在服务器中使用 Ollama + OpenWebUI 部署最新大模型
欢迎关注我的CSDN:https://spike.blog.csdn.net/ 本文地址:https://spike.blog.csdn.net/article/details/140992533 免责声明:本文来源于个人知识与公开资料,仅用于学术交流,欢迎讨论,不支持转载。 Ollama 是一个开源的大型语言模型(LLM)服务工具,目的是简化本地运行…...
重启人生计划-积蓄星火
🥳🥳🥳 茫茫人海千千万万,感谢这一刻你看到了我的文章,感谢观赏,大家好呀,我是最爱吃鱼罐头,大家可以叫鱼罐头呦~🥳🥳🥳 如果你觉得这个【重启人生…...
2024.08.11 校招 实习 内推 面经
地/球🌍 : neituijunsir 交* 流*裙 ,内推/实习/校招汇总表格 1、自动驾驶一周资讯 - 比亚迪将采购华为智驾系统,用于方程豹新款越野车;英特尔发布第一代车载独立显卡;黑芝麻智能上市首日破发大跌 自动…...
LCA(Lowest Common Ancestor)
LCA(Lowest Common Ancestor) 定义 在树上取两点 x,yx,y,他们的 LCA 为距离他们最近的公共祖先。 本章主要讲的是倍增求 LCA。 暴力求取 从 xx 开始向上移动到根结点,并标记沿途结点。从 yy 开始向上移动到根结点,…...
张钹院士:大模型时代的企业AI发展趋势
在当今技术迅速发展的时代,生成式人工智能与大模型正成为推动产业变革的重要力量。随着AI技术的不断成熟与普及,它的应用已从个人领域扩展至企业层面,广泛覆盖各行各业。 那么,新技术究竟会给产业带来哪些积极地影响?…...
php连接sphinx的长连接事宜以及sphinx的排除查询以及关于sphinx里使用SetSelect进行复杂的条件过滤或复杂查询
一、php连接sphinx的长连接事宜以及sphinx的排除查询 在使用php连接sphinx时,默认的sphinx连接非长连接,于是在想php连接sphinx能否进行一些优化 publish:January 9, 2018 -Tuesday: 方法:public bool SphinxClient::open ( void ) — 建立到…...
抓包分析排查利器TCPdump
tcpdump命令介绍与常规用法。 基础命令介绍 # 固定语法 -i 指定网卡名称 -nn 显示IP地址 -w 指定输出的文件名称 tcpdump -i eth0 -nn -w test.cap-nn 不把主机的网络地址与协议转换成名字 -w 把数据包数据写入指定的文件 and 连接参数 host 指明主机 port 指明端口 src 源IP…...
八种排序算法的复杂度(C语言)
归并排序(递归与非递归实现,C语言)-CSDN博客 快速排序(三种方法,非递归快排,C语言)-CSDN博客 堆排序(C语言)-CSDN博客 选择排序(C语言)以及选择排序优化-CSDN博客 冒泡排序(C语言)-CSDN博客 直接插入排序(C语言)-CSDN博客 希尔排序( 缩小增量排序 )(C语言)-CSDN博客 计数…...
docker compose部署rabbitmq集群,并使用haproxy负载均衡
一、创建rabbitmq的data目录 mkdir data mkdir data/rabbit1 mkdir data/rabbit2 mkdir data/rabbit3 二、创建.erlang.cookie文件(集群cookie用) echo "secretcookie" > .erlang.cookie 三、创建haproxy.cfg配置文件 global log stdout fo…...
git强制推送代码教程
git强制推送代码教程 首先说明情况,我的代码remote了两个git库,现在想要推送到其中一个,但是版本不对,被拒绝,因此下面将进行强制推送 首先检查远程库都有哪些 git remote -v2. 检查当前的分支 git branch当前分支前…...
windows C++-高级并发和异步(三)
深入了解 winrt::resume_foreground(下) 调用 winrt::resume_foreground 时会始终先排队,然后展开堆栈。 也可选择设置恢复优先级。 winrt::fire_and_forget RunAsync(DispatcherQueue queue) {...co_await winrt::resume_foreground(queue, DispatcherQueuePrior…...
钱多网站/推广小程序
matlab中矩阵LDLT分解与Cholesky分解矩阵LDLT分解与Cholesky分解:矩阵的LDLT消去函数的程序代码:%矩阵的LDLT分解function [s,l,d]ldlt(a)s1;l0;d0;%判断矩阵是否对称if a~a %矩阵不对称,输出错误信息 s0;else bdiag(a); %列向量b存放矩阵a的…...
常用网站开发软件/网络推广平台网站推广
目录:一、变量 二、变量类型 三、条件判断 四、循环 五、函数 六、模块 七、数据结构一、变量变量用来存放数据,语法:变量名 变量值,一般为了便于阅读,变量名采用数据意义数据类型来命名。如namestr 马云,…...
wordpress大前端破解/宁波seo网络推广推荐
Part 2 动画 我们通过set方法就可以快速的修改canvas上的图形的属性。但是,往往我们在开发网站的时候除了完成功能需求之外,也需要提高网页的美观。所以动画是一个必不可少的功能。 举个例子: rect.set(angle, 45);给这个变化属性添加动画…...
中山外贸网站建设/seo工具包括
参考:https://blog.csdn.net/zhouzuoluo/article/details/84781490转载于:https://www.cnblogs.com/web-fusheng/p/10682825.html...
男女做啊免费视频网站/网站技术制作
来自:http://kakajw.iteye.com/blog/1063843,感谢作者解决问题。 Tomcat 5.5使EL表达式不被解析。 现象 代码${userSession.user_name}是JSP中的一个代码片段; 如果部署到tomcat5.5中,不会显示出session中的变量user用户名,而只会…...
移动网站建设哪家好/免费数据分析网站
//程序存在bug,会不断占用内存直到死机//是malloc函数的问题/**************************************************** 文件名:pthread_server.c* 文件描述:创建子线程来接收客户端的数据***************************************************…...