0
点赞
收藏
分享

微信扫一扫

STL之AVL模拟的实现(万字长文详解)

STL之AVL模拟的实现

AVL树的概念

为什么会有AVL树?在STL中对map/multimap/set/multiset其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中 插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),

因此 map、set等关联式容器的底层结构是对==二叉树进行了平衡处理,即采用平衡树来实现==

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

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

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

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

image-20230411163816649

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

AVL树的实现

节点类

namespace MySTL
{
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode(const std::pair<K,V>& kv)//构造函数
:_kv(kv)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
{}
std::pair<K, V> _kv;
AVLTreeNode* _left;//左节点
AVLTreeNode* _right;//右节点
AVLTreeNode* _parent;//父节点
int _bf;//balance factory//平衡因子!
};
}

成员变量

namespace MySTL
{
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
private:
Node* _root = nullptr;//一个根节点!
};
}

insert

插入方法
namespace MySTL
{
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
bool insert(const std::pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
//为什么不使用cur-> _parent而是搞了一个新的parent呢?
//因为当cur走到最后就是一个nullptr!如果此时使用cur->_parent那么就出现了空指针的解引用!
while (cur)
{
parent = cur;
if (kv.first > cur->_kv.first)
{
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
cur = cur->_l eft;
}
else
{
return false;
}
}
cur = new Node(kv);
if (cur->_kv.first > parent->_kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
/////////////////////////////////////////////////////////
/
/上面的插入逻辑就是二叉搜索树的插入逻辑!
return true;
}
private:
Node* _root = nullptr;

};
}

==AVL树的插入逻辑与二叉搜索树的插入逻辑是一致的!==image-20230411210037181

如果插入在左边 那么根据父节点的 ==平衡因子 = 右子树高度 - 左子树高度==,此时平衡因子为 0 -1 = -1

如果插入在右边 那么就是 1- 0 = 1

我们需不需要对这颗树进行==处理首先就是要看更新后的平衡因子!==——一旦平衡因子绝对值大于1,就要开始处理这颗树

更新平衡因子

==那么该如何更新平衡因子呢==

image-20230411213715932

==所以我们发现平衡因子停止更新的有三种情况!==

  1. 平衡因子更新后变成 0 ,那么停止向上更新!
  2. 平衡因子更新后变成 2/-2 ,二叉树出问题!要进行平衡处理!停止向上更新!
  3. 平衡因子一直更新!直到平衡因子更新到根节点位置!
#include<iostream>
#include<cassert>
namespace MySTL
{
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
bool insert(const std::pair<K, V>& kv)
{
///插入操作
///////////////////////////////////////////////
//更新平衡因子
while (parent)//parent为空就结束!因为根节点的parent就是nullptr
{
if (cur == parent->_left)//如果插入的是左树就--
parent->_bf--;
else if (cur == parent->_right)//如果插入右树就++
parent->_bf++;

// 是否继续更新依据:子树的高度是否变化
// 1、parent->_bf == 0说明之前parent->_bf是 1 或者 -1
// 说明之前parent一边高一边低,这次插入填上矮的那边,parent所在子树高度不变,不需要继续往上更新
// 2、parent->_bf == 1 或 -1 说明之前是parent->_bf == 0,两边一样高,现在插入一边更高了,
// parent所在子树高度变了,继续往上更新
// 3、parent->_bf == 2 或 -2,说明之前parent->_bf == 1 或者 -1,现在插入严重不平衡,违反规则
// 就地处理--旋转
if (parent->_bf == 0)//平衡因子为0更新结束
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)//平衡因子为1继续向上更新!
{
cur = parent;
parent = parent->_parent;//这时候父节点指针的作用就凸显出来了,如果父节点指针,我们就很难去找到原来的父节点!
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//平衡处理!
}
else
{
assert(false);//如果到意味着在我们插入更新树之前就出问题了!
}
}
return true;
}
//我们也可以使用cur->_parent代替parent
//但是这样子可读性相比parent比较差一点
private:
Node* _root = nullptr;

};
}

左单旋
要处理旋转的情况——重点

要旋转的情况可以说有==无限多种情况下面我们可以先看几种==!

进行旋转的方法——重点

==旋转的目标==

  1. ==让这颗子树的左右高度差不超过1==
  2. ==旋转过程中,继续保持它是平衡搜索树!==
  3. ==更新调整子节点的平衡因子==
  4. ==降低子树的高度,让子树的高度跟插入前保持一致!==

