AVL树
引言
在之前的文章中,介绍了二叉搜索树,它可以实现最快O(logN)
的搜索时间复杂度:
戳我看二叉搜索树详解哦
但是当二叉搜索树的左右子树高度不平衡时,如果要查找的数据在较高的子树中,那么查找的效率就会降低。在极端情况下就像是在单链表中查找,搜索的时间复杂度就成了O(N)
:
如果能有某种特性的存在,使得二叉搜索树的左右子树始终是平衡的,即左右子树几乎等高。那么在搜索时的效率就会稳定在接近O(logN)
的值上。具有这样的某种特性的二叉搜索树称为平衡二叉树:
AVL树就是平衡二叉树的一种:
AVL树介绍
AVL树在满足二叉搜索树条件的基础上,给每个结点中增加了一个平衡因子(这个平衡因子的值是该结点左右子树的高度差);
AVL树要求平衡因子的绝对值小于等于1,即任意一个结点左右子树的高度差不超过1。 通过降低树的高度,从而达到减少平均搜索时间的效果(在这里做例子的平衡二叉树是按照大小次序依次插入的结果)
:
AVL树的左右子树的高度差不超过1,是一种高度平衡的二叉树,平均搜索时间可以达到O(logN)
。
Key与Key-Value
根据搜索时的形式的不同可以分为K
与K-V
两种:
在K
模式中,只有K这一种类型的数据,在建树时根据key
的大小来创建,搜索时也根据key
的大小来搜索,搜索到时就获取key
这一种数据。适用于判断某段数据中存不存在某个值:
在K-V
模式中,有两种类型的数据:建树与搜索时以key
为依据,通过搜索到的key
可以访问到与其在一起存储的值value
。相当于key
与value
之间建立有一个映射关系。应用范围广泛,例如字典中,首先搜索输入的单词(key
)在字典中是否存在,再通过找到的单词访问到其对应的翻译(value
):
AVL树模拟实现
对于AVL树的实现,与之前的二叉搜索树不同的就是增加了平衡因子。需要在插入的过程中保持平衡因子的合适,就需要对树的形状做出调整。在本篇文章中介绍的是K-V
类型的模式(使用非递归方式):
为防止命名冲突,将实现放在我们的命名空间qqq
中:
AVLTree
是一个类模板,有两个模板参数,即K
与V
,表示key
的类型与value
的类型;
成员变量就是根结点的指针:
namespace qqq
{
template<class K, class V>
class AVLTree
{
protected:
Node* _root = nullptr; //Node是根节点类型typedefe来的
};
}
结点类
接下来来对结点的类型进行实现:
AVLTreeNode
是也是一个类模板,模板参数为K
与V
;
在结点中储存数据的结构为pair
,其中first
为K
类型,second
为V
类型;
成员变量有数据_kv
;
指向父结点的指针_parent
;
指向左右孩子结点的指针_left
、_right
;
以及平衡因子_bf
:
(由于在AVL树中需要经常访问结点的成员变量,所以结点类使用struct
类,其成员默认为public
)
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv;
AVLTreeNode<K, V>* _parent;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
int _bf;
};
结点类的构造函数
结点类构造函数的参数类型为const pair<K, V>
,缺省参数为其默认构造的匿名对象;
在初始化列表中对各个成员变量置空即可:
AVLTreeNode(const pair<K, V> kv = pair<K, V>())
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
insert
在AVL树中插入时:
- 首先自上而下搜索插入位置;
- 找到插入位置后将新结点链接到树上;
- 然后自下而上修改平衡因子;
- 如果遇到不平衡的情况就进行旋转调平衡:
搜索插入位置
- 搜索插入位置的部分与二叉搜索树时类似,向下搜索:
- 首先定义一个结点指针
cur
向下遍历,另外再使用一个结点指针parent
来记录它的父结点,方便找到位置后插入; while
循环向下遍历,当cur
为空时,即找到插入位置,此时将新结点链接到parent
下即可:
// 部分代码 :向下搜索插入位置//
while (cur != nullptr) //搜索
{
if (newnode->_kv.first > cur->_kv.first)
{
parent = cur;
cur = parent->_right;
}
else if (newnode->_kv.first < cur->_kv.first)
{
parent = cur;
cur = parent->_left;
}
else //相等即插入失败
{
return false;
}
}
当找到位置后,还需要另作判断
- 当
parent
为空时,说明当前插入的结点是树的第一个结点,直接将newnode
赋值给_root
即可; - 若不为空,还需要通过比较
parent->first
与newnode->first
的值来确定要链接parent
的左结点还是右结点; - 链接即使
parent
的左或右指针指向newnode
,newnode
的父指针指向parent
; - 最后令
cur
等于newnode
:
// 部分代码 :判断newnode要链接parent的那个结点 //
if (parent == nullptr) //插入
{
_root = newnode;
}
else if (newnode->_kv.first < parent->_kv.first)
{
parent->_left = newnode;
newnode->_parent = parent;
cur = newnode;
}
else
{
parent->_right = newnode;
newnode->_parent = parent;
cur = newnode;
}
然后就需要对平衡因子进行修改
- 现在新结点即
cur
的平衡因子自然是0,所以直接从parent
开始循环向上修改; - 判断,若
cur
插入在了parent
的左边,parent
的平衡因子 -1,反之 +1 :
// 部分代码 :进行当前位置的修改平衡因子 //
if (cur == parent->_left) //更新parent的平衡因子
{
--parent->_bf;
}
else
{
++parent->_bf;
}
修改一次后,对 parent 的平衡因子进行判断
- 若
parent
的平衡因子为0,说明parent
这棵子树已经平衡,不会再对上面的结点有所影响,所以直接break
; - 若
parent
的平衡因子为-1或1,说明还不平衡,将cur
与parent
都向上移动继续修改平衡因子; - 若
parent
的平衡因子为-2或2,说明这棵树的平衡因子已经不合理,需要进行旋转调平衡;
调平后直接break即可:
// 部分代码 :对修改后的parent进行判断 //
if (parent->_bf == 0) //parent平衡因子为0,即向上调整结束
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1) //继续向上调整
{
cur = parent;
parent = cur->_parent;
}
else if (parent->_bf == -2 || parent->_bf == 2) //旋转降低平衡因子
{
//不合理,进行旋转调平
break;
}
else
{
assert(0);
}
- 最后,当parent的值为空时,即整棵树的平衡因子都已经修改完成,退出循环即可。
insert整体代码
对于调整平衡这部分代码,在下面的旋转部分会详细介绍
bool insert(const pair<K, V>& kv)
{
//先插入(搜索插入)
Node* newnode = new Node(kv);
Node* parent = nullptr;
Node* cur = _root;
while (cur != nullptr) //搜索
{
if (newnode->_kv.first > cur->_kv.first)
{
parent = cur;
cur = parent->_right;
}
else if (newnode->_kv.first < cur->_kv.first)
{
parent = cur;
cur = parent->_left;
}
else //相等即插入失败
{
return false;
}
}
if (parent == nullptr) //插入
{
_root = newnode;
}
else if (newnode->_kv.first < parent->_kv.first)
{
parent->_left = newnode;
newnode->_parent = parent;
cur = newnode;
}
else
{
parent->_right = newnode;
newnode->_parent = parent;
cur = newnode;
}
//向上调整平衡因子
while (parent != nullptr)
{
if (cur == parent->_left) //更新parent的平衡因子
{
--parent->_bf;
}
else
{
++parent->_bf;
}
if (parent->_bf == 0) //parent平衡因子为0,即向上调整结束
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1) //继续向上调整
{
cur = parent;
parent = cur->_parent;
}
else if (parent->_bf == -2 || parent->_bf == 2) //旋转降低平衡因子
{
if (parent->_bf == 2 && cur->_bf == 1) //右右——单次左旋parent
{
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1) //左左——单次右旋parent
{
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1) //右左——先右旋cur再左旋parent
{
RotateRL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)//左右——先左旋cur再右旋parent
{
RotateLR(parent);
}
else //兜底
{
assert(0);
}
break;
}
else
{
assert(0);
}
}
return true;
}
旋转调平衡
旋转调平衡既要保证调整之后树平衡,有要保证搜索树的特征不变。根本目的就是要降低树的整体高度。
在调平衡的情况中,共有四种情况:
右右——左旋
第一种情况为:parent
的右子树高度比左子树高,在其右子树中又有右子树较高。我们用gparent
、parent
、cur
、curleft
来记录这些结点:
需要注意的是,当parent
不平衡为右右时,一定存在上面的关系:cur
的右子树高度为h + 1
,cur
左子树的高度为h
,parent
左子树的高度也一定是h
:
若cur
右子树的高度大于h + 1
,则不平衡的结点应该为cur
;
若cur
右子树的高度为h
, 则cur
平衡因子为0,parent
就不会不平衡;
若parent
左子树的高度大于h
,则parent
就不会不平衡;
若parent
左子树的高度小于h
,则在插入之前此树就已经不平衡了。
在这种情况下,parent
的平衡因子为2,cur
的平衡因子为1(该结论用于在insert
中快速判断为哪种情况)
这种不平衡情况的解决方式为单次左旋:
- 首先将
curleft
与parent
的右指针建立链接,curleft
一定比parent
大; - 然后将
parent
与cur
的左指针建立链接,parent
及curleft
一定比cur
小; - 最后再将
cur
与parent
的父结点建立链接(当然,这需要判断cur
应该为gparent
的左或右子树),若parent
就是_root
,gparent
就是nullptr
,这步就变成了将cur
赋值给_root
;
- 调平后,将
parent
与cur
的平衡因子全部修改为0即可:
void RotateL(Node* parent)
{
Node* gparent = parent->_parent;
Node* cur = parent->_right;
Node* curleft = cur->_left;
parent->_right = curleft;
if (curleft != nullptr)
{
curleft->_parent = parent;
}
cur->_left = parent;
parent->_parent = cur;
cur->_parent = gparent;
if (gparent == nullptr)
{
_root = cur;
}
else
{
if (cur->_kv.first < gparent->_kv.first)
{
gparent->_left = cur;
}
else
{
gparent->_right = cur;
}
}
parent->_bf = cur->_bf = 0;
}
左左——右旋
第二种情况为:parent
的左子树高度比右子树高,在其左子树中又有左子树较高。我们用gparent
、parent
、cur
、curright
来记录这些结点:
与上面的情况正好相反,这种左左的情况也一定满足上图的关系:cur
的左子树高度为h + 1
,cur
右子树的高度为h
,parent
右子树的高度也一定是h
。这里就不赘述了。
在这种情况下,parent
的平衡因子为 -2,cur
的平衡因子为 -1
解决这种不平衡的方法为单次右旋(与单次左旋相反):
- 首先将
curright
与parent
的左指针建立链接,curright
一定比parent
大; - 然后将
parent
与cur
的右指针建立链接,parent
及curright
一定比cur
大; - 最后再将
cur
与parent
的父结点建立链接(当然,这需要判断cur
应该为gparent
的左或右子树),若parent
就是_root
,gparent
就是nullptr
,这步就变成了将cur
赋值给_root
:
- 调平后,将
parent
与cur
的平衡因子全部修改为0即可:
void RotateR(Node* parent)
{
Node* gparent = parent->_parent;
Node* cur = parent->_left;
Node* curright = cur->_right;
parent->_left = curright;
if (curright != nullptr)
{
curright->_parent = parent;
}
cur->_right = parent;
parent->_parent = cur;
cur->_parent = gparent;
if (gparent == nullptr)
{
_root = cur;
}
else
{
if (cur->_kv.first < gparent->_kv.first)
{
gparent->_left = cur;
}
else
{
gparent->_right = cur;
}
}
parent->_bf = cur->_bf = 0;
}
右左——右左双旋
第三种情况为:parent
的右子树高度比左子树高,在其右子树中又有左子树较高。我们用gparent
、parent
、cur
、curleft
来记录这些结点:
在这种情况中,树的高度一定满足图示的关系:cur
的左子树高度为h + 1
,cur
右子树的高度为h
,parent
左子树的高度也一定是h
:
若cur
左子树的高度大于h + 1
,就应该是cur
不平衡;
若cur
左子树的高度等于h
,parent
就不会不平衡,平衡因子为0;
若parent
左子树的高度大于h
,parent
就不会不平衡;
若parent
左子树的高度小于h
,则在插入之前此树就已经不平衡了。
在这种情况下,parent
的平衡因子为2,cur
的平衡因子为 -1
单次旋转已经不足以将这种情况调整平衡了,需要进行两次旋转,即左右双旋:
首先认为cur
不平衡,对cur
进行一次右单旋。但是在进行右单旋时,curleft
的左右子树高度不确定,可能有三种情况:
在对cur
进行右单旋后的状态就类似于前面的右右,通过一次左单旋即可平衡;
然后再对parent
进行一次左单旋(具体单旋的步骤就不赘述了):
旋转调平后,我们再来总结规律:
右左双旋最后的结果为:curleft
做根结点,parent
做左子结点,cur
做右子节点。curleft
的左子树做parent
的右子树,curleft
的右子树做cur
的左子树。
而上面提到的,curleft
的左右子树高度包含三种情况即(h,h)
、(h-1,h)
、(h,h-1)
。这三种情况也决定着右左旋转后parent
与cur
的平衡因子的值(curleft
的平衡因子为0是一定的):分别对应(0,0)
、(-1,0)
、(0,1)
。需要在旋转后进行判断赋值:
void RotateRL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
int bf = curleft->_bf;
RotateR(cur);
RotateL(parent);
//根据curleft的平衡因子对parent与cur的平衡因子进行赋值
if (bf == 0)
{
curleft->_bf = 0;
cur->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
curleft->_bf = 0;
cur->_bf = 1;
parent->_bf = 0;
}
else if(bf == 1)
{
curleft->_bf = 0;
cur->_bf = 0;
parent->_bf = -1;
}
else
{
assert(0);
}
}
左右——左右双旋
第四种情况为:parent
的左子树高度比右子树高,在其左子树中又有右子树较高。我们用gparent
、parent
、cur
、curright
来记录这些结点:
与上面右左的情况相反,这种情况下一定满足上图关系:cur
的右子树高度为h + 1
,cur
左子树的高度为h
,parent
右子树的高度也一定是h
,这里就不赘述了。
在这种情况下,parent
的平衡因子为 -2,cur
的平衡因子为 1
与上面情况相反,需要进行左右双旋来调整平衡:
首先认为cur
不平衡,对cur
进行一次左单旋。当然,这里的左单旋也有三种情况:
然后再对parent
进行一次右单旋:
左右双旋总结:
与右左双旋相反,左右双旋最后的结果为:curright
做根结点,parent
做右子结点,cur
做左子节点。curright
的右子树做parent
的左子树,curright
的左子树做cur
的右子树。
同时,curright
的左右子树高度包含三种情况即(h,h)
、(h-1,h)
、(h,h-1)
。这三种情况也决定着左右旋转后cur
与parent
的平衡因子的值(curright
的平衡因子为0是一定的):分别对应(0,0)
、(-1,0)
、(0,1)
。需要在旋转后进行判断赋值:
void RotateLR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
int bf = curright->_bf;
RotateL(cur);
RotateR(parent);
if (bf == 0)
{
curright->_bf = 0;
cur->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
curright->_bf = 0;
cur->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
curright->_bf = 0;
cur->_bf = -1;
parent->_bf = 0;
}
else
{
assert(0);
}
}
在完全了解不平衡的条件与解决方案后,我们就可以补全上面insert
的不平衡时的种类判断以及旋转调平衡的代码了(在前面insert
部分以及后面的源码部分都有展示,这里就不再写了)。
源码
namespace qqq
{
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv;
AVLTreeNode<K, V>* _parent;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
int _bf;
AVLTreeNode(const pair<K, V> kv = pair<K, V>())
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
bool insert(const pair<K, V>& kv)
{
//先插入(搜索插入)
Node* newnode = new Node(kv);
Node* parent = nullptr;
Node* cur = _root;
while (cur != nullptr) //搜索
{
if (newnode->_kv.first > cur->_kv.first)
{
parent = cur;
cur = parent->_right;
}
else if (newnode->_kv.first < cur->_kv.first)
{
parent = cur;
cur = parent->_left;
}
else //相等即插入失败
{
return false;
}
}
if (parent == nullptr) //插入
{
_root = newnode;
}
else if (newnode->_kv.first < parent->_kv.first)
{
parent->_left = newnode;
newnode->_parent = parent;
cur = newnode;
}
else
{
parent->_right = newnode;
newnode->_parent = parent;
cur = newnode;
}
//向上调整平衡因子
while (parent != nullptr)
{
if (cur == parent->_left) //更新parent的平衡因子
{
--parent->_bf;
}
else
{
++parent->_bf;
}
if (parent->_bf == 0) //parent平衡因子为0,即向上调整结束
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1) //继续向上调整
{
cur = parent;
parent = cur->_parent;
}
else if (parent->_bf == -2 || parent->_bf == 2) //旋转降低平衡因子
{
if (parent->_bf == 2 && cur->_bf == 1) //右右——单次左旋parent
{
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1) //左左——单次右旋parent
{
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1) //右左——先右旋cur再左旋parent
{
RotateRL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)//左右——先左旋cur再右旋parent
{
RotateLR(parent);
}
else //兜底
{
assert(0);
}
break;
}
else
{
assert(0);
}
}
return true;
}
void RotateL(Node* parent)
{
Node* gparent = parent->_parent;
Node* cur = parent->_right;
Node* curleft = cur->_left;
parent->_right = curleft;
if (curleft != nullptr)
{
curleft->_parent = parent;
}
cur->_left = parent;
parent->_parent = cur;
cur->_parent = gparent;
if (gparent == nullptr)
{
_root = cur;
}
else
{
if (cur->_kv.first < gparent->_kv.first)
{
gparent->_left = cur;
}
else
{
gparent->_right = cur;
}
}
parent->_bf = cur->_bf = 0;
}
void RotateR(Node* parent)
{
Node* gparent = parent->_parent;
Node* cur = parent->_left;
Node* curright = cur->_right;
parent->_left = curright;
if (curright != nullptr)
{
curright->_parent = parent;
}
cur->_right = parent;
parent->_parent = cur;
cur->_parent = gparent;
if (gparent == nullptr)
{
_root = cur;
}
else
{
if (cur->_kv.first < gparent->_kv.first)
{
gparent->_left = cur;
}
else
{
gparent->_right = cur;
}
}
parent->_bf = cur->_bf = 0;
}
void RotateRL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
int bf = curleft->_bf;
RotateR(cur);
RotateL(parent);
if (bf == 0)
{
curleft->_bf = 0;
cur->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
curleft->_bf = 0;
cur->_bf = 1;
parent->_bf = 0;
}
else if(bf == 1)
{
curleft->_bf = 0;
cur->_bf = 0;
parent->_bf = -1;
}
else
{
assert(0);
}
}
void RotateLR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
int bf = curright->_bf;
RotateL(cur);
RotateR(parent);
if (bf == 0)
{
curright->_bf = 0;
cur->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
curright->_bf = 0;
cur->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
curright->_bf = 0;
cur->_bf = -1;
parent->_bf = 0;
}
else
{
assert(0);
}
}
bool isAVLTree()
{
return isBalance(_root);
}
protected:
Node* _root = nullptr;
//这两个函数将用于测试AVL树是否平衡
int height(Node* root)
{
if (root == nullptr)
{
return 0;
}
int leftheight = height(root->_left);
int rightheight = height(root->_right);
return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}
bool isBalance(Node* root)
{
if (root == nullptr)
{
return true;
}
int leftheight = height(root->_left);
int rightheight = height(root->_right);
return abs(rightheight - leftheight) < 2 && isBalance(root->_left) && isBalance(root->_right);
}
};
}
测试
最后,我们来测试一下我们的AVL树是否合格。
这时我们需要两段测试代码:height
用于测量树的高度、isBalance
用于判断树是否平衡。这两段代码都是递归实现的,体现了分治的思想,在前面C语言部分有介绍:戳我看二叉树练习题哦(二叉树的最大深度)
测试的代码在上面的源码部分有展示,这里就只演示一下效果:
namespace qqq
{
void testAVLfunc() //测试用例,使用10000000个随机数
{
const int N = 10000000;
std::vector<int> v;
v.reserve(N);
srand(time(nullptr));
for (size_t i = 0; i < N; i++)
{
v.push_back(rand()); //生成随机数
}
AVLTree<int, int> avl;
for (auto e : v)
{
avl.insert(make_pair(e, e));
cout << "Insert:" << e << "->" << avl.isAVLTree() << endl;
}
cout << avl.isAVLTree() << endl;
}
}
int main()
{
qqq::testAVLfunc();
return 0;
}
这个代码需要跑很久,结果当然是没问题的:
总结
到此,关于AVL树的介绍就结束了
AVL树是高度平衡的,这就导致在插入的时候,会有大量的时间用在了调平衡上,有些得不偿失。在下一篇文章中将介绍另一种平衡二叉树——红黑树。它的平衡度没有那么高,带来了一些效率的提升
如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出
如果本文对你有帮助,希望一键三连哦
希望与大家共同进步哦