AVL树是对普通二叉搜索树的一种优化。当二叉搜索树插入的元素是有序的时候或者接近有序的时候,二叉搜索树的性能会大大降低。二叉搜索树可能会变成一个歪脖子树。比如下图:
关于二叉搜索树 之前博客有介绍。二叉搜索树(BST)
为了解决这个问题,俄罗斯两位数学家,G.M.Adelson-Velskii和E.M.Landis在1962年共同发明了一种解决平衡二叉树的算法。同时也根据两位数学家的名字命名为AVL树
AVL树
AVL树是一种叉排序树,其中每个结点的左子树和右子树高度差最多为1
AVL树要么是一颗空树,要么左子树和右子树都是AVL树,且左右子树的高度之差的绝对值不超过1
比如下面图1就不是AVL树,图二就是AVL树
图1 结点58左子树高度为3,右子树高度为0。左右子树的高度差为3,不合符AVL树的性质。
平衡因子
将二叉树中的右子树的高度减去左子树的高度的值称为平衡因子BF(balance factor)。(也可以是左子树减去右子树的高度)
如果一棵树是AVL树,那么这棵树上的所有结点的平衡因子只可能是 0、1、-1。
比如上面图1各结点的平衡因子如下:
这里以右子树减左子树作为BF为例
结点58的平衡因子是-3,很明显不是AVL树
图二各结点的平衡因子如下
各结点的平衡因子只有0、1、-1。所以这是棵AVL树。
AVL树结点的定义
这里直接实现KV模型的AVL树。用pair键值对作为树的元素值。为了后续实现更简单将AVL树定义为三叉链的结构(左子树,右子树,父结点)。并且在结点中引入平衡因子。
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode<K,V>* _left;//结点的左子树
AVLTreeNode<K,V>* _right;//结点的右子树
AVLTreeNode<K,V>* _parent;//结点的父结点
pair<K,V> _kv//结点元素(用键值对进行存储)
int _bf;//结点的平衡因子
AVLTreeNode(const pair<K,V>& kv)
:_left(nullptr)
, _right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_bf(0)
{}
};
AVL树类和函数接口
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
AVLTree()
:_root(nullptr)
{}
bool insert(const pair<K, V>& kv)
private:
Node* _root;
};
AVL树插入元素
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树的插入也可以看成是二叉搜索树的插入。那么 AVL树的插入过程可以分为两步:
- 按照二叉搜索树的方式进行插入
- 调整该结点的平衡因子
- 调整完该结点的平衡因子之后,如果出现不平衡(BF不等于0、1、-1),组要进行旋转处理,让继续成为AVL树。
- 调整完该结点的平衡因子之后,如果平衡因子依然是0、1、-1 ,则无需处理。
如何调整结点的平衡因子:
-
当插入的结点时当前结点的左孩子:让BF–(这里的BF以右减左为基准);
比如下面对62插入一个47结点。62的BF从0变为-1;
-
当插入的结点是当前结点的右孩子:让BF++;
比如对下面这颗树插入88,62的BF从-1变为0
如果插入完之后当前结点的FB为0。说明插入后这棵树左右子树高度已经平衡则无需处理。(说明之前一边高一边低,插入之后正好平衡);
比如下面对47结点插入51之后,结点47的BF从-1 变为 0 。刚好平衡无需处理。
如果插入完之后当前节点的BF为1或者-1。说明高度变了(之前的BF为0 已经平衡,插入之后一边高一边低),这时候需要更新父结点的BF。
比如对下面35结点插入30之后,35的BF从0变为-1, 35的父结点47也从0变为-1,根节点从-1变为了-2。此时就要更新自己的父结点以及祖宗结点,然后根据父结点或者祖宗结点的BF来判断下一步操作。
如果更新完父节点以及组宗结点的BF为2或者-2。则说明已经不是AVL树了。此时就需要进行旋转处理了。
比如上面插入30之后,祖宗结点的BF为-2;就需要进行旋转处理 使之平衡。如何旋转,后面会详细介绍。
下面先实现插入和调整平衡因子的代码
代码实现:
bool insert(const pair<K, V>& kv)
{
//空树,直接作为根节点
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
//按照二叉搜索树的插入进行插入
Node* cur = _root;
Node* parent = nullptr;
//确定插入结点的位值
while (cur != nullptr)
{
if ((kv.first) < (cur->_kv.first))//待插入结点Key小于当前结点的Key
{
parent = cur;
cur = cur->_left;
}
else if ((kv.first) > (cur->_kv.first))
{
parent = cur;
cur = cur->_right;
}
else//Key值冗余 不允许插入
{
return false;
}
}
//链接
cur = new Node(kv);
//确定插入结点的位置,在父结点的左边还是右边
if (kv.first < parent->_kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
//调整平衡因子
while (parent != nullptr)
{
//插入到右子树中将BF++
if (cur == parent->_right)
{
parent->_bf++;
}
//插入到左子树中 将BF--
else if (cur == parent->_left)
{
parent->_bf--;
}
//BF为0 说明已经平衡 无需处理
if (parent->_bf == 0)
{
break;
}
// BF 为1或者-1,说明高度变了,要继续更新父结点以及组织结点的BF
else if (parent->bf == 1 || parent->_bf == -1)
{
parent = parent->_parent;
cur = cur->_parent;
}
//已经不平衡了 进行旋转处理。
else if (parent->bf == 2 || parent->_bf == -2)
{
//旋转处理
}
// 如果出现其他情况,说明插入之前这棵树就不是AVL树
else
{
cout << "插入前不是AVL树" << endl;
}
}
return true;
}
最小不平衡子树
在进行旋转之前,先认识一下最小不平衡子树。
距离插入结点最近,且平衡因子绝对值大于1 的结点为根节点,称之为最小不平衡子树。比如下面新插入节点37,距离它最近平衡因子绝对值大于1的节点为58。以58为根节点称之为最小不平衡子树。
旋转
旋转的目的就是为了降低树的高度,让这棵树平衡成为AVL树
旋转一共有四种情况,分别是左单旋,右单旋,左右双旋,右左双旋。
下面一一介绍这四种情况
1. 左单旋(逆时针旋转)
当插入节点在右子树的右侧。
比如对下面抽象图进行插入结点(图中a、b、c、x、y都是高度为h的AVL树)
插入结点在右子树的右侧,此时以30为根结点称之为最小不平衡子树。需要对这颗最小不平衡子树进行左单旋处理 从而降低整棵树的高度,让继续成为AVL树。
注意: 这里虽然整颗树的根节点20的平衡因子也受到了影响,但是旋转是只用处理最小不平衡子树即可。不用
左单旋处理完之后,整颗树整棵树的仍然是AVL树
如何左单旋?
对上面一些关键结点进行命名。方便后续处理
- 让最小不平衡子树的subRL结点作为parent结点的右子树
- 让这颗最小不平衡子树的parnet做为subR的左子树
- 让subR做为这颗最小不平衡子树的根节点
- 最后更新平衡因子
代码实现:
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* parent_parent = parent->_parent;
//让subRL结点作为parent结点的右子树 更新完之后处理subRL_parent;
parent->_right = subRL;
if (subRL != nullptr)
{
subRL->_parent = parent;
}
//让parnet做为subR的左子树 更新完之后处理parent的_parent
subR->_left = parent;
parent->_parent = subR;
//subR做为这颗最小不平衡子树的根节点
if (parent_parent == nullptr)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (parent_parent->_left == parent)
{
parent_parent->_left = subR;
}
else
{
parent_parent->_right = subR;
}
subR->_parent = parent_parent;
}
//更新平衡因子
parent->_bf = subR->_bf = 0;
}
2. 右单旋(顺时针旋转)
当新插入节点在左子树的左侧。
比如对下面AVL树左子树左侧中插入新节点
此时以60为根节点为最下不平衡子树。
需要对这颗最小不平衡子树进行右单旋处理,从而降低整颗A树的高度,让继续成为AVL树。
右单旋之后,整颗树仍然是AVL树
如何右单旋?
对上面一些关键结点进行命名。方便后续处理
- 让这颗最小不平衡子树的subLR节点做为parent节点的左子树
- 让这颗最小不平衡子树的parent节点做为subL的右子树
- 让subL做为整颗最小不平衡子树的根节点
- 更新平衡因子
代码实现:
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* parent_parent = parent->_parent;
//让subLR节点做为parent节点的左子树 更新完之后处理subLR的_parent;
parent->_left = subLR;
if (subLR != nullptr)
{
subLR->_parent = parent;
}
//让parent节点做为subL的右子树 更新完之后处理parent的_parent
subL->_right = parent;
parent->_parent = subL;
//让这颗最小不平衡子树的parent节点做为subL的右子树
if (parent_parent == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent_parent->_left == parent)
{
parent_parent->_left = subL;
}
else
{
parent_parent->_right = subL;
}
subL->_parent = parent_parent;
}
//更新平衡因子
parent->_bf = subL->_bf = 0;
}
3. 左右双旋
新节点插入在较高左子树的右侧:先左单旋再右单旋
比如对下面AVL树插入一个新节点。
注意::这里较高左子树值指的是结点30。如果新节点插入在c树(不论左右)也会引发旋转。这里以新节点插入在b树下为例。
此时以90为根节点的称为最小不平衡子树。这种情况就需要对这棵树进行左右双旋处理,降低整颗树的高度,从而让这颗树继续成为AVL树。
先以最小不平衡子树的根节点左子树做为根进行左单旋:
- 让最小不平衡子树的subRL结点作为parent结点的右子树
- 让这颗最小不平衡子树的parnet做为subR的左子树
- 让subR做为这颗最小不平衡子树的根节点
- 最后更新平衡因子
再以最小不平衡子树的根节点做为根进行右单旋
- 让这颗最小不平衡子树的subLR节点做为parent节点的左子树
- 让这颗最小不平衡子树的parent节点做为subL的右子树
- 让subL做为整颗最小不平衡子树的根节点
- 更新平衡因子
双旋转完之后,整颗树变为下面这种情况。达到了旋转的目的。
注意:: 双旋完之后,要更新平衡因子。如何更新:
- 当新节点在b树下:
subLR双旋转前的bf为-1,双旋之后变为0,subL的bf变为0,parent的bf变为1
- 当新节点在c树下
subLR双旋转前的bf为1,双旋之后变为0,subL的bf变为-1,parent的bf变为0
- 当60就是新插入节点
subLR双旋转前的bf为0,双旋之后变为0,subL的bf变为0,parent的bf变为0
代码实现:
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
//左单旋
RotateL(parent->_left);
//右单旋
RotateR(parent);
//更新平衡因子
if (bf == 1)
{
parent->_bf = 0;
subLR->_bf = 0;
subL->_bf = -1;
}
else if(bf == -1)
{
parent->_bf = 1;
subLR->_bf = 0;
subL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subLR->_bf = 0;
subL->_bf = 0;
}
else
{
assert(false);
}
}
4. 右左双旋
新节点插入较高右子树的左侧:先右单旋再左单旋
比如对下面AVL树插入一个新节点:
注意: 这里较高右子树指的是90,如果新节点插入在b树(不论左右)也会引发旋转。这里以新节点插入在c树下为例。
此时以30为根节点称为最小不平衡子树。这种情况就需要对这颗子树进行旋转处理,降低整棵树的高度,让继续成为AVL树。
先让最小不平衡子树根节点的右子树90做为根节点进行右单旋
- 让这颗最小不平衡子树的subLR节点做为parent节点的左子树
- 让这颗最小不平衡子树的parent节点做为subL的右子树
- 让subL做为整颗最小不平衡子树的根节点
- 更新平衡因子
再以最小不平衡子树的根节点做为根进行左单旋
- 让最小不平衡子树的subRL结点作为parent结点的右子树
- 让这颗最小不平衡子树的parnet做为subR的左子树
- 让subR做为这颗最小不平衡子树的根节点
- 最后更新平衡因子
双旋完之后,整颗树变为下面这种情况:
注意: 这里和左右双旋一样,要更新BF。
- 当新节点在c树下
subRL双旋之前bf为1,双旋之后,subRL的平衡因子变为0,parent的平衡因子变为-1,subR的平衡因子变为0
- 当新节点在b树下
subRL双旋之前bf为-1,双旋之后,subRL的平衡因子变为0,parent的平衡因子变为0,subR的平衡因子变为1
- 当60就是新节点
subRL双旋之前bf为0,双旋之后,subRL的平衡因子变为0,parent的平衡因子变为0,subR的平衡因子变为0
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
//以最小不平衡子树的右结点进行右单旋
RotateR(parent->_right);
//左单旋
RotateL(parent);
if (bf == 1)
{
subR->_bf = 0;
parent->_bf = -1;
subRL->_bf = 0;
}
else if (bf == -1)
{
subR->_bf = 1;
parent->_bf = 0;
subRL->_bf = 0;
}
else if (bf == 0)
{
subR->_bf = 0;
parent->_bf = 0;
subRL->_bf = 0;
}
else
{
assert(false);
}
}
关于旋转的逻辑全部写完,下面就是什么什么时候要进行左单旋,什么时候要右单旋,以及什么时候双旋
通过上面画的图可以看出:
- 当平衡因子都为正的时候,需要进行左单旋,
- 当平衡因子都为负的时候,需要进行右单旋。
- 当父结点的平衡因子为父且当前节点平衡因子为正时,需要进行左右双旋,先左单旋,让平衡因子都为负,再进行右单旋,从而达到旋转的目的
- 当父结点的平衡因子为正,当前节点的平衡因子为负的时候,需要进行右左双。先右单旋,让平衡因子都为正,再进行左单旋,从而达到旋转的目的
注意: 要旋转时的父节点的平衡因子只能为2或-2,当前节点的平衡因子只能为1或-1。如果出现别的情况,说明插入前这棵树就不是AVL树。
整个插入代码为
bool insert(const pair<K, V>& kv)
{
//空树,直接作为根节点
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
//按照二叉搜索树的插入进行插入
Node* cur = _root;
Node* parent = nullptr;
//确定插入结点的位值
while (cur)
{
if ((kv.first) < (cur->_kv.first))//待插入结点Key小于当前结点的Key
{
parent = cur;
cur = cur->_left;
}
else if ((kv.first) > (cur->_kv.first))
{
parent = cur;
cur = cur->_right;
}
else//Key值冗余 不允许插入
{
return false;
}
}
//链接
cur = new Node(kv);
//确定插入结点的位置,在父结点的左边还是右边
if (kv.first < parent->_kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
//调整平衡因子
while (parent != nullptr)
{
//插入到右子树中将BF++
if (cur == parent->_right)
{
parent->_bf++;
}
//插入到左子树中 将BF--
else
{
parent->_bf--;
}
//BF为0 说明已经平衡 无需处理
if (parent->_bf == 0)
{
break;
}
// BF 为1或者-1,说明高度变了,要继续更新父结点以及组织结点的BF
else if (parent->_bf == 1 || parent->_bf == -1)
{
parent = parent->_parent;
cur = cur->_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;
}
// 如果出现其他情况,说明插入之前这棵树就不是AVL树
else
{
assert(false);
}
}
return true;
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* parent_parent = parent->_parent;
//让subRL结点作为parent结点的右子树 更新完之后处理subRL_parent;
parent->_right = subRL;
if (subRL != nullptr)
{
subRL->_parent = parent;
}
//让parnet做为subR的左子树 更新完之后处理parent的_parent
subR->_left = parent;
parent->_parent = subR;
//subR做为这颗最小不平衡子树的根节点
if (parent_parent == nullptr)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (parent_parent->_left == parent)
{
parent_parent->_left = subR;
}
else
{
parent_parent->_right = subR;
}
subR->_parent = parent_parent;
}
//更新平衡因子
parent->_bf = subR->_bf = 0;
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* parent_parent = parent->_parent;
//让subLR节点做为parent节点的左子树 更新完之后处理subLR的_parent;
parent->_left = subLR;
if (subLR != nullptr)
{
subLR->_parent = parent;
}
//让parent节点做为subL的右子树 更新完之后处理parent的_parent
subL->_right = parent;
parent->_parent = subL;
//让这颗最小不平衡子树的parent节点做为subL的右子树
if (parent_parent == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent_parent->_left == parent)
{
parent_parent->_left = subL;
}
else
{
parent_parent->_right = subL;
}
subL->_parent = parent_parent;
}
//更新平衡因子
parent->_bf = subL->_bf = 0;
}
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right; //subLR不可能为nullptr,因为subL的平衡因子是1
int bf = subLR->_bf; // 记录平衡因子
//以最小不平衡子树的左结点进行左单旋
RotateL(parent->_left);
//右单旋
RotateR(parent);
//更新平衡因子
if (bf == 1)//对应插入再C树下
{
parent->_bf = 0;
subLR->_bf = 0;
subL->_bf = -1;
}
else if(bf == -1)//对应插入再b树下
{
parent->_bf = 1;
subLR->_bf = 0;
subL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subLR->_bf = 0;
subL->_bf = 0;
}
else
{
assert(false);
}
}
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
//以最小不平衡子树的右结点进行右单旋
RotateR(parent->_right);
//左单旋
RotateL(parent);
//更新平衡因子
if (bf == 1)//对应插入再c树下
{
subR->_bf = 0;
parent->_bf = -1;
subRL->_bf = 0;
}
else if (bf == -1)//对应插入再b树下
{
subR->_bf = 1;
parent->_bf = 0;
subRL->_bf = 0;
}
else if (bf == 0)
{
subR->_bf = 0;
parent->_bf = 0;
subRL->_bf = 0;
}
else
{
assert(false);
}
}
AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
- 验证其为二叉搜索树
- 如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
void Inorder()
{
_Inorder(_root);
cout << endl;
}
void _Inorder(Node* root)
{
if (nullptr == root)
{
return;
}
_Inorder(root->_left);
cout << root->_kv.first << ' ';
_Inorder(root->_right);
}
- 验证其为平衡树
- 从叶子结点处开始计算每棵子树的高度。(每棵子树的高度 = 左右子树中高度的较大值 + 1)
- 分别判断左右子树的高度
- 若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。
bool isAVLTree()
{
int hight = 0;
return _IsBalanced(_root, hight);
}
bool _IsBalanced(Node* root, int& hight)
{
//空树也是AVL树
if (nullptr == root)
{
hight = 0;
return true;
}
//判断左子树
int leftHight = 0;
if (!(_IsBalanced(root->_left, leftHight)))
{
return false;
}
//判断右子树
int rightHight = 0;
if (!(_IsBalanced(root->_right, rightHight)))
{
return false;
}
//检查该节点的平衡因子
if (rightHight - leftHight != root->_bf)
{
cout << "平衡因子设置异常:" << root->_kv.first << endl;
}
hight = max(leftHight, rightHight) + 1;
return abs(rightHight - leftHight) < 2;
}
参考源码
- gitee AVL树
- 菜鸟一枚,写的不好的地方请各位大佬多多包涵,手下留情。