image-20230412161728408

所以进行==左旋转==的时候我们要保持一下几个原则!

  1. 让问题节点(平衡因子为2的节点)的右孩子的左节点,变成问题节点的右节点
  2. 让问题节点成为==问题节点的原右子节点的左节点!==

==至此我们的左单旋才是真正结束了!==

右单旋!
要处理旋转的情况——重点

==理解了上面的左旋转后我们理解右旋转就很容易了!==

我们先看一下右旋转的抽象图!

image-20230413111343390

==很明显我们从抽象图中可以看出来!需要进行右单旋的条件==

if(parent->_bf == -2 && cur->_bf == -1);
进行旋转的方法——重点

右单旋的旋转其实和左单旋是思路是一一样的!

  1. ==将问题节点的左子节点的右子节点,变成问题节点的左节点!==
  2. ==然后将问题节点变成,问题节点原左节点的右节点!==
左右双旋
需要左右双旋的情况——重点

image-20230413151226803

如果是折线的右边高,我们发现依旧无法平衡!只是变成了一个左边高的折线了!虽然依旧是搜索树!但是还是无法平衡!

这时候单旋转依旧解决不了问题了!所以我们就必须用到==双旋转==了!

==下面是左右双旋的抽象图==

image-20230413151446371

为了方便理解我们还是画几个具体的图来

image-20230413153309691

image-20230413153536504

==所以我们可以判断出需要进行左右双旋的情况条件为==

if(parent->_bf == -2 && cur->_bf == 1)
左右双旋的操作——重点

image-20230413215842468

namespace MySTL
{
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
private:
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;//这个是重点!最后会是树的新根!
int old_bf = subLR->_bf;
RotateL(subL);
RotateR(parent);

//左右双旋的问题在于更新平衡因子!
if (old_bf == 1)//在右边插入
{
parent->_bf = 0;
subL->_bf = -1;

subLR->_bf = 0; //这个是最终旋转后是子树的新根!所以bf一定会为 0
}
else if(old_bf == -1)//左边插入!
{
parent->_bf = 1;
subL->_bf = 0;

subLR->_bf = 0;
}
else if(old_bf == 0)//这个节点就是新插入的节点!
{
parent->_bf = 0;
subL->_bf = 0;

subLR->_bf = 0;
}
else
{
assert(false);
}

}
private:
Node* _root = nullptr;

};
}

右左双旋
需要右左双旋的情况——重点

我们先看一下==右左双旋抽象图==

image-20230414112801548

有了上面的左右双旋!对于右左双旋我们就很好理解!

右左双旋的操作——重点

image-20230414112546290

namespace MySTL
{
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
private:
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;//这个是重点!最后会是树的新根!
int old_bf = subRL->_bf;
RotateR(subR);
RotateL(parent);

if (old_bf == 1)//在右边插入
{
subR->_bf = 0;
parent->_bf = -1;

subRL->_bf = 0;//!!!!!千万不要更新错误!博主就因为更新错了节点,检查了一个小时o(╥﹏╥)o
}
else if (old_bf == -1)//在左边插入!
{
subR->_bf = 1;
parent->_bf = 0;

subRL->_bf = 0;
}
else if (old_bf == 0)//该节点就是新增节点!
{
subR->_bf = 0;
parent->_bf = 0;

subRL->_bf = 0;
}
else
{
assert(false);//说明其他地方出问题了!
}
}
private:
Node* _root = nullptr;

};
}

==右左双旋也要注意平衡因子的更新!==

==同样是分三种情况!==

image-20230414113733568

