赞
踩
之前我们学习了二叉搜索树,但是有时候因为节点插入的问题,可能会退化为单支树,这样会导致查找效率变得底下如顺序表。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度
这种树就是AVL树.
AVL树满足两个条件:
平衡因子=右子树高度-左子树高度
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
O
(
l
o
g
2
n
)
O(log_2 n)
O(log2n),搜索时间复杂度O(
l
o
g
2
n
log_2 n
log2n)
AVL树是一种平衡二叉树,其内部存储的是pair键值对,我们模拟实现的时候直接用pair来存储即可。
我们先来写AVL的节点定义:
每个节点都要有一个平衡因子用来保证其AVL树特性
template<class K,class V> struct AVLTreeNode { typedef AVLTreeNode<K, V> Node; AVLTreeNode(const pair<K,V> &kv) :_left(nullptr) ,_right(nullptr) ,_parent(nullptr) ,_bf(0) ,_kv(kv) {} Node* _left; Node* _right; Node* _parent;//记录当前节点的父亲 int _bf;//记录节点的平衡因子 pair<K, V> _kv;//保存记录的key,val };
然后我们来写AVL的框架:
template<class K,class V>
class AVLTree {
typedef AVLTreeNode<K, V> Node;
public:
//...
private:
Node* _root = nullptr;
};
下面我们来实现AVL树的插入:
插入分为两步:
插入节点的部分和二叉搜索树的代码一样,主要是修改平衡因子的部分
当插入新节点后,这个新节点的双亲节点的平衡因子会发生变化:
1.新插入的节点cur在其双亲节点parent的左孩子时,parent的平衡因子-1
2. 新插入的节点在parent的右孩子时,parent的平衡因子+1
代码如下:
bool insert(const pair<K,V> &kv) { if (_root == nullptr) { _root = new Node(kv); return true; } Node* parent = nullptr; Node* cur = _root; while (cur) { if (kv.first > cur->_kv.first) { parent = cur; cur = cur->_right; } else if (kv.first < cur->_kv.first) { parent = cur; cur = cur->_left; } else { return false; } } //找到了插入位置 cur = new Node(kv); if (kv.first > parent->_kv.first) { parent->_right = cur; } else { parent->_left = cur; } cur->_parent = parent; //修改平衡因子 while (parent) { if (cur == parent->_left) {//如果加在左边,则父节点的平衡因子-- parent->_bf--; } else { parent->_bf++;//右边则++ } //调整平衡因子 //... } return true; }
修改后这里有三种情况:
我们先来说前两种情况,第三种我们在旋转中讲解
第一种情况:
修改后parent的平衡因子为0,这里就不用再进行调整了
第二种情况:
修改后平衡因子为1,则以parent为根的这颗树的高度发生了变化,则要继续向上调整平衡因子
代码:
bool insert(const pair<K,V> &kv) { if (_root == nullptr) { _root = new Node(kv); return true; } Node* parent = nullptr; Node* cur = _root; while (cur) { if (kv.first > cur->_kv.first) { parent = cur; cur = cur->_right; } else if (kv.first < cur->_kv.first) { parent = cur; cur = cur->_left; } else { return false; } } //找到了插入位置 cur = new Node(kv); if (kv.first > parent->_kv.first) { parent->_right = cur; } else { parent->_left = 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 = cur->_parent; parent = parent->_parent; } else if (parent->_bf == 2 || parent->_bf == -2) { //旋转 } else { assert(false);//说明插入之前就有问题 } } return true; }
上面的两种情况,更新parent的平衡因子后AVL树的特性还保持着,但是第三种情况更新后,双亲的平衡因子为2/-2,破坏了平衡二叉搜索树的特性,所以就要进行以parent为根的树的旋转
AVL树的旋转也分为四种情况
1. 新节点插入较高左子树的左侧:右单旋
2. 新节点插入较高右子树的右侧:左单旋
3. 新节点插入较高左子树的右侧—左右双旋:先左单旋再右单旋
4. 新节点插入较高右子树的左侧—右左双旋:先右单旋再左单旋
下面我们来依次解释:
右单旋:
右单旋是新插入的节点在左子树中,使其整棵树右高左低,所以要旋转右子树来降低高度差使这棵树变得平衡
具体过程看下图:
这里我们来看一下具体当各子树高度的情况:
下面我们来编写一下右单璇的代码:
首先我们定义两个节点用来标记当前需要旋转的节点。
对于右单旋,我们找当前parent的左孩子 subL 和该左孩子的右孩子 subLR
然后我们在旋转时还要注意一下:
(1) 30这个节点的右子树可能存在也可能不存在
不存在时我们就不能直接将subL的parent指针直接指向60
void RotateR(Node* parent) {//右单旋
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) {//当subLR存在时,subLR的parent才指向parent
subLR->_parent = parent;
}
//...
}
(2) 60这个节点可能是根也可能不为根。
不为根时,我们还需要一个 ppnode 节点来标记parent的双亲节点,用来将新的’根’节点去链接到原来的树上。
void RotateR(Node* parent) {//右单旋 Node* subL = parent->_left; Node* subLR = subL->_right; parent->_left = subLR; if (subLR) {//当subLR存在时,subLR的parent才指向parent subLR->_parent = parent; } if (parent == _root) {//如果p是根,则subL更新为根 _root = subL; subL->_parent = nullptr; } else {//将旋转后的子树的根节点链接到原来的树上 if (ppnode->_left == parent) { ppnode->_left = subL; } else { ppnode->_right = subL; } subL->_parent = ppnode; } //... }
最后一步就是修改平衡因子,旋转后的节点不用再调整,因为旋转后子树的两端高度都相等,达到平衡。
void RotateR(Node* parent) {//右单旋 Node* subL = parent->_left; Node* subLR = subL->_right; parent->_left = subLR; if (subLR) {//当subLR存在时,subLR的parent才指向parent subLR->_parent = parent; } subL->_right = parent; Node* ppnode = parent->_parent;//保存p的parent指向 parent->_parent = subL; if (parent == _root) {//如果p是根,则subL更新为根 _root = subL; subL->_parent = nullptr; } else {//将旋转后的子树的根节点链接到原来的树上 if (ppnode->_left == parent) { ppnode->_left = subL; } else { ppnode->_right = subL; } subL->_parent = ppnode; } //更新节点的平衡因子 parent->_bf = 0; subL->_bf = 0; }
现在来看左单旋:
左单旋和右单旋相反,左边高右边低,所以进行左单旋来降低高度差
这里的情况都和右单旋转相反,我们直接上代码:
void RotateL(Node* parent) {//左单旋 Node* subR = parent->_right; Node* subRL = subR->_left; parent->_right = subRL; if (subRL) {//当subRL存在时,subRL的parent才指向parent subRL->_parent = parent; } subR->_left = parent; Node* ppnode = parent->_parent;//保存p的parent指向 parent->_parent = subR; if (parent == _root) {//如果p是根,则subR更新为根 _root = subR; subR->_parent = nullptr; } else {//将旋转后的子树的根节点链接到原来的树上 if (ppnode->_left == parent) { ppnode->_left = subR; } else { ppnode->_right = subR; } subR->_parent = ppnode; } //更新节点的平衡因子 parent->_bf = 0; subR->_bf = 0; }
我们现在来看左右双旋,先左单旋再右单旋
这里的左右双旋其实就是相当右单旋的那种场景下,将b子树拆分成两颗子树,再将新增节点加在拆分后的子树上
我们以加在左子树为例讲解:
对新增节点后的树进行旋转,先以subL为根进行左旋,
再对parent为根进行右旋
加在右子树的过程和这个相反,希望大家可以自己去推导…
知道了大概的过程,我们现在来写代码:
和单旋一样,我们定义变量来协助我们旋转
void RotateRL(Node* parent) {//双旋(先左单旋,再右单旋)
Node* subL = parent->_left;
Node* subLR = subL->_right;
//..
}
这里需要注意:新增节点所加的位置有三种情况
(1) 加在拆分后的左子树上
(2) 加在拆分后的右子树上
(3) 上图的60这个位置本身是空的
那么我们如何区分这三种情况呢,
我们可以用subLR的平衡因子来区分,subLR的因子为-1时,说明左高,则加在了左子树上,为1时,说明右高,则加在了右子树,为0时说明这个位置原来是空的。
void RotateLR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; int bf = subLR->_bf; RotateL(subL); RotateR(parent); subLR->_bf = 0; if (bf == -1) { parent->_bf = 1; subL->_bf = 0; }else if (bf == 1) { parent->_bf = 0; subL->_bf = -1; } else if (bf == 0) { parent->_bf = 0; subL->_bf = 0; } else { assert(false); } subLR->_bf = 0; }
右左双旋
右左双旋和左右双旋相反,各位老铁可以参考左右双旋来自己去画出图来理解
代码:
void RotateRL(Node* parent) {//双旋(先右单旋,再左单旋) Node* subR = parent->_right; Node* subRL = subR->_left; int bf = subRL->_bf;//以subRL的因子为标准判断所加的子树的位置 RotateR(subR); RotateL(parent); //修改平衡因子 //增加节点后有三种情况 subRL->_bf = 0; if (bf == -1) {//加在左子树上 parent->_bf = 0; subR->_bf = 1; } else if (bf == 1) { parent->_bf = -1; subR->_bf = 0; } else if (bf == 0) { parent->_bf = 0; subR->_bf = 0; } else { assert(false); } }
AVL的插入讲完了,我们来看看如何证明咋们模拟实现的就是AVL树
一棵树如果是AVL树,那么首先它是一个二叉搜索树,其次他的每个子树的高度差不能超过1
这里我们做了一点小优化,我们在判断时先传入高度,判断高度差时类似于后序遍历的方式,从下往上去计算高度差
代码如下:
bool IsBalance() { int height = 0; return _IsBalance(_root, height); } bool _IsBalance(Node* root,int &height) { if (root == nullptr) { height = 0; return true; } int leftHeight = 0, rightHeight = 0; if (!_IsBalance(root->_left, leftHeight) || !_IsBalance(root->_right, rightHeight)) { return false; } if (abs(rightHeight - leftHeight) >= 2) { cout <<root->_kv.first<< "不平衡" << endl; return false; } if (rightHeight - leftHeight != root->_bf) { cout << root->_kv.first << "平衡因子异常" << endl; return false; } height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1; return true; }
我们今天终于讲完了AVL树,我们的C++部分也开始上了难度,希望大家可以跟上我们的讲解,下一节我们要来开始手撕红黑树
!!!
在此之前我们要熟悉前面的二叉搜索树和AVL树的内容,希望大家都能学好C++。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。