简介
AVL树是最先被发明出来的自平衡二叉查找树,在1962由前苏联科学家G. M. Adelson-Velsky和E. M. Landis在论文中发表。AVL树中引入了平衡因子,每一个节点都有一个平衡因子(一般是右子树高度 - 左子树高度);AVL树要求左右子树高度相差不能超过1,即平衡因子只能是0,1或-1。AVL树的高度始终是log(n),n为节点数量。
AVL树的结构
template<class K,class V>
struct AVLTreeNode //定义节点的结构
{
pair<K, V> _kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf;//平衡因子balance factor
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
template<class K,class V>
class AVLTree
{
typdef AVLTreeNode<K, V> Node;
public:
private:
Node* _root = nullptr;
};
_kv是AVL树储存的键值对,既能存储键值key,每一个key也能对应储存一个value值。
为什么要有_parent?
AVL树通过平衡因子来控制树的平衡,插入或删除数据后,需要回溯到父节点计算平衡因子,所以我们需要_parent来储存每一个节点父节点的值。
AVL树的插入
插入的基本流程
寻找插入的位置
按照二叉树遍历的顺序找到要插入的位置。
更新平衡因子
插入一个新的节点之后,如果父节点的平衡因子发生改变,则需更新从根节点到父节点这条路径的所有节点的平衡因子。
判断是否产生不平衡
如果更新平衡因子之后,没有出现平衡因子不为0,-1,1的,则说明树是平衡的,插入结束。
如果出现了不平衡,对不平衡⼦树旋转,旋转后本质调平衡的同时,本质降低了⼦树的⾼度,不会再影响上⼀层,所以插⼊结束。
平衡因子的更新
更新的原则
更新停止的条件
插入的整体框架:
bool insert(pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;//不应许插入相同的值
}
}
cur = new Node(kv);
if (cur->_kv.first < parent->_kv.first)
{
parent->_left = cur;
}
else if (cur->_kv.first > parent->_kv.first)
{
parent->_right = cur;
}
cur->_parent = parent;
//更新平衡因子
while (parent)
{
// 更新平衡因⼦
if (cur == parent->_left)
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)
{
// 不平衡了,旋转处理
break;
}
else
{
assert(false);
}
}
return true;
}
旋转
旋转的原则
1.保持搜索树的规则
2.让树变平衡,降低旋转树的高度
根据插入的位置不同,旋转一共分为四种,左单旋/右单旋/左右双旋/右左双旋
右单旋
右单旋适合处理下图这种情况,插入前,左子树高度刚好比右子树高1,且刚好插入值为在左子树最左边(比原来树中的所有值都小)。
普遍情况:
为了方便理解我们这里取特例:
插入节点-3之后,树不再平衡。这时候就需要通过旋转来使树再次平衡。怎样让树再次平衡呢?
我们只需要操作最深的那一条路径就好了,让它高度减一,在移到右子树上,这样平衡因子就为0了。
虽然不符合AVL树的规则了,但是这颗树还是符合二叉搜索树的规则的。还是可以中序遍历。
这棵树中序遍历的结果是:-3,1,5,8,10,15。把10拿下来让左子树高度减一,再向办法把10放到右子树中,5的右子树绝对比它的父节点(10)小,所以可以把10变成5的右节点,同时让8接到10的左节点。这样即保证中序遍历的结果又使树恢复了平衡。
代码实现:
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 需要注意除了要修改孩⼦指针指向,还是修改⽗亲
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
// parent有可能是整棵树的根,也可能是局部的⼦树
// 如果是整棵树的根,要修改_root
// 如果是局部的指针要跟上⼀层链接
if (parentParent == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
parent->_bf = subL->_bf = 0;
}
要处理的三个节点
注意:parent不一定是根节点,所以要创建一个临时变量parentParent储存parent的父节点,处理好子树之后,再让subL的父节点指向parentParent(parentParent为空则,subL变为根节点)。
左单旋
左单旋适合一下情况
树中的左旋和右旋互为对称操作,左旋和右旋是可以相互抵消的,即经历一次左旋和一次右旋之后会恢复成原树。
代码实现:
void RoateL(Node* parent)
{
Node* subR = parent->_right;
parent->_right = subR;
Node* subRL = subR->_left;
if (subRL)
{
subRL->_parent = subR;
}
Node* parentParent = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
parent->_right = subRL;
if (subRL)
{
subRL->_parent = parent;
}
if (parentParent)
{
subR->_parent = parentParent;
if (parentParent->_left == parent)
{
subR->_parent = parentParent;
parentParent->_left = subR;
}
else if(parentParent->_right == parent)
{
subR->_parent = parentParent;
parentParent->_right = subR;
}
}
else if (parentParent == nullptr)
{
_root = subR;
}
parent->_bf = subL->_bf = 0;
}
左右双旋
观察下面这种情况,它并不是纯粹的左边高,对于节点10是左边高,对于节点5来说是右边高。
这时候单纯的右单旋或者左单旋都不能解决问题, 这时候两次单旋就可以解决问题。以节点5为旋转点,进行一次左单旋,这时候就是一个完全左边高的树结构,完全符合右单旋的应用场景,以节点10为旋转点进行一次右单旋,树恢复平衡。
要处理的节点:
代码实现:
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
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);
}
}
右左双旋
右左双旋于左右双旋类似,接下来我们以普适情况抽象出a,b,c三棵子树来讨论。另外我们需要把b⼦树的 细节进⼀步展开为12和左⼦树⾼度为h-1的e和f⼦树,因为我们要对b的⽗亲15为旋转点进⾏右单旋,右单旋需要动b树中的右⼦树。
场景二:h >= 1时,新增结点插⼊在f⼦树,f⼦树⾼度从h-1变为h并不断更新12->15->10平衡因⼦, 引发旋转,其中12的平衡因⼦为1,旋转后15和12平衡因⼦为0,10平衡因⼦为-1。
场景三:h == 0时,a/b/c都是空树,b⾃⼰就是⼀个新增结点,不断更新15->10平衡因⼦,引发旋 转,其中12的平衡因⼦为0,旋转后10和12和15平衡因⼦均为0。
代码实现:
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
查找
按照遍历二叉树的逻辑查找即可。
Node* Find(const K& key)//按照键值查找
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < key)
{
cur = cur->_right;
}
else if (cur->_kv.first > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
AVL树平衡检测
对于实现的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 _IsBalanceTree(Node* root)
{
// 空树也是AVL树
if (nullptr == root)
return true;
// 计算pRoot结点的平衡因⼦:即pRoot左右⼦树的⾼度差
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
int diff = rightHeight - leftHeight;
// 如果计算出的平衡因⼦与pRoot的平衡因⼦不相等,或者
// pRoot平衡因⼦的绝对值超过1,则⼀定不是AVL树
if (abs(diff) >= 2)
{
cout << root->_kv.first << "⾼度差异常" << endl;
return false;
}
if (root->_bf != diff)
{
cout << root->_kv.first << "平衡因⼦异常" << endl;
return false;
}
// pRoot的左和右如果都是AVL树,则该树⼀定是AVL树
return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}