前言
如果map/set的底层使用二叉搜索树实现,当插入数据接近有序时,二叉搜索树就会退化成单支树,搜索效率退化成O(n)。为解决退化的问题,需要map/set底层实现的二叉搜索树做平衡处理
AVL树的概念
作为平衡树的一种,AVL树有以下性质
1.左右子树都是AVL树
2.左右子树的高度差(又称平衡因子,平衡因子=右树高度-左树高度)的绝对值不超过1
AVL树的搜索效率为O(logN),如果树有n个节点,它的高度保持在logN,AVL树做到了严格的平衡
AVL树实现
结构定义
template <class K, class V>
struct AVLNode
{
pair<K, V> _kv;
AVLNode* _left;
AVLNode* _right;
AVLNode* _parent;
int _bf;
AVLNode(const pair<K, V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
,_kv(kv)
{}
};
template <class K, class V>
class AVLTree
{
typedef AVLNode<K, V> Node;
public:
bool Insert(const pair<K, V>& kv);
void Inorder() { _Inorder(_root); }
void Leorder() { _Leorder(_root); }
bool IsAVLTree() { return _IsAVLTree(_root); }
private:
Node* _root = nullptr;
void RotateL(Node* parent);
void RotateR(Node* parent);
void RotateRL(Node* parent);
void RotateLR(Node* parent);
void _Inorder(Node* root);
void _Leorder(Node* root);
int _Height(Node* root);
bool _IsAVLTree(Node* root);
};
AVL树的节点实现了三叉链,即增加了一个parent指针,另外bf作为平衡因子用来平衡整颗树。
左单旋
A,B,C是三颗高度相同的子树
左单旋:往较高的右子树的右边插入节点,由于parent右子树subR比parent左子树A高出一个节点,所以只要往右子树的subR插入节点,subR的高度都将会高于2个节点,不满足AVL树的性质。由于parent小于subR,将parent作为subR的左子树,B大于parent,将B作为parent的右子树。旋转前subR的平衡因子为1,右边较高,旋转后subR达到平衡,subR作为新的根节点。
template <class K, class V>
void AVLTree<K, V>::RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
subR->_left = parent;
parent->_right = subRL;
pparent = parent->_parent;
parent->_parent = subL;
if (subRL)
subRL->_parent = parent;
if (_root == parent)
{
_root = subL;
subR->_parent = nullptr;
}
else // 该子树是树的一部分,将子树链接到原树
{
subR->_parent = pparent;
if (subR->_kv.first < pparent->_kv.first)
{
pparent->_left = subR;
}
else
{
pparent->_right = subR;
}
}
parent->_bf = 0;
subR->_bf = 0;
if (subRL)
subRL->_bf = 0;
}
右单旋
右单旋:往较高的左子树的左边插入节点,将parent向右旋转,parent做subL的右孩子,subL的右孩子做parent的左孩子,subL做根。
template <class K, class V>
void AVLTree<K, V>::RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
subL->_right = parent;
parent->_left = subLR;
pparent = parent->_parent;
parent->_parent = subL;
if (subLR)
subLR->_parent = parent;
if (_root == parent)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
subL->_parent = pparent;
if (subL->_kv.first < pparent->_kv.first)
{
pparent->_left = subL;
}
else
{
pparent->_right = subL;
}
}
parent->_bf = 0;
subL->_bf = 0;
if (subLR)
subLR->_bf = 0;
}
右左双旋
右左双旋:插入较高右子树的左边,parent的平衡因子为1,插入后为2,需要进行调整。其中B,C的高度为h-1,只有当h大于等于1时才符和上图的情况。parent的右子树subR较高,往subR的右边插入节点,需要左单旋,而往subR的左边subRL的任意子树插入节点就需要进行右左双旋。以往B子树插入节点,subRL的平衡因子为-1,subR也为-1,parent为2,先对subR进行右单旋,subR做subRL的右子节点,subRL之前的右子节点做subR的左子节点,旋转后的树就是一个左单旋的情况,对parent进行左单旋,完成后parent的平衡因子为0,subR为1,subRL为0。
当往subRL的右子树插入节点,右左双旋之后subRL的平衡因子为0,parent为-1,subR为0。
还有一种特殊情况,上面讨论的范围都是在h大于等于1时,当h为0,即只有parent,subR,subRL的情况,旋转后每个节点的平衡因子都为0。
template <class K, class V>
void AVLTree<K, V>::RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
size_t bf = subRL->_bf;
RotateR(subR);
RotateL(parent);
if (bf == 0)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 1;
}
else
{
assert(false);
}
}
左右双旋
同样,往较高左子树的左边插入节点,需要右单旋,而往较高左子树的右边插入节点需要左右双旋,之后平衡因子的更新看着图就能很快分析出来。如果节点插入到B后面,subL平衡因子为0,parent为1,subLR为0;插入到C后面,subL为-1,parent为0,subLR为0;当只有parent,subL,subLR三个节点时,三者都为0。
template <class K, class V>
void AVLTree<K, V>::RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
size_t bf = subLR->_bf;
RotateL(subL);
RotateR(parent);
if (bf == 0)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
Insert代码
template <class K, class V>
bool AVLTree<K, V>::Insert(const pair<K, V>& kv)
{
// 先按搜索树的规则插入
// 然后更新平衡因子
if (_root == nullptr)
{
_root = new Node(kv);
_root->_bf = 0;
return true;
}
Node* cur = _root;
Node* parent = nullptr;
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;
}
}
if (kv.first < parent->_kv.first)
{
cur = new Node(kv);
parent->_left = cur;
cur->_parent = parent;
}
else
{
cur = new Node(kv);
parent->_right = cur;
cur->_parent = parent;
}
// 更新平衡因子
while (parent)
{
if (parent->_left == cur)
{
parent->_bf--;
}
else
{
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)
{
RotateRL(parent);
}
// 左右双旋
// 较高左树的右边插入
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
break;
}
else
{
// 未插入前的平衡树出现问题
assert(false);
}
}
return true;
}
验证AVL树
根据AVL树的特征:左右子树高度差不超过1,所有的子树都是AVL树,递归树的每个节点,以每个节点为根节点,判断这样的树是否为AVL树。
template <class K, class V>
bool AVLTree<K, V>::_IsAVLTree(Node* root)
{
assert(root);
int LHeight = _Height(root->_left);
int RHeight = _Height(root->_right);
if (RHeight - LHeight == -2 || RHeight - LHeight == 2)
{
cout << "平衡因子异常" << endl;
return false;
}
if (RHeight - LHeight != root->_bf)
{
cout << "平衡因子不符合实际" << endl;
return false;
}
if (root->_left)
_IsAVLTree(root->_left);
if (root->_right)
_IsAVLTree(root->_right);
return true;
}
- 求出左右子树的高度,判断相减后的绝对值是否为2,若为2,打印出错提示
- 将两者高度相减后判断是否对于平衡因子,不相等则打印出错提示
- 最后判断左右子树是否不为空,若不为空则继续递归左右子树
- 不断得用上面的三个标准判断每一颗子树是否为AVL树
最后的结果是:通过插入随机数验证
AVL树 VS 红黑树
- 相同的地方:两者的插入,删除和查找的效率都是logN
- 不同的地方:
- AVL树较红黑树严格,保持了高度的平衡,所以查找可能会快一些
- 红黑树的调整频率比AVL树低,它没有那么严格,所以插入和删除会快一些,但也只是少了几次的旋转
- 两者的实现难度都很高,需要考虑很多的边界情况,但是红黑树较AVL更好理解,它只有两种颜色,而AVL有四种平衡因子,情况越多,就越需要越细的考虑
综上所述,两种各有优劣,我们需要根据不同的场景选择不同的结构
- 如果查找操作比较频繁,插入和删除操作少,那么选择AVL树合适
- 如果插入和删除的操作频繁,删除操作较少,那么选择红黑树合适