目录
之前我们已经学习了搜索二叉树,我们已经知道了搜索二叉树在插入有序的元素时,最终会退化为链表结构,导致查找元素的效率变低,所以我们引申出了平衡搜索二叉树,平衡搜索二叉树有两种,一种是通过高度平衡,一种是通过颜色来平衡。本期我们学习的是基于高度来进行平衡的平衡搜索二叉树------AVL树。
AVL树的概念
AVL树是由俄罗斯科学家G. M. Adelson-Velsky和E. M. Landis发明的一种以高度进行平衡的平衡搜索二叉树。
AVL树具有以下特点:
- AVL树的任意一个子树都是AVL树。
- AVL树的任意一个节点的平衡因子的绝对值不超过1(-1,0,1)。
何为平衡因子,平衡因子就是当前节点的所在的树的右子树的高度-左子树高度所得的值。
AVL树的结构如下。
template<class k,class v>
struct AVLTreeNode
{
AVLTreeNode<k, v>* _left;
AVLTreeNode<k, v>* _right;
AVLTreeNode<k, v>* _parent;
pair<k, v> _pair;
//平衡因子
int _bf;
//构造函数进行初始化
AVLTreeNode(const pair<k, v>& pair)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{
_pair = pair;
}
};
template<class k,class v>
class AVLTree
{
typedef AVLTreeNode<k, v> Node;
public:
AVLTree()
{
_root = nullptr;
}
private:
Node*_root;
};
AVL树的操作
元素的插入
在AVL树中元素的插入分为三步:
- 与普通搜索二叉树的插入一样,先找到合适的位置,然后将元素插入。
- 更新平衡因子。
- 根据平衡因子进行AVL树的旋转,保证整个树符合AVL的特点。至于旋转有几种,我们下面会讲到。
查找合适的位置
if (_root == nullptr)
{
_root = new Node(pair);
return true;
}
//1.找到合适的位置将元素插入
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if ( pair.first<cur->_pair.first )
{
parent = cur;
cur = cur->_left;
}
else if (cur->_pair.first < pair.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
//插入当前元素
cur = new Node(pair);
if (pair.first < parent->_pair.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
更新平衡因子和AVL树的旋转操作
while(parent)
{
//2.平衡因子的更新
if (cur == parent->_left)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
//3.根据平衡因子,判断当前节点所在的树需不需要进行旋转,1,-1,0不用旋转,2,-2要进行旋转
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)
{
RRotate(parent);
}
else if (parent->_bf == 2 && cur->_bf == 1)
{
LRotate(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RLRotate(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
LRRotate(parent);
}
else
{
assert(false);
}
break;
}
else
{
assert(false);
}
}
- 如果左边都比右边高,就进行右单旋。
- 如果右边都比左边高,就进行左单旋。
- 如果从下往上看,cur是左边比右边高,parent右边比左边高,就进行右左双旋。
- 如果从下往上看,cur是右边比左边高,parent左边比右边高,就进行左右双旋。
AVL树的旋转分类
右单旋
右旋的示意图如下。
代码如下。
//右旋
void RRotate(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* parentparent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
if (parent == _root)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
if (parent == parentparent->_left)
{
parentparent->_left=subL;
subL->_parent = parentparent;
}
else
{
parentparent->_right=subL;
sub->_parent = parent->_parent;
}
}
subL->_bf = parent->_bf = 0;
}
左单旋
左单旋示意图如下。
代码如下。
//左单旋
void LRotate(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* parentparent = parent->_parent;
parent->_right = subRL;
if (subRL)
{
subRL->_parent = parent;
}
subR->_left = parent;
parent->_parent = subR;
if (parent == parentparent->_left)
{
parentparent->_left = subR;
subR->_parent = parentparent;
}
else
{
parentparent->_right = subR;
subR->_parent = parentparent;
}
parent->_bf = subR->_bf = 0;
}
左右双旋
左右双旋示意图如下。
对于左右双旋而言,平衡因子的更新使我们要格外注意的,因为双旋是基于两个单旋的,但是单旋之后,两个节点的平衡因子是全部变成0的,但是实际上,元素插入位置的不同,最终单旋之后的平衡因子并不一定是0,这是要根据插入的位置进行更改的。图示如下。
代码如下。
void LRRotate(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
LRotate(subL);
RRotate(parent);
if (subLR->_bf == -1)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 1;
}
else if (subLR->_bf == 1)
{
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
else
{
subLR->_bf = subL->_bf = parent->_bf = 0;
}
else
{
assert(false);
}
}
右左双旋
右左双旋示意图如下。
同样的,对于右左双旋而言,平衡因子的更新如左右双旋一样,也是需要在插入的元素的位置不同时,进行修改的。示意图如下。
右左双旋代码。
//右左双旋
void RLRotate(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//这里要用刚开始的平衡因子,在进行左右双旋之后,平衡因子会发生变化
int bf = subRL->_bf;
RRotate(subR);
LRotate(parent);
if (bf == 1)
{
subRL->_bf = 0;
parent->_bf = -1;
subR->_bf = 0;
}
else if (bf == -1)
{
subRL->_bf = 0;
parent->_bf = 0;
subR->_bf = 1;
}
else if (bf == 0)
{
subRL->_bf = subR->_bf = parent->_bf = 0;
}
else
{
assert(false);
}
}
求AVL树的高度
int height()
{
_height(_root);
}
int _height(Node* root)
{
if (root == nullptr)
{
return 0;
}
else
{
int LeftHeight = _height(root->_left);
int RightHeight = _height(root->_right);
return (LeftHeight > RightHeight) ? LeftHeight + 1 : RightHeight + 1;
}
}
与之前求二叉树的高度的代码类似,即求出左右子树的高度,整个二叉树的高度就是左右子树中较大的+1。
判断AVL树是否是平衡树
bool IsBalance()
{
return _IsBalance(_root);
}
//判断AVL树是否是平衡树
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 << root->_pair.first << "现在是:" << root->_bf << endl;
cout << root->_pair.first << "应该是:" << RightHeight - LeftHeight << endl;
return false;
}
return abs(RightHeight - LeftHeight) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
判断AVL树是否是平衡树,即判断右子树的高度减去左子树的高度是否等于根节点的平衡因子的值。这里我们用到了子函数,因为在类外我们无法访问根节点。基于此情况,我们一般设置子函数来进行函数的设置,这也是之后进行编码的一种方法。
测试代码
void TestAVLTree()
{
AVLTree<int, int> t;
//int a[] = {5,4,3,2,1,0};
int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto e : a)
{
t.insert(make_pair(e,e));
cout << "Insert" << e << ":" << t.IsBalance() << endl;
}
t.InOrder();
cout << endl;
cout << t.IsBalance() << endl;
}
运行结果如下。
插入的元素在进行了右左双旋之后成为了AVL树,符合预期。
AVL树的性能
因为AVL树的特点保证了任意一个子树的左右高度差的绝对值不超过1,所以一般情况下,我们会认为AVL树就是一个完全二叉树,所以AVL树的高度我们就认为是logN,所以在查找元素时的事假复杂度就是O(logN)。所以,对于AVL树而言,查找一个元素的效率是很高的。
但是,在插入元素时,往往伴随着AVL树的旋转,有时候每插入一个元素就会进行一次旋转,所以旋转的次数较多。
基于此,如果是想创建一个查找的数据结构,推荐使用AVL树,但是如果对应的修改操作非常多,则不推荐使用AVL树。
以上便是AVL树的所有内容。
本期内容到此结束^_^