AVL树
相信学过数据结构的同学对于二叉搜索(排序)树一定不陌生,二叉搜索树是一颗左子节点比根节点小,右子节点比根大且他的子树也满足这种规则的一颗二叉树。二叉搜索树从名字上就知道他是用来进行数据的搜索的,理论上来讲他的搜索效率是log(n),但是事实上在某些极端情况下他好像显得并不那么有效。现在对空树插入一个有序的数列,你会发现他成为了一个单只树,最坏的搜索情况居然退化为o(n)。
其实会发生这一现象的根本原因是我们无法对树的形状进行控制,所以为了解决这个棘手的问题,就从二叉搜索树衍生出了一颗效率更高的树AVL树,之所以叫这个名字是因为发明这颗树的俩个人名字缩写是AVL。
AVL树的概念
相信通过上面的了解,现在你已经知道了,AVL树实际上是普通搜索树的改良,所以AVL树也是一颗搜索树,那么为什么AVL树的效率会比普通搜索树好呢?这里我们又必须提到一个新的概念,平衡二叉树。平衡二叉树是指当前根节点的左子树与右子树高度差的绝对值不超过1,同时他的子树也要满足这一规则。(如下图)
现在我们可以对AVL树进行定义:AVL树是一颗高度平衡的二叉搜索树。
所以现在我们明确了AVL是怎么保证他的搜索效率的,但是AVL树到底是怎么进行形状的调整的,一起接着往下看。
AVL的平衡因子
为了保证,我们的树是平衡的,AVL树引入了平衡因子的概念。每个节点都拥有自己的平衡因子,平衡因子的计算方式是右子树的高度减去左子树的高度,且平衡因子只有可能是-1,0,1三个数字。
当平衡因子大于1或者小于负1时说明我们这颗树已经不是一颗平衡的二叉搜索树了。但是如果仅仅对树进行插入操作,我们一定会出现上图中第二种情况,此时出现平衡因子会变为2,不满足AVL的规则,所以这时我们就需要对树进行旋转操作。
AVL树的节点
在对树进行操作之前,我们暂且需要了解树节点的结构,结构体中_bf就是我们上面讲过的平衡因子,因为对树操作需要进行跟新平衡因子,所以我们多创建了一个指向父节点的指针。pair<K, V> _kv不过你不懂就把他理解为节点中存放的值。
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf;//平衡因子
pair<K, V> _kv;
AVLTreeNode(const pair<K, V>& kv)//进行初始化
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
, _kv(kv)
{}
};
AVL树的旋转
说了这么多,终于来到了AVL树的硬菜,AVL树的平衡因子就是帮助我们在进行对AVL树插入不满足AVL树规则后进行辅助旋转的一个标记。
1.AVL树的左单旋
AVL树的左单旋将会发生在下面的情况,节点上方代表平衡因子,节点中的值代表存储的val。可以看到此时节点1的平衡因子变成了2,不满足规则所以需要进行旋转。
旋转的规则:
- cur节点的左树成为parent节点的右树
- parent节点成为cur节点的左树
你会发现,经过下面的旋转之后,这颗树成为了一颗平衡的二叉搜索树,但是这只是最简单的情况。
如果此时在情况更复杂的一颗树中进行插入:
现在我们对他进行旋转:(现在图中9的左子树为空,但是有的情况是不为空的,这也就是为什么说cur的左需要成为parent的右)
其实经过上面的讲解我们可以将上面的过程可以化成一个笼统的抽象图:
不妨我们对照我们的图来刨析我们的代码:
void RotateL(Node* parent)//左单旋
{
Node* SubR = parent->_right;
Node* SubRL = SubR->_left;
parent->_right = SubRL;//parent的右等于SubR的左
if (SubRL)
{
SubRL->_parent = parent;//SubRL若不为空就让他指向parent
}
SubR->_left = parent;
Node* pNode = parent->_parent;//记录parent的父亲节点,SubR指向当前parent的父亲节点,parent可能还存在父亲
parent->_parent = SubR;
if (_root == parent)//判断当前parent如果为空那么新的_root就是SubR
{
_root = SubR;
}
else
{
if (pNode->_left == parent)
{
pNode->_left = SubR;
}
else
{
pNode->_right = SubR;
}
}
SubR->_parent = pNode;
parent->_bf = SubR->_bf = 0;//最后记得将平衡因子置为0
}
2.AVL树的右单旋
在拥有了左单旋的技能之后,右单旋的道理相同,我们直接画抽象图来表示:
来看看我们右单旋的代码:
与左单旋完全相同的道理,如果理解了左单旋,相信右单旋完全不在话下。
void RotateR(Node* parent)//右单旋
{
Node* SubL = parent->_left;
Node* SubLR = SubL->_right;
parent->_left = SubLR;
if (SubLR)
{
SubLR->_parent = parent;
}
SubL->_right = parent;
Node* pNode = parent->_parent;
parent->_parent = SubL;
if (parent == _root)
{
_root = SubL;
}
else
{
if (pNode->_left == parent)
{
pNode->_left = SubL;
}
else
{
pNode->_right = SubL;
}
}
SubL->_parent = pNode;
parent->_bf = SubL->_bf = 0;
}
3.AVL树AVL树的右左双旋
你可能会被双旋这个名词吓到,因为他看起来好像要比上面的步骤变得更难的,但是我想告诉你,如果你理解了单旋,面对双旋你一定会迎刃而解。让我们来看看双旋最简单的情况吧。
如果你插入的节点是这个样子的,那么恭喜你,你已经触发了双旋。
双旋的过程是这样的:先以3节点为轴进行右旋,然后以1节点为轴进行左旋。
说到这里你是不是觉得很简单,所以我们都非常高兴的写下了下面的代码:看上去好像没什么问题,但是不妨画个抽象图再来看看
void RotateRL(Node* parent)
{
RotateR(parent->_right);
RotateL(parent);
我们观察最后一部分,30节点的平衡因子最后变成了-1,但是我们上面的代码在调用左右单旋后将这三个节点的平衡因子都置成了,所以显然的是不对的
不仅仅是这样,当我们观察图的第二部分时,我们发现对于60节点插入在左侧同样会发生双旋,这时候又出现了下面的情况,此时我们发现30节点的平衡因子变成了0,90的平衡因子变成了1.
说到这里,右左双旋的情况就总结完了,我们发现双旋后需要进行对平衡因子进行重新调整,所以我们总结为下面三种情况
经过上面的分析,我们重新写出了一份双旋的代码:
void RotateRL(Node* parent)
{
Node* SubR = parent->_right;
Node* SubRL = SubR->_left;
int bf = SubRL->_bf;//记录右左双旋之前SubRL的平衡因子
RotateR(parent->_right);
RotateL(parent);
if (bf == 1)//如果之前为1,对照上图插入在SubRL右侧的情况
{
SubR->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)//如果之前为-1,对照上图插入在SubRL左侧的情况
{
SubR->_bf = 1;
parent->_bf = 0;
}
else if (bf == 0)//说明我自己就是刚刚插入的节点
{
parent->_bf = SubR->_bf = 0;
}
SubRL->_bf = 0;//SubRL最后的平衡因子总为0
}
4.AVL树AVL树的左右双旋
相信就算我没有讲,你现在已经学会了AVL树的左右双旋,AVL树的左右双旋也同上面一样有3种情况,笔者这里只给大家画出复杂抽象图的中的一种,其余的相信你听明白后一定可以解决。
附上代码:
void RotateLR(Node* parent)//对照抽象图动手画一画会变得非常明了
{
Node* SubL = parent->_left;
Node* SubLR = SubL->_right;
int bf = SubLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 1)
{
SubL->_bf = -1;
parent->_bf = 0;
}
else if (bf == -1)
{
SubL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 0)
{
parent->_bf = SubL->_bf = 0;
}
SubLR->_bf = 0;
}
到这里,AVL树的旋转就告一段落了,但是这篇文章的全文,这部分一定是最重要的,因为学习AVL树就是要明白他的底层是怎么实现并且调整的。
AVL树插入
上面讲了那么多,其实根本是为我们的插入操作做铺垫,要想正确的插入,我们就需要知道怎么跟新平衡因子,什么时候发生了旋转的操作。
直接拿出我们的代码进行刨析:(看起来长,但是思路理清也是非常容易理解的)
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 (cur->_kv.first > kv.first)//如果插入的值比当前节点的值小,就走左子树
{
parent = cur;
cur = cur->_left;
}
else if (cur->_kv.first < kv.first)//如果插入的值比当前节点的值大,就走右子树
{
parent = cur;
cur = cur->_right;
}
else
{
return false;//说明节点已经存在
}
}
cur = new Node(kv);
if (parent->_kv.first < kv.first)//判断当前cur节点应该链在上一个节点的哪边
{
parent->_right = cur;
cur->_parent = parent;//这里是三叉链,一定要链起来
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
//以上部分和普通树插入没有区别
--------------------------------------------------------------------------------------------------------
while (parent)//当parent为nullptr时不再更新
{
if (cur == parent->_right)//跟新平衡因子
parent->_bf++;
else
parent->_bf--;
if (parent->_bf == 0)//满足规则,结束
{
break;
}
else if (abs(parent->_bf) == 1)//高度变了,继续跟新
{
cur = parent;
parent = parent->_parent;
}
else if (abs(parent->_bf) == 2)//不满足规则,需要旋转
{
if (parent->_bf == 2)
{
if (cur->_bf == 1)//树呈 \ 状
{
RotateL(parent);//左单旋
}
else if (cur->_bf == -1)//树呈 > 状
{
RotateRL(parent);//右左双旋
}
}
else if (parent->_bf == -2)
{
if (cur->_bf == -1)//树呈 / 状
{
RotateR(parent);//右单旋
}
else if (cur->_bf == 1)//树呈 < 状
{
RotateLR(parent);//左右双旋
}
}
//旋转完成,不会影响上一层
break;
}
else
{
//出现这种情况说明平衡因子错误
assert(false);
}
}
return true;
}
根据我们的代码我们把平衡因子的更新带领大家再理一理:我们把插入的节点叫cur,他的父亲叫parent
- 如果cur插入后parent的平衡因子变成0,说明之前parent的平衡因子不是1就是-1,相当于我们把之前左或者右的空缺补上了,对于上一层来说子树的高度没有变化,所以直接break
- 如果cur插入后parent的平衡因子绝对值变成1,说明parent之前平衡因子为0,这时候对于parent往上的树来说他们的子树高度发生了变化,所以需要接着迭代着向上更新
- 如果更新的过程中parent的平衡因子绝对值变成了2,那么说明这颗树已经不满足平衡树的规则,这时候就需要发生旋转,旋转就是对照着上面的图中的那几种情况
AVL树的删除
AVL树的删除可以说是比插入旋转更难的一个操作,这里我们就不详细讲解了,但是思路可以提一下。对于一棵AVL树的删除实它跟搜索树的删除很类似就是寻找一个替换节点然后删掉它的替换节点。
删除分三个情况:
-
del的左为空.
-
del的右为空.
-
del的左右都不为空
删除之后我们需要进行调节平衡因子,这里与插入刚好相反:
- 如果cur删除后parent的平衡因子变成0,说明之前parent的平衡因子不是1就是-1,相当于我们把之前长出来的那一边删除了,对于上一层来说子树的高度发生了变化,所以需要接着迭代着向上更新
- 如果cur插入后parent的平衡因子绝对值变成1,说明parent之前平衡因子为0,相当于我们把两个左右节点的其中一个删除了,对于一层来说子树的高度没有变化,所以直接结束
ps:删除操作读者可以仅仅作为了解,如果想更进一步进行了解删除可以去《算法导论》找找答案。
AVL树的验证
到这里AVL树就接近尾声了,但是对于我们构建的树我们有必要进行测试,看他到底满不满足条件,所以我们打印他的中序遍历看是否则有序,且根据接口判断他是否为平衡树.
bool _IsBlanace(Node* root)//代码就不在详细解答了,一个简单的判断问题
{
if (root == nullptr)
return true;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return abs(leftHeight - rightHeight) < 2 && _IsBlanace(root->_left) && _IsBlanace(root->_right);
}
int _Height(Node* root)//求树的高度
{
if (root == nullptr)
return 0;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return rightHeight > leftHeight ? rightHeight + 1 : leftHeight + 1;
}
总结
AVL树是我们高阶的数据结构中一个非常重要的数据结构,他的查找复杂度基本是稳定的log(n),所以效率非常的高,你可以想想在中国15亿人口中,用AVL树查找只需要30~31次,这是多么惊人的速度。but,这棵树也具有一个很大的缺陷,他会频繁的触发旋转,导致效率没有想象中的那么完美,所以之后又衍生出了更具有使用价值的红黑树,但是要想学习红黑树,一定要搞明白AVL树中的旋转操作,他们的旋转操作几乎完全相同。
这篇博客写了很久很久,这个图真的是非常难画(猛虎落泪),希望可以帮助到阅读的同学,如果博客中有什么问题请提出,我一定虚心改正。
AVL树完整代码
这里只将AVL树构建和验证的代码贴出来(emmm,主要是太长了),其他代码读者可以自己尝试实现。
#include<iostream>
#include<windows.h>
#include<assert.h>
using namespace std;
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf;
pair<K, V> _kv;
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
, _kv(kv)
{}
};
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
AVLTree()
:_root(nullptr)
{}
~AVLTree()
{
DestoryTree(_root);
_root = nullptr;
}
void DestoryTree(Node* root)//销毁树
{
if (root == nullptr)
return;
DestoryTree(root->_left);
DestoryTree(root->_right);
delete root;
}
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 (cur->_kv.first > kv.first)//如果插入的值比当前节点的值小,就走左子树
{
parent = cur;
cur = cur->_left;
}
else if (cur->_kv.first < kv.first)//如果插入的值比当前节点的值大,就走右子树
{
parent = cur;
cur = cur->_right;
}
else
{
return false;//说明节点已经存在
}
}
cur = new Node(kv);
if (parent->_kv.first < kv.first)//判断当前cur节点应该链在上一个节点的哪边
{
parent->_right = cur;
cur->_parent = parent;//这里是三叉链,一定要链起来
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
while (parent)//当parent为nullptr时不再更新
{
if (cur == parent->_right)//跟新平衡因子
parent->_bf++;
else
parent->_bf--;
if (parent->_bf == 0)//满足规则,结束
{
break;
}
else if (abs(parent->_bf) == 1)//高度变了,继续跟新
{
cur = parent;
parent = parent->_parent;
}
else if (abs(parent->_bf) == 2)//不满足规则,需要旋转
{
if (parent->_bf == 2)
{
if (cur->_bf == 1)//树呈 \ 状
{
RotateL(parent);
}
else if (cur->_bf == -1)//树呈 > 状
{
RotateRL(parent);
}
}
else if (parent->_bf == -2)
{
if (cur->_bf == -1)//树呈 / 状
{
RotateR(parent);
}
else if (cur->_bf == 1)//树呈 < 状
{
RotateLR(parent);
}
}
//旋转完成,不会影响上一层
break;
}
else
{
//出现这种情况说明平衡因子错误
assert(false);
}
}
return true;
}
void RotateR(Node* parent)//右单旋
{
Node* SubL = parent->_left;
Node* SubLR = SubL->_right;
parent->_left = SubLR;
if (SubLR)
{
SubLR->_parent = parent;
}
SubL->_right = parent;
Node* pNode = parent->_parent;
parent->_parent = SubL;
if (parent == _root)
{
_root = SubL;
}
else
{
if (pNode->_left == parent)
{
pNode->_left = SubL;
}
else
{
pNode->_right = SubL;
}
}
SubL->_parent = pNode;
parent->_bf = SubL->_bf = 0;
}
void RotateL(Node* parent)//左单旋
{
Node* SubR = parent->_right;
Node* SubRL = SubR->_left;
parent->_right = SubRL;
if (SubRL)
{
SubRL->_parent = parent;
}
SubR->_left = parent;
Node* pNode = parent->_parent;//记录当前parent的父亲节点,以便让SubR指向当前parent的父亲节点
parent->_parent = SubR;
if (_root == parent)
{
_root = SubR;
}
else
{
if (pNode->_left == parent)
{
pNode->_left = SubR;
}
else
{
pNode->_right = SubR;
}
}
SubR->_parent = pNode;
parent->_bf = SubR->_bf = 0;
}
void RotateLR(Node* parent)
{
Node* SubL = parent->_left;
Node* SubLR = SubL->_right;
int bf = SubLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 1)
{
SubL->_bf = -1;
parent->_bf = 0;
}
else if (bf == -1)
{
SubL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 0)
{
parent->_bf = SubL->_bf = 0;
}
SubLR->_bf = 0;
}
void RotateRL(Node* parent)
{
Node* SubR = parent->_right;
Node* SubRL = SubR->_left;
int bf = SubRL->_bf;//记录右左双旋之前SubRL的平衡因子
RotateR(parent->_right);
RotateL(parent);
if (bf == 1)
{
SubR->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
SubR->_bf = 1;
parent->_bf = 0;
}
else if (bf == 0)//说明我自己就是刚刚插入的节点
{
parent->_bf = SubR->_bf = 0;
}
SubRL->_bf = 0;
}
void Inorder()
{
_Inorder(_root);
cout << endl;
}
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first << " ";
_Inorder(root->_right);
}
bool IsBlanace()//判断树是否为平衡树
{
return _IsBlanace(_root);
}
bool _IsBlanace(Node* root)
{
if (root == nullptr)
return true;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return abs(leftHeight - rightHeight) < 2 && _IsBlanace(root->_left) && _IsBlanace(root->_right);
}
int _Height(Node* root)//求树的高度
{
if (root == nullptr)
return 0;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return rightHeight > leftHeight ? rightHeight + 1 : leftHeight + 1;
}
private:
Node* _root;
};