map/multimap/set/multiset这几个容器有个共同点是: 其底层都是按照二叉搜索树来实现的,但是普通的二叉搜索树有其自身的缺陷, 假如往树中插入的元素有序或者接近有序, 二叉搜索树就会退化成单支树, 时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
AVL树的概念
二叉搜索树虽可以提升查找的效率,但如果数据有序或接近有序时二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
所以什么是平衡二叉树?
1.空树
2.或者是具有以下性质的二叉搜索树:
它的左右子树都是AVL树且左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
log_2 n,搜索时间复杂度O(log_2 n)。
AVL树结构的定义
那我们这里以KV模型的结构实现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;//balence factor
AVLTreeNode(const pair<K,V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_bf(0)
{}
};
这里我们给结点增加一个_parent指针指向它的父亲结点,方便我们后续调整平衡, 带来方便的同时我们也需要去维护每个结点的_parent指针.
template<class K,class V>
class AVLTree
{
typedef ALVTreeNode<K, V> Node;
public:
//成员函数
private:
Node* _root = nullptr;
};
AVL树的操作
插入
AVL树就是在二叉搜索树的基础上引入了平衡因子来控制树的相对平衡,因此AVL树也可以看成是二叉搜索树。所以插入的逻辑其实跟搜索二叉树是一样的,不同的地方在于平衡二叉树插入之后如果整棵二叉树或者其中某些子树不平衡了我们要对插入的结点进行调整使得它重新变的平衡.
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
else
{
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;
//平衡因子更新
//..
return true;
}
}
平衡因子
什么是平衡因子?
一个结点的平衡因子就是它的左右子树的高度差, 一般是右子树减左子树的高度(我们这里的讲解也统一以右子树-左子树的高度作为平衡因子)。
为什么要更新平衡因子?
我们在AVL树中插入了一个新结点之后, 会不会影响到树中结点的平衡因子?
当然是会的!
因为一旦插入了新的结点,整棵树的高度或者某些子树的高度必然会发生变化,那树的高度发生变化,必然会影响与之关联的结点的平衡因子。
不更新平衡因子行不行?
不行, 为什么呢?
因为如果一棵二叉搜索树是AVL树,那么它必须满足任何一个结点的平衡因子都在[-1, 0, 1]这个范围内。而现在插入新结点会导致平衡因子变化, 那么更新之后, 某些结点的平衡因子可能就不在[-1, 0, 1]这个正常范围内了。那他就不是一棵AVL树了,所以我们才要更新平衡因子,以此来判断这个树还是否是一棵AVL树。
如果不是AVL树, 即有结点的平衡因子不在正常范围内了, 那这棵树的平衡就受到影响了, 那我们就需要对新插入的结点进行调整, 使他变回AVL树。如果插入之后平衡没有受到影响,就不需要调整了。
现在先实现插入新结点后如何更新平衡因子.
如何更新平衡因子?
首先插入一个新结点之后, 可能会影响到哪些结点的平衡因子?
最先影响的肯定是它的祖先, 因为新插入的结点在它祖先的子树上, 那它祖先的子树高度发生变化, 平衡因子必然也会发生变化, 但是会影响祖先的祖先吗?
不一定!可能只影响一部分。
这种情况只影响祖先的平衡因子:
这种情况就影响了全部的祖先的平衡因子:
观察可以发现平衡因子更新的规律:
因为平衡因子的计算是右子树高度-左子树高度, 所以, 于新结点的父亲来说:
1.如果插入在了右子树, 那么父亲的平衡因子就要++
2.如果插入在了左子树, 那么父亲的平衡因子就要- -
这时候parent指针的作用就体现出来了:
parent更新后,是否需要继续往上更新?
首先parent肯定要更新, 因为插入之后它的子树的高度变了, 什么情况下parent更新完之后还要继续往上更新parent的祖先?取决于parent所在的这棵子树的高度有没有发生变化.
1.如果插入之后以parent根节点的这棵树的高度没有变化, 即parent的平衡因子更新之后为0, 那就不会影响parent祖先的平衡因子, 就不需要往上继续更新了.
因为更新之后为0的话,说明插入之前它的平衡因子为1或者-1, 然后我们在左边或者是右边插入了一个结点, 它的平衡因子就变成了0, 这棵树变平衡了, 高度就没有发生变化
2.如果插入之后parent这棵子树的高度发生了变化,那parent的平衡因子更新完成后就需要继续往上更新,也就是如果parent的平衡因子更新之后为1或-1, 则parent这棵树的高度发生变化, 需要继续向上更新.
因为parent的平衡因子在更新之后变成了1或者-1说明它更新之前的平衡因子一定是0,说明他之前两边高度是平衡的, 而现在插入之后变为1或-1, 说明右边或者左边高了, 因此高度肯定是变化了, 那就要继续往上更新。
那可能是-2或者2加一减一之后变成-1或1吗?
不可能, 因为AVL树的平衡因子的范围都是在[-1, 0, 1]内的。
3. parent的平衡因子更新之后为2或-2
如果是2或-2, 那已经不在平衡因子的正常范围内了, 那就说明当前parent所在的这棵子树已经不平衡(通常把这棵树叫做最小不平衡子树),那就不要再往上更新了, 就要去调整结点使这棵最小不平衡子树重变平衡。
怎么调整呢,?要分情况进行旋转, 使它的高度恢复到插入之前, 从而也就不需要再继续往上更新了。
平衡因子更新代码实现:
因为不知道要向上更新几次, 所以是一个循环, 循环什么时候结束?
它可能向上更新几次就出现平衡因子为2或-2的情况要开始调整了, 但是不排除可能根节点都更新完了, 整棵树还是平衡的, 不需要作调整
比如这种情况:
//平衡因子更新
while (parent)
{
if (cur == parent->right)
parent->_kv++;
else if (cur == parent->left)
parent->_kv--;
//判断是否需要继续向上更新,需要就往上走(bf==1或-1)等待下次循环更新
//如果不平衡了就进行处理(bf==2或-2),不需要处理不需要调整就break(bf==0)
if (parent->_kv == 0)
{
break;
}
else if (parent->_kv == 1 || parent->_kv == -1)
{
cur = parent;
parent = parent->_parent;
}
else if (parent->_kv == -2 || parent->_kv == 2)
{
//旋转调整
}
}
接下来重点说一下对于不平衡的情况如何进行调整, 即AVL树的旋转
AVL树的旋转
如果在一棵原本是平衡的AVL树中插入一个新节点, 可能造成不平衡, 此时必须调整树的结构, 使之平衡化。根据节点插入位置的不同, AVL树的旋转分为四种:
新节点插入较高右子树的右侧—右右: 左单旋
什么样的情况要进行左单旋呢?
图里面的a,b,c是什么?
这里给的是一个抽象图, a、b、c分别代表三棵高度为h的AVL子树, 这里的h可以为任何整数值(所以h取不同的值(0,1,2,3,4...), 这里具体的情况是有很多种的, 但是针对这一类情况, 我们的处理是统一的),举个例子:
假如h = 0, 情况只有1种,在60的右节点插入
h = 1, 情况有2种, 在c的左右结点都可以插入
h = 2, 情况有36种, h为2的二叉树形状有3种, a和b分别取3种就是9种, c只能是一颗h为2的满二叉树即平衡因子为0, 所以有4种插入情况, 总共为9×4=36种.
为什么c只能是一颗h为2的满二叉树即平衡因子为0的二叉树?
因为要保证c中插入后平衡因子要一路顺着祖先向上调整, 平衡因子为1或者-1的话一定是再c中插入后c就变成平衡无需向上调整平衡因子或者是c自己插入后本身就不平衡, c就变成了最小不平衡子树, 因此这里讨论的c都是平衡因子为0的.
h=3情况a和b只要高度为3即可,c不一定是满的二叉树,平衡因子为0且插入后因子能一直向上调整即可,情况更复杂, 所以可以看出这里的抽象图的意义, 所有的情况都被一种抽象图给概括了.
如何进行左单旋
现在30这个结点的平衡因子是不在正常范围内的, 这棵树是不平衡的且右边高,所以要对30这棵树进行左单旋,怎么左单旋呢?
相当于把30往左边向下旋转,所以叫左单旋。
进行了左单旋之后, 这棵树就重新变成AVL树, 达到平衡状态了,树的高度也降下去了。降高度是一方面, 在使它变平衡的同时也保持了它依旧是一颗搜索二叉树, 因为AVL树就是平衡的搜索二叉树(旋转过程选择的孩子都是满足搜索树的大小关系的)。
代码:
,
void rotateL(Node* parent)// 旋转的时候传要旋转的子树的根结点即可
{
//获取需要操作到的几个结点
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* parentParent = parent->_parent;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;//h=0时,subRL是空树,就不需要链接父结点了
subR->_left = parent;
parent->_parent = subR;
//parent上面可能还有结点,我们这里旋转的可能是一整棵树,也可能是一棵树中的子树。
//如果parent就是_root,更改_root,然后_parent置为空,因为根节点的_parent是nullptr
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
//如果是子树的话,上面还有结点
//用之前保存的parentParent进行链接
else
{
if(parent == parentParent->_right)
parentParent->_right = subR;
else if (parent == parentParent->_left)
parentParent->_left = subR;
subR->_parent = parentParent;
}
//最后,旋转之后要更新一下平衡因子
subR->_bf = parent->_bf = 0;
}
什么时候调用左单旋
那我们代码写好了,什么时候调用呢?
如果parent的平衡因子是2, subR(对应我们在更新平衡因子的那个循环里就是cur)的平衡因子是1,此时要进行的就是左单旋
新节点插入较高左子树的左侧—左左:右单旋
什么情况要进行右单旋?
和左单旋类似, 同样的我们这里讨论的情况是插入之后a的高度要发生变化, 且会影响到当前这棵树(当然它可以是一棵子树)根结点的平衡因子, 导致整棵树不平衡, 这时我们可以用右单旋解决。
如何操作?
相当于把30往右边向上旋转,所以叫右单旋。
30的右子树比30大,比60小,所以可以做60的左子树, 然后60整棵树都比30大, 所以可以做30的右子树。这样这棵树就重新变平衡了,30成为了新的根结点。
代码实现:
void rotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* parentParent = parent->_parent;
//这里换了一种思路,先把子结点链接起来
subL->_right = parent;
parent->_left = subLR;
//然后链接父结点
parent->_parent = subL;
if (subLR)
subLR->_parent = parent;
//和左单旋一样,判断parent是不是根
if (_root == parent)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
{
parentParent->_left = subL;
}
else if (parentParent->_right == parent)
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
//调整平衡因子
subL->_bf = parent->_bf = 0;
}
什么时候调用右单旋?
新节点插入较高左子树的右侧—左右:先左单旋再右单旋(左右双旋)
什么情况进行左右双旋
这里给的是在b插入, 在c插入当然也是左右双旋, 但是插入之后平衡因子的更新会有一些不同,后面会提到, 这还是抽象图, 我们来画几个具象图看一下.
和左单旋中分析的类似,h==0只有一种情况
h==1有两种情况
h==2有36种情况:
h==3有144*225种:
高度为3的二叉树有C44+C43+C42+C41共15种, b或c是平衡因子为0的h==2的二叉树所以只能是x,
当在b插入b就是x,c可以是xyz中的任意一共3*4,在c插入同理也是3*4种,总计(12*2)*(15*15)种.
所以能看出来抽象图是为了统一各种子情况
如何进行左右双旋
首先对于这种情况,们如果只进行左或者右的单旋是解决不了问题的
那要进行双旋,怎么做呢?
上面已经说了针对这种情况要进行的是左右双旋,那顾名思义就是先进行一个左单旋(对根的左子树),再进行一个右单旋(对根)
将双旋变成单旋后再旋转, 即:先对30进行左单旋, 然后再对90进行右单旋, 旋转完成后再
考虑平衡因子的更新.第一步的左单旋相当于把它转化成右单旋的情况,然后右单旋即可.我们能发现它就是把60推上去做根,然后60的左右子树分给30的右子树和90的左子树.
代码实现:
那左右双旋的代码的一部分可以直接复用左右单旋,但是左右双旋麻烦的地方在于平衡因子的调节,我们上面提到插入在b和c它们最后平衡因子更新不同
能看到插入在b和c旋转之后它们的平衡因子更新是不一样的,那如何判断在b插入还是在c插入呢?
插入之后60这个结点的平衡因子是不同的
那除此之外还有第三种情况,h为0的时候, 平衡因子的更新又有所不同:
所以,平衡因子的更新这里我们要分三种情况:
1. subLR->bf == 0
2. subLR->bf == 1
3. subLR->bf == -1
(subLR就是图上的60结点)
void rotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR= subL->_right;
int bf = subLR->_bf;//先记录subLR的平衡因子,分情况进行平衡因子的更新
//左右单旋
rotateL(parent->_left);
rotateR(parent);
//分情况进行平衡因子的更新
if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
assert(false);
}
}
新节点插入较高右子树的左侧—右左:先右单旋再左单旋(右左双旋)
插入到b这棵树上也可以, 和左右双旋一样, 高度h不同, 就会产生很多不同的情况,但我们可以统一处理
和左右双旋一样,这样的情况只旋一次是不能达到平衡的,.所以第一次右单旋其实是把它变成左单旋的情况,然后再进行一次左单旋就平衡了。
最终的结果就相当于把60推上去做根,然后60的左右子树分别分给30的右子树和90的左子树。
右左双旋代码实现 :
与左右双旋一样,平衡因子更新还是三种情况,通过插入之后subRL的平衡因子区分:
1.在c插入,subRL->_bf == 1
2. subRL->_bf == -1
3. subRL->_bf == 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)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else if(bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else
{
assert(false);
}
}
总结
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑:
(1) pParent的平衡因子为2, 说明pParent的右子树高, 设pParent的右子树的根为SubR
1.当SubR的平衡因子为1时, 执行左单旋
2.当SubR的平衡因子为-1时, 执行右左双旋
(2) pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为SubL
1.当SubL的平衡因子为-1是,执行右单旋
2.当SubL的平衡因子为1时,执行左右双旋
旋转完成后,原pParent为根的子树高度降低,已经平衡,不需要再向上更新平衡因子
//旋转
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);
}
else
{
assert(false);
}
//旋转结束后,不需要再向上更新平衡因子了
break;
}
AVL树的测试
验证其为二叉搜索树
我们插入一些数据, 如果中序遍历可得到一个有序的序列, 就说明为二叉搜索树.
补充成员函数中序遍历:
void test2()
{
int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
AVLTree<int, int> t;
for (auto e : a)
{
t.Insert(make_pair(e, e));
}
t.InOrderPrint();
}
int main()
{
test2();
return 0;
}
验证其为平衡树
如何验证它是否平衡呢?
我们可以去计算高度, 如果每一个结点左右子树的高度差的绝对值不超过1, 就证明它是平衡的。
为什么不用平衡因子判断呢?
首先, 不是所有的AVL树的实现里面都有平衡因子的, 只是我们这里采用了平衡因子, 这是AVL树的一种实现方法而已。其次,, 我们不敢保证我们自己写到代码计算出来的平衡因子一定是正确的。
所以, 我们来写一个通过高度差来判断是否平衡的函数:
bool IsBalance()
{
return _IsBalance(_root);
}
bool _IsBalance(Node* _root)
{
if (_root == nullptr)
return true;
int left = _Height(_root->_left);
int right = _Height(_root->_right);
return abs(left - right) <= 1
&& _IsBalance(_root->_left)
&& _IsBalance(_root->_right);
}
int _Height(Node* _root)
{
if (_root == nullptr)
return 0;
int left = _Height(_root->_left);
int right = _Height(_root->_right);
return left > right ? left + 1 : right + 1;
}
void test2()
{
int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
AVLTree<int, int> t;
for (auto e : a)
{
t.Insert(make_pair(e, e));
}
t.InOrderPrint();
cout << endl;
cout << t.IsBalance() << endl;
}
int main()
{
test2();
return 0;
}
判断平衡因子的更新是否正确
计算一下高度差,看他和平衡因子相不相等就行了:
大量随机数构建AVL树进行测试
上面的测试数据量比较小, 且不够随, 下面我们生成一些随机数来构建AVL树, 测试一下:
void test3()
{
srand(time(nullptr));
const int N = 1000;
AVLTree<int, int> t;
for (int i = 0;i<N;i++)
{
int x = rand();
t.Insert(make_pair(x,x));
}
t.InOrderPrint();
cout << endl;
cout << t.IsBalance() << endl;
}
int main()
{
test3();
return 0;
}
AVL树查找
AVL树的查找那和搜索二叉树是一样的,可以看之前搜索二叉树的文章。
AVL树的删除(了解)
AVL树的删除操作了解即可
AVL树的删除操作主要分为以下几个步骤:
删除操作首先还是按照搜索树的规则先找到这个结点, 然后又要像二叉搜索树的删除一样, 分为三种情况: 左子树为空, 右子树为空, 左右都不为空
AVL树删除还要注意更新平衡因子:
和插入相反, 删除右结点的话, 父结点的平衡因子--, 删除左结点的话, 父结点的平衡因子++, 然后判断需不需要向上继续更新平衡因子, 插入的时候如果父结点平衡因子变为1或-1就要向上更新, 因为此时子树的高度变了, 一路上祖先的平衡因子就要跟着变化, 现在删除相反, 父结点平衡因子变为1或-1子树的高度并没有变化, 因为是从0变化过来的, 子树的高度没有变, 不会影响上一层, 就结束了.
而结点平衡因子变为0, 就代表子树的高度变化, 比如1变为0的话, 就代表高的那棵树被删除了, 这棵树的高度就变了, 需要向上更新平衡因子了, 直至到达根节点或不需要进一步调整为止。
上面都是只更新平衡因子的情况, 如果更新后平衡因子为2或-2, 需要旋转:
左右双旋就不演示了
AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树, 其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度, 即 O(logN)
但是如果要对AVL树做一些结构修改的操作,性能非常低下。
比如: 插入时要维护其绝对平衡, 旋转的次数比较多, 更差的是在删除时, 有可能一直要让旋转持续到根的位置
因此: 如果需要一种查询高效且有序的数据结构, 而且数据的个数为静态的(即不会改变), 可以考虑AVL树, 但一个结构经常修改, 就不太适合。