1. AVL树的概念
1.1. 引入
前面我们简单学习了并实现了二叉搜索树,二叉搜索树可以提高查找的效率,但是有些特殊情况下,二叉搜索树会出现问题。
我们知道,单纯的二叉搜索树,时间复杂度可能不是 log(n), 而是 n, 因为如果在插入数据有序的情况下,二叉搜索树会退化成单支树(单链表),查找元素就会使效率低下。
因此,两位俄罗斯的两位数学家,,G.M.Adelson-Velskii 和 E.M.LANDIS 在1962年发明了一种解决这种问题的方法:当向搜索二叉树中插入新节点后,如果能保证每个节点的左右高度差绝对值不超过 1(超过了则需要对树中节点进行调整),即可降低树的高度,从而减少平均搜索长度。
1.2. 概念
- 它的左右子树都是 AVL树
- 左右子树的高度差(简称平衡因子)的绝对值不超过 1(-1/0/1)
平衡因子:右子树高度 - 左子树高度
比如这里的搜索二叉树,其每个节点的(右子树高度-左子树高度) 的绝对值都是小于等于 1 的。这就是一棵 AVL树
如果一棵 AVL 树有 n 个节点,其高度可以保持在 log(n)。
注意这里为什么平衡因子可以是 -1 和 1,而不是所有平衡因子都是 0?
有些情况下做不到左子树和右子树高度相等:
当前两层的三个节点插满后,想要插入第 4 个节点,就必须有一个节点的平衡因子发生变化,如果平衡因子只能是 0 ,那么这里就无法在不改变平衡因子的条件下插入新节点。
同时还要注意,平衡因子并不是必须的。
平衡因子的作用是辅助我们判断节点的左右子树高度差,但是并不是个必需品,如果有其他更方便的方式判断节点左右子树高度差,那么也可以不需要平衡因子的辅助。
AVL 树是高度平衡的二叉搜索树(这里的高度就是平衡因子,子树的高度差)
其实 -1, 1 的情况也就是向下多了一层,对于查找来说影响并不大。
2. 简单的模拟实现
2.1. 节点
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode* _left;
AVLTreeNode* _right;
AVLTreeNode* _parent;
pair<K, V> _KV;
int _bf;
}
这里有些地方和前面二叉搜索树不同。
_parent,当插入或者删除节点时,我们需要更新平衡因子,不只是当前插入/删除节点的平衡因子,还有对应父节点的平衡因子,这里的 _parent 方便我们向上查找父节点。
_bf ,balance factor 平衡因子,辅助我们判断左右子树高度差。
2.2. insert
AVL树本质上还是一棵二叉搜索树,所以当插入节点时,还是需要先完成查找的工作
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
bool insert(const K& key, const V& value)
{
if(_root == nullptr)
{
_root = new Node(key, value);
}
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
if(cur->_kv.first < key)
{
parent = cur;
cur = cur->_right;
}
else if(cur->_kv.first > key)
{
parent = cur;
cur = cuur->_left;
}
else
{
return false;
}
}
cur = new Node(key, value);
if(parent->_kv.first < key)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
}
private:
Node* _root;
}
这一步我们完成了插入,但是光完成插入还远远不能满足 AVL树的结构,接下来我们需要观察,当 左右子树高度发生变化时,我们需要怎么调整。
这里我们需要分情况来讨论:
当插入节点在 8 的左边时,我们看到父节点平衡因子需要 -1, 除此之外,并不需要对 8 的父节点进行修改。
-1 后,我们发现,树的高度未发生变化,所以当平衡因子更新后,节点平衡因子绝对值小于等于 1 的,就不需要多余的修改。
如果是下面这种情况呢?
在 9 的右孩子位置插入节点。
9 的平衡发生变化,9 的平衡因子需要改变,同时影响了 8, 使 8 的平衡因子发生改变, 又向上影响,使 7 的平衡因子发生改变…
这样 9改变-> 8改变-> 7改变-> 5改变
这里我们在处理平衡因子时,要注意:
- 如果子树的高度不变,就不会继续往上影响祖先
- 如果子树的高度改变,就会继续往上影响祖先
新插入的节点平衡因子都是 0
插在左孩子节点,bf–, 插在右孩子节点 bf++
平衡因子的修改大概是这个道理,但是平衡因子的绝对值 >= 2 了怎么办?
此时说明树的结构出现问题,就需要我们修改树的结构。
我们先写出修改平衡因子的这段代码
平衡因子的修改分为下面 3 种情况。
- 父亲 bf 更新后 为 0, 父亲所在的子树高度不变,不需要往上更新,插入结束,在插入节点前,父亲的平衡因子为 -1/1 ,一边高一边低,新插入的节点填上了低的那一边。
- 父亲 bf 更新后 为 1/-1 ,插入前平衡因子为 0, 父亲所在的高度发生变化,必须往上继续更新
- 父亲 bf 更新后为 2/-2 , 父亲所在的子树违反规则,需要调整处理
while(parent)
{
if(cur == parent->_left)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
if(parent->_kv == 0)
{
break;
}
else if(parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
}
else
{
//开始修改
}
}
这里判断循环的条件不是很好取,所以我们取极端点的情况,如果 parent 一直向上找,找到了 _root, 但是 _root 的 parent 是空指针,_root 的左右子树中所有节点,其_parent 都不为空,只有 _root 的 _parent 为空,所以我们就按照 parent == nullptr 作为循环的出口。
2.2.1. 左单旋
上面我们把平衡因子处理后,发现结构出现了问题,此时就需要去修改结构。
这里先看第一种情况:
当出现某一子树的结构是这样时,1 的平衡因子为 2,就需要旋转。
旋转一定是 左右两边不均衡(平衡因子绝对值大于1)
旋转目的:
- 左右均衡
- 保持搜索树的规则
这里我们需要找一个数,让这个数作为这棵子树的根,同时还要满足上面旋转的目的。
此时我们看见这里的 2,好像比较适合,把 1 放在 2 的左子树, 3 放在 1 的右子树
这样旋转一下就可以了,然后把 1, 2 的平衡因子修改一下即可(3本来就是 2 的右子树,所以 3 可以不动,3也就不需要修改平衡因子)
这种简单的大概知道怎么旋转后,我们看个稍微复杂点的
这棵树,9 的平衡因子为 2 ,结构出现问题。
首先还是,先找到需要这里谁最适合当根,9 的左子树比 右子树低 2, 所以不可能在 9 的左子树去找,只能去右子树上找, 18 右子树高度也只有 1 ,不合适,所以我们还是选 15。
此时就要注意怎么旋转
这里,要把 15 的左子树给 9 的右子树,再让 15 的左孩子指针指向 9,转化成这个结构
这就是左单旋,把新父亲的左孩子指向原父亲,把新父亲的左子树给原父亲的右子树。
其实上面 那个简单的情况也能这样处理(n看做空指针)
新节点插入较高右子树的右侧 – 右右左单旋
我们研究一下,为什么右边多插入一个节点就能平衡了,是不是对所有在 满足“右右”的树 都适合
插入前, a/b/c 是高度为 h 的 AVL子树(h>=0)
h == 0 :
60 的右节点是新插入的节点。
h == 1:
h == 1 和 h == 0 的情况还好,情况都比较固定,但是 h == 2 的 AVL 树 情况就复杂了,光底层的节点情况就有 3 种
先把这三种情况设为 x, y, z。
对应这个图中,a,b 可以是 x,y, z 中的任意一种,但是 c 一定是 x。
c 要满足的是,在右边插入一定会高
注:这里我们只关注插入在 c 位置的下方,主要是 60 的右子树高度发生了变化。
这里a, b 的位置 x, y,z 都可以,所以要是对这些情况排列组合,起码要 9 种情况。同时 c 如果是 x 的结构,在 c 下面插入节点,还分了 4 种情况
合计h == 2 的情况,有 36 种树的结构。
但是不管高度为多少,我们主要关注的还是 30 和 60 这两个节点。
通过旋转,让 30 这个节点的左树向下移动,让30原本的右子树整体向上移动。
旋转细节:
- b 变成 30 的右子树
- 30 变为 60 的左子树
- 60 称为这棵树的根
2.2.2. 左单旋实现
简单知道实现逻辑,下面就开始实现左单旋
void Rotalel(Node* parent)
{
Node* subR = parent;
Node* subRL = subR->_left;
subR->_left = parent;
parent->_right = subRL;
}
按照逻辑我们先这样写,但是这样写对吗?
我们一方面要考虑代码正确性,同时还要思考,会不会出现特殊情况需要特殊处理。
- 这里我们用的是三叉连,除了直接改变节点,还要改变节点的各个指针。
subR->_left = parent;
parent->_parent = subR;
parent->_right = subRL;
subRL->_parent = parent;
三叉连在这里这样处理
但是还存在特殊情况,subRL 为 空
subRL 为空时,我们直接使用了 subRL 的 _parent 就会导致代码崩溃,所以这里要特殊处理一下。
除此之外,parnet 可能是子树,也可能是_root 节点
这里也需要特殊处理
void Rotatel(Node* parent)
{
Node* subR = parent;
Node* subRL = subR->_left;
subR->_left = parent;
if(parnet != _root)
{
parent->_parent = subR;
}
parent->_right = subRL;
if(subRL != nullptr)
{
subRL->_parent = parent;
}
}
经过处理,我们的结构变成了这个样子,但是 subR 的 parent 节点怎么处理?
在旋转中,30 可能是 root, 也可能不是,如果是 root,直接把 subR 的 parent 置空,如果不是空,就需要修改。
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* parentParent = parent->_parent;
parent->_right = subRL;
subR->_left = parent;
parent->_parent = subR;
if(subRL)
{
subRL->_parent = parent;
}
if(parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if(parentParent->_left == parent)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
parent->_bf =subR->_bf = 0;
}
注意这里的细节:
- subRL 是否为空
- parent 是不是根节点
- subR 要插在根节点的哪个位置
- 旋转后平衡因子的 更新
这里平衡因子的更新要留意
我们进入左旋时,是 parent->_bf == 2,subR->_bf == 1
这种情况才能进行左旋,所有路径高度我们都能确定,所以这里我们旋转后的高度,就按照上图所示的来判断即可。
2.2.3. 右单旋
还是相同的图,还是相同的逻辑,只不过这次是
左左右单旋- 左子树中左边高的位置插入节点,需要右单旋。
这里因为和 右右左单旋 类似,就是个镜面翻转,不做细讲。
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
parent->_left = subLR;
if(subLR)
{
subLR->_parent = parent;
}
if(parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if(parentParent->_left == parent)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
parent->_bf = subL->_bf = 0;
}
2.2.4. 左旋右旋条件
上面我们把左旋右旋的代码写好了,但是在 insert 函数里,什么时候去使用这两个函数呢?
我们看到左单旋的条件是 parent->_bf == 2, subR->_bf = 1。
而在 insert 函数里我们没有 subR,我们有 cur,所以进入左单旋的条件
else if(parent->_bf == 2 || parent->_bf == -2)
{
if(parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent);
}
}
满足上面这个条件,这里一定是左单旋。
右单旋镜面翻转一下
else if(parent->_bf == 2 || parent->_bf == -2)
{
if(parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent);
}
if(parent->_bf== -2 && cur->_bf == -1)
{
RotateR(parent);
}
}
旋转目的:
- 从不平衡,变成了平衡子树
- 旋转的本质,降低了高度
2.2.5.左右双旋
上面两种情况很简单,左树左边高左边插节点,右树右边高右边插节点,但是如果是下面的情况呢?
这里,我们是在右树的左子树插入节点
这里的 b 变成了 h + 1
我们进行左单旋先看看
旋转完后,我们发现,b 的位置并没有改变,不仅如此,60 的左子树高度为 h + 2, 右子树高度为 h,高度差从2变为了 -2。
这里左单旋并不能改变不平衡的问题,不过按照这里的结构,我们把 b 细分一下
注:我们注重的是在 90 的左子树插入节点导致平衡改变,不止是在 b, c 上插入节点。
这里,a,d是高度为 h 的 AVL 树,b, c 高度是 h - 1 的 AVL树(也可以是空树h>=1)
如果 h == 0
这种情况就和上面左单旋看起来类似,只是此时 60 就是新插入的节点。
但是 h == 1, h == 2
首先,上图中的 a,d 可以是 x, y, z 中的任意一种,但是 c, d 只能是 x(因为要保证在c,d任意位置插入节点都能去旋转)
所以,当 h == 2 时,这里可能组成的树有 3 x 3 x 4 = 36种。
如果 h == 3 时,情况更多,所以我们还是需要找规律来旋转。
h == 1 时,60 这个节点就是新增的
h == 2 使,b,c就是新插入的节点。
我们采用的方法:
- 90 为旋转点进行右单旋
- 20 为旋转点进行左单旋
第一次 90 左单旋,使所有高的位置都在节点的右边,然后再从 30 这里进行右单旋。
旋转后,我们发现 60 的左子树分到了 30 的右边,60 的右子树分到了 90 的左边。
如果这里是 c 节点下面插入 在这里处理方式一样,只要是在 60 下面插入(包括60),都要 进行左右双旋
最简单的情况,60 就是新插入的节点,还是相同的思路,先对 90 进行左单旋,然后对 30 进行右单旋。
理论成立,开始实现
注:因为需要调整平衡因子,所以这里我们通过函数来实现左右双旋和右左双旋
void RotateRL(Node* parent)
{
RotateR(parent->_right);
RotateL(parent);
}
前面我们实现了左单旋和右单旋,这里我们直接复用就行了,最主要的还是平衡因子的处理
观察上图变化过程,我们知道,参与旋转的有三个节点,parent, parent->_right, parent->_right->_left。
所以我们主要修改这三个节点的平衡因子
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
RotateR(subR);
RotateL(parent);
//...
}
该处理平衡因子了,第一个问题:插入的节点在 b 还是在 c 上?
这里我们看见,当插入在b,c 上,结果是不同的
所以在修改前,我们要想办法记录在哪个位置插入的节点。
最简单的方法就是,看平衡因子,插入节点后,我们对应的 subRL 的平衡因子会改变,我们可以记录插入后的 subRL 的平衡因子来判断在什么位置插入的节点。
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(subR);
RotateL(parent);
if(bf == 0)
{
parent->_bf = subR->_bf = subRL->_bf = 0;
}
else if(bf == -1)
{
parent->_bf = 0;
subRL->_bf = 0;
subR->_bf = 1;
}
else if(bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else
{
assert(false);
}
}
注意这里分的四种情况:
第2种和 第3种根据上面图片可以观察出来。
第1种,bf == 0,cur 就是新增节点,就不存在 cur 哪边高的问题
第4种,当 bf 不属于上面任何一种情况,说明 bf 出现了问题,出现了不该出现的值,直接报错处理。
else if(parent->_bf == 2 || parent->_bf == -2)
{
if(parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent);
}
else if(parent->_bf == -2 && parent->_bf == -1)
{
RotateR(parent);
}
else if(parent->_bf == 2 && cur->_bf == 1)
{
RotateLR(parent);
}
else if(parent->_bf == 2 && cur->_bf == 1)
{
RotateLR(parent);
}
//...
break;
}
- 旋转让这棵子树平衡
- 旋转降低了树的高度,恢复到和插入前一样的高度,所以对上一层没有影响,不需要向上继续更新。
2.2.6.左右双旋
左右双旋和右左双旋的逻辑基本一致,就是右左双旋的一个对称处理
情况二和情况三,需要注意平衡因子的变化,剩下两种情况和上面右左双旋一致
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR;
RotateL(subL);
RotateR(parent);
if(bf == 0)
{
parent->_bf = subL->_bf = subLR->_bf = 0;
}
else if(bf == -1)
{
parent->_bf = 1;
subLR->_bf = 0;
subL->_bf = 0;
}
else if(bf == -1)
{
parent->_bf = 0;
subLR->_bf = 0;
subL->_BF = -1;
}
else
{
assert(false);
}
}
insert 全部代码
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)
{
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 (parent->_kv.first < kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
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 = 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;
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
subR->_left = parent;
Node* parentParent = parent->_parent;
parent->_parent = subR;
if (subRL)
{
subRL->_parent = parent;
}
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
parent->_bf = subR->_bf = 0;
}
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;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
subL->_bf = parent->_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)
{
//subRL 自己就是新增
parent->_bf = subR->_bf = subRL->_bf = 0;
}
else if (bf == -1)
{
// subRL 左子树新增
parent->_bf = 0;
subRL->_bf = 0;
subR->_bf = 1;
}
else if (bf == 1)
{
//subRL 右子树新增
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else
{
assert(false);
}
}
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 0)
{
parent->_bf = subL->_bf = subLR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
subLR->_bf = 0;
subL->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else
{
assert(false);
}
}
2.3. 测试输出
2.3.1. 简单遍历
还是先遍历一遍,看看有没有什么问题
遍历还是中序遍历,注意写法即可
void InOrder()
{
_InOrder(_root);
}
private:
void _InOrder()
{
if(root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_kv.first << " ";
_InOrder(root->_right);
}
test_AVLTree()
{
int a[] = {16, 3, 7, 11, 9, 26, 18, 14, 15};
AVLTree<int, int> at1;
for(auto e : a)
{
at1.Insert(make_pair(e,e));
}
at1.InOrder();
}
我们可以看到这里的输出结果,是按照升序来的,这能说明我们的树是 AVL 树吗?
不能,从遍历结果来看,这只能说明树是一搜索二叉树,是不是 AVL树还需要验证。
2.3.2. AVL树的检查
我们需要一个函数来判断,当前的树是不是 AVL树
每个节点都有平衡因子。能不能直接通过平衡因子来判断?
不行,平衡因子是一个辅助因素,只能作为参考高度的依据,不能作为直接判断高度的依据。如果我们代码中修改平衡因子的地方出现问题,全部都是1,能说明我们的树是AVL树吗?
所以这里我们需要计算高度的函数,通过这个函数计算左右子树的高度,来判断当前的树是不是 AVL 树
bool isbalance()
{
return _isbalance();
}
private:
bool _isbalance(Node* root)
{
if(root == nullptr)
{
return false;
}
int leftheight = _hieght(root->_left);
int rightheight = _height(root->_right);
return (abs(rightheight - leftheight) < 2 && _isbalance(root->_left) && _isbalance(root->_right));
只要我们的求高度函数写对了,这里检查 AVL 树的逻辑就不会有什么问题。(abs求绝对值)
下面开始写检查高度的代码
bool isbalance()
{
return _isbalance(_root);
}
int height()
{
return _height(_root);
}
private:
bool _isbalance(Node* root)
{
if(root == nullptr)
{
return true;
}
int leftheight = _height(root->_left);
int rightheight = _height(root->_right);
if(rightheight - leftheight != root->_bf)
{
cout << root->_kv.first << "平衡因子异常" << endl;
cout << rightheight - leftheight << " " << root->_bf <<endl;
return false;
}
return (abs(rightheight - leftheight) < 2 && _isbalance(root->_left) && _isbalance(root->_right));
}
int _height(Node* root)
{
if(root == nullptr)
{
return 0;
}
int leftheight = _height(root->_left);
int rightheight = _height(root->_right);
return (leftheight < rightheight) ? (rightheight + 1) : (leftheight + 1);
}
我们现在来测试一下 树 是不是 AVL树
void test_AVLTree1()
{
int a1[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
xsz::AVLTree<int, int> t1;
for (auto e : a1)
{
if (e == 7)
{
int i = 0;
}
t1.insert(make_pair(e,e));
}
t1.InOrder();
cout << "is balance:" << t1.isbalance() << endl;
}
可以看到,这里是 1 说明树确实是 AVL 树,但是这能说明我们的代码对吗?
要想证明我们的代码没问题,仅仅只有这一串数字组成的树是无法证明的,所以我们需要大量的数据来测试
void test_AVLTree2()
{
const int N = 1000000;
vector<int> a1;
a1.reserve(N);
srand(time(0));
for (int i = 0; i < N; i++)
{
a1.push_back(rand());
}
xsz::AVLTree<int, int> t1;
for (auto e : a1)
{
t1.insert(make_pair(e, e));
}
cout <<"is balance:" << t1.isbalance() << endl;
cout <<"height:" << t1.height() << endl;
}
这里我们能看到,100w 的数据,这里也能平衡,同时树的高度是 18(这里的随机是伪随机,会产生相同的数据,但是相同数据不能插入,所以表面上是100w个数据,实际上少于100w的数据)
这里的 1w 个数据也才 16层
当我们插入 100w 个数据,测量插入数据,检测AVL树,查看树高的时间,我们看到,debug 下只用了 246ms,这速度,真的非常快
2.3.3. size
这个没什么说的,查看有多少个节点
size_t size()
{
return _size(root);
}
private:
size_t _size(Node* root)
{
if(root == nullptr)
{
return 0;
}
return _size(root->_left) +_size(root->_right) + 1;
}
除了这个递归的方法,其他方式都可以,比如在私有成员里加入 size 的成员变量,插入元素++,删除元素–。
这里能看到,100w 个数据,实际上就 3w 多个。
2.3.4. find
AVL树 也是搜索二叉树,查找逻辑就按照搜索二叉树来就行了
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;
}
只查找了1次,最多查找了18次,所以这里时间很快。
我们查找所有数据,debug 下用了 197ms,和插入速度不相上下。
2.3.5. erase
erase 主要作为了解内容
学习 erase 的难度比 insert 的难度更大,所以这里只做理解,不实现
首先,删除节点还是要先查找到节点,这里我们不建议使用find,因为我们删除节点后,还要对父节点进行操作,所以最好在 erase 内实现查找,并记录父节点。
比如这棵树,找到节点后,还要分为各种情况。
左为空,右为空,左右都不为空
删除 2
删除 2 后,1 的平衡因子变为 -1, 但是 1 这棵树高度不变,所以 3 及以上的节点平衡因子不需要更新。
删除 9
删除 9 后,8 的平衡因子发生改变,同时 8 这棵树的高度也发生改变,所以需要向上更新。
删除 6
删除 6 后,7 的平衡因子发生改变,变成 2,此时需要进行旋转,右右左单旋,旋转后, 8 这棵树的 bf == 0,向上更新,5 的平衡因子 -1,但是 5 这棵树整体高度不变,不需要向上更新。
AVL树的删除需要注意:
父节点的 平衡因子 与另一边的高度相关,删除后,根据 这棵树的另一棵子树结构来判断怎么旋转
AVL树在实践中用的不多,因为 AVL树 是 严格的高度搜索二叉树,由于只要出现 bf == 2/-2 的情况,就必须进行旋转,最后生成的树,最短路径和最长路径差值最多为 1 。
但是 AVL 树 插入,删除,查找的 时间复杂度都是 logN,这一点是完全优于一般的搜索二叉树的。