insert总代码
#pragma once
#include<iostream>
#include<cassert>
#include<algorithm>
namespace MySTL
{
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode(const std::pair<K,V>& kv)
:_kv(kv)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
{}
std::pair<K, V> _kv;
AVLTreeNode* _left;
AVLTreeNode* _right;
AVLTreeNode* _parent;
int _bf;//balance factory
};
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
bool insert(const std::pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
parent = cur;
if (kv.first > cur->_kv.first)
{
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(kv);
if (cur->_kv.first > parent->_kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
///////////////////////////////////////////////
//更新平衡因子
while (parent)//parent为空就结束!因为根节点的parent就是nullptr
{
if (cur == parent->_left)
parent->_bf--;
else if (cur == parent->_right)
parent->_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);
}
else
{
assert(false);
}
break;
//旋转完后高度不变就不用继续更新了!

}
else
{
assert(false);//如果到意味着在我们插入更新树之前就出问题了!
}
}
return true;
}
private:
void RotateL(Node* parent)//左单旋
{
Node* subR = parent->_right;//父节点的右子节点
Node* subRL = subR->_left;//右子节点的左子节点

//让右孩子的左节点成为parent的右节点!
parent->_right = subRL;

//有3个节点_parent需要进行更新!
//以及一个节点的子节点需要进行更新!
if (subRL)
subRL->_parent = parent;//如果右子节点的左子节点不是一个空节点!那么它的parent也要更新!

Node* ppNode = parent->_parent;
if (ppNode) //如果这个颗树是一个子树!那么要跟更新parent的父节点的子节点!
{
if (ppNode->_left == parent)
ppNode->_left = subR;
else
ppNode->_right = subR;
}
else
{
_root = subR;//如果不是一个子树,那么subR此时就是根!
}
//更新subR的父节点!
subR->_parent = ppNode;

//让parent 成为右子节点的左节点
subR->_left = parent;

//此时parent是subR的左节点!
parent->_parent = subR;

//更新平衡因子!
subR->_bf = parent->_bf = 0;

}

void RotateR(Node* parent)
{
Node* subL = parent->_left;//父节点的左子节点
Node* subLR = subL->_right;//父节点的左子节点的右子节点

//将父节点的左子节点的右子节点变成父节点的新左子节点!
parent->_left = subLR;

//将父节点的原左子节点的右子节点的父节点进行更新为parent
if (subLR)
subLR->_parent = parent;

//更新parent的父节点该指向的新节点!
Node* ppNode = parent->_parent;
if (ppNode)
{
//改树是一个子树
//将ppNode指向新的子树根节点!
if (ppNode->_left == parent)
ppNode->_left = subL;
else
ppNode->_right = subL;
}
else
{
//如果不是子树,直接更新,新的根节点
_root = subL;
}
//更新原父节点的左子节点的父节点
subL->_parent = ppNode;

//将父节点变成原父节点的左子节点的右节点!
subL->_right = parent;
//将父节点的父节点进行更新
parent->_parent = subL;

//更新平衡因子
subL->_bf = parent->_bf = 0;
}

void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;//这个是重点!最后会是树的新根!
int old_bf = subLR->_bf;
RotateL(subL);
RotateR(parent);

//左右双旋的问题在于更新平衡因子!
if (old_bf == 1)//在右边插入
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0; //这个是最终旋转后是子树的新根!所以bf一定会为 0
}
else if(old_bf == -1)//左边插入!
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else if(old_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 old_bf = subRL->_bf;
RotateR(subR);
RotateL(parent);

if (old_bf == 1)//在右边插入
{
subR->_bf = 0;
parent->_bf = -1;

subRL->_bf = 0;
}
else if (old_bf == -1)
{
subR->_bf = 1;
parent->_bf = 0;

subRL->_bf = 0;
}
else if(old_bf == 0)
{
subR->_bf = 0;
parent->_bf = 0;

subRL->_bf = 0;
}
else
{
assert(false);
}
}

private:
Node* _root = nullptr;


};
}

avl树测试

对于avl的平衡测试!我们可以采用如下代码

#pragma once
#include<iostream>
#include<cassert>
#include<algorithm>
namespace MySTL
{
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
bool Isbalance()
{
return _Isbalace(_root);
}
private:
int Height(Node* root)
{
if (root == nullptr)
return 0;
int left = Height(root->_left) + 1;
int right = Height(root->_right) + 1;

return std::max(left, right);
}
bool _Isbalace(Node* root)
{
if (root == nullptr)
return true;

int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);

if (rightHeight -leftHeight!= root->_bf)
{
cout << 该平衡因子异常: << root->_kv.first << endl;
}
return std::abs(leftHeight - rightHeight) < 2
&& _Isbalace(root->_left)
&& _Isbalace(root->_right);
}
private:
Node* _root = nullptr;
};
}

AVL树的删除

因为AVL树也是二叉搜索树,==可按照二叉搜索树的方式将节点删除==,然后==再更新平衡因子==,只不过与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。

image-20230417101706922

==如果兴趣读者可以选择自己去实现!要但是要记住删除的旋转后的平衡因子要进行重新调整!==

AVL树的性能

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

但是如果要对AVL树做一些结构修改的操 作,性能非常低下==,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时, 有可能一直要让旋转持续到根的位置。==

因此:==如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。==

举报

相关推荐

0 条评论