目录
1.AVL树的概念
AVL树——自平衡二叉搜索树。
二叉搜索树的数据若是有序或是接近有序,则会退化成单支树,相当于在顺序表中搜索元素,复杂度为 O(n) 。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:
向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之
差的绝对值不超过1(需要对树中的结点进行调整),即可保证为“对数高度”,从而降低树的高度,减少平均搜索长度。
AVL树所具备的性质
- 一棵AVL树的左右子树都是AVL树(注意空树是AVL树的一种)
- 左右子树的高度之差(平衡因子)的绝对值不超过1
一棵拥有n个结点的AVL树的搜索时间复杂度为 O(log2n)。
AVL树结点定义
我们实现 Key-Value 模型的AVL树,这里介绍AVL树的结点结构:
- 三叉链:三个指针分别执行父结点,左子树结点和右子树结点
- 键值对:作为数据存储的单元
- 平衡因子:记录当前结点的左右子树的高度差,(右子树高度-左子树高度)
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _parent;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
//存储键值对
pair<K, V> _kv;
//平衡因子
int bf;
AVLTreeNode(const pair<K,V>& kv):
_kv(kv),
_left(nullptr),
_right(nullptr),
_parent(nullptr),
bf(0)
{}
};
同时构建先出AVLTree类的框架,我们重点关注插入,查找和删除的接口
template <class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
AVLTree() :_root(nullptr)
{}
//拷贝构造和赋值重载都要深拷贝
~AVLTree()
{
//...
}
bool insert(const pair<K, V>& kv)
{
}
Node* Find(const K& key)
{
}
bool erase(const K& key)
{
}
private:
Node* _root;
};
2. AVL树的插入
- 按照二叉搜索树的规则,将新增结点插入到树中(小的往左,大的往右,或者逆序)。
- 更新平衡因子,因为平衡因子描述了当前子树的平衡状态,如果平衡因子的绝对值等于2,就需要进行旋转操作,来使树继续保持平衡(下面详述)。
由于一个结点的平衡因子是否需要更新,是取决于该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的直系祖先结点的平衡因子可能需要更新。
一个可能的新增的情况:
因此需要我们“顺藤摸瓜”往上更新平衡因子,我们将新增结点的父结点称为parent,更新规则如下:
- 新增结点在parent结点的右边,那么parent的平衡因子++。
- 新增结点在parent结点的左边,那么parent的平衡因子–。
更新parent的平衡因子后,还需要进行如下判断:
-
如果parent的平衡因子为1或者-1,还需继续往上更新平衡因子
🚩parent的左右高度差变大,说明新增的结点导致parent为根结点的子树高度增加,parent的父结点的平衡因子也会变化。
-
如果parent的平衡因子为0,则无需往上更新
🚩parent的左右高度达到平衡,说明新增的结点填补了高度差,原先的高度保持不变,所以对parent的父结点的平衡因子不产生影响。
-
如果parent的平衡因子为2或者-2,说明parent的平衡因子违反了AVL树的性质,需要对其进行旋转处理。
🚩当parent的平衡因子为±2时,cur的平衡因子一定是±1,而不会是0(因为cur如果为零,就停止了向上更新平衡因子)。
parent与cur两两组合下,共可能出现四种旋转情况:
- parent的平衡因子为 2 ,cur的平衡因子为 1 ——
左单旋
(函数名RotateL) - parent的平衡因子为 -2 ,cur的平衡因子为 -1 ——
右单旋
(函数名RotateR) - parent的平衡因子为 2 ,cur的平衡因子为 -1 ——
右左双旋
(函数名RotateRL) - parent的平衡因子为 -2 ,cur的平衡因子为 1 ——
左右双旋
(函数名RotateLR)
经过旋转处理之后的parent的平衡因子已经为0(下面会详述),无需往上更新,插入结点成功。
这里insert函数返回值的类型与map的insert的返回值一致,方便我们后续修改AVL树。
- parent的平衡因子为 2 ,cur的平衡因子为 1 ——
于是可先整理出插入结点的代码框架:
pair<Node* , bool > insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return make_pair(_root,true);
}
Node* cur = _root;
Node* parent=_root;
while (cur)
{
parent = cur;
if (kv.first > cur->_kv.first)
{
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
cur = cur->_left;
}
else
{
return make_pair(cur,false);
}
}
cur = new Node(kv);
Node* newnode = cur;
if (kv.first > parent->_kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
//更新平衡因子,检查是否需要旋转
while (parent)//最远可更新到_root
{
//更新平衡因子
if (cur == parent->_right)
parent->_bf++;
else
parent->_bf--;
//讨论平衡因子情况
if (parent->_bf == 0)
{
break;
}
else if(parent->_bf==1 || parent->_bf==-1)
{
//parent的高度增加,会影响 parent->_parent
//继续往上更新
cur = parent;
parent = parent->_parent;
}
else if(parent->_bf == 2 || parent->_bf == -2)
{
//parent所在子树已不平衡,旋转当前子树
if (parent->_bf == 2 )
{
if (cur->_bf == 1)
RotateL(parent);//左单旋
else//cur->_bf == -1
RotateRL(parent);//右左双旋
}
else //parent->_bf == -2
{
if (cur->_bf == -1)
RotateR(parent);//右单旋
else//cur->_bf == 1
RotateLR(parent);//左右双旋
}
break;//旋转处理后,parent的平衡因子已经为0
}
else
{
assert(false);//说明在插入此节点之前树已经不平衡了,需检查代码。
}
}
return make_pair(newnode,true);//插入成功
}
3.AVL树的旋转(出现不平衡子树)
前面谈的旋转是基于平衡因子的讨论,现在观察下造成“平衡破坏”的插入情况,可分为4种,
- 外侧插入
- parent(2),cur(1) —— 插入点在parent右子结点的右子树
- parent(-2),cur(-1) —— 插入点在parent左子结点的左子树
- 内测插入
- parent(2),cur(-1) —— 插入点在parent右子结点的左子树
- parent(-2),cur(1) —— 插入点在parent左子结点的右子树
单旋转
外侧插入的情况,仅需单旋转即可解决问题
左单旋
插入点在parent右子结点的右子树
为了调整状态,把cur向上提起,使parent自然向左侧下滑,由于cur的左子树需连接parent,所以先将cur的左子树挂到parent的右侧,这么做是因为,二叉搜索树的规则使我们知道 cur>parent,故cur左子树处于parent与cur之间,其也可以成为parent的右子树。
调整后cur成为该棵子树的根,由于cur平衡因子先前为1,而原来的父结点滑向了左子树,使得左高度+1,故此时cur的平衡因子为0,不用再往上更新平衡因子。
我们进一步抽象这种现象,得到普适的旋转情况:
不难理解,因为右边太重,所以需要左旋来维持平衡。
左单旋的代码如下
void RotateL(Node* parent)
{
Node* Pparent = parent->_parent;
Node* subR = parent->_right;
Node* subRL = subR->_left;
//parent 和 subR 的关系建立
parent->_parent = subR;
subR->_left = parent;
//parent 和 subRL 的关系建立
parent->_right = subRL;
if (subRL != nullptr)
{
subRL->_parent = parent;
}
//subR 和 Pparent 的关系建立
if (Pparent != nullptr)
{
subR->_parent = Pparent;
if (parent == Pparent->_left)
{
Pparent->_left = subR;
}
else
{
Pparent->_right = subR;
}
}
else
{
_root = subR;
subR->_parent = nullptr;
}
//更新结点平衡因子
subR->_bf = 0;
parent->_bf = 0;
}
右单旋
插入点在parent左子结点的左子树
需要右单旋的情况和左单旋正好呈镜像:
把cur向上提起,使parent自然向右侧下滑,由于cur的右子树需连接parent,所以先将cur的右子树挂到parent的左侧,这么做是因为,二叉搜索树的规则使我们知道 cur < parent,故cur右子树处于cur与parent之间,其也可以成为parent的左子树。
调整后cur成为该棵子树的根,由于cur的平衡因子先前为-1,而原来的父结点滑向了右子树,使得右高度+1,故此时cur的平衡因子为0,不用再往上更新平衡因子,而parent的左右子树高度都为h,平衡因子也为0。
我们进一步抽象这种现象,得到普适的旋转情况:
右单旋的代码如下:
void RotateR(Node* parent)
{
Node* Pparent = parent->_parent;
Node* subL = parent->_left;
Node* subLR = subL->_right;
// parent 与 subL的关系建立
subL->_right = parent;
parent->_parent = subL;
// parent 与 subLR 的关系建立
if (subLR != nullptr)
{
subLR->_parent = parent;
}
parent->_left = subLR;
// Pparent 与 subL的关系建立
if (Pparent != nullptr)
{
subL->_parent = Pparent;
if (Pparent->_left == parent)
{
Pparent->_left = subL;
}
else
{
Pparent->_right = subL;
}
}
else
{
_root = subL;
subL->_parent = nullptr;
}
//更新结点平衡因子
subL->_bf = 0;
parent->_bf = 0;
}
双旋转
内侧插入导致的不平衡状态,单旋转无法解决这种情况:❌
面对内侧的插入需要两次旋转来达成平衡
右左双旋
插入点在parent右子结点的左子树(可能为空树),这里不再举例而直接给出普适的抽象图:
旋转规则:
- subR向右旋,subRL向上提起,使subR成为subRL的右子树;
- subRL原先的右子树处于subRL和subR之间,故同时使其成为subR的左子树(注意橙色方框位置的变化)。
- parent向左旋,subRL向上提起,使parent成为subRL的左子树;
- subRL原先的左子树处于parent和subRL之间,故同时使其成为parent的右子树(注意蓝色方框的位置变化)
- 平衡因子的讨论
上面的两次单旋可以复用之前单旋的代码,然而双旋的难点在于讨论结点的平衡因子:
🚩注意:新增的结点在subRL的左子树还是右子树不影响旋转的方向,但是会影响旋转后的平衡因子,上图的的新增结点在subRL的右子树(平衡因子为1)这次新增结点在subRL的左子树(平衡因子为-1)。
可以看到旋转之后,subRL的平衡因子始终为0,但是parent和subR的平衡因子会因为新增因子在subRL的左右而导致有所不同。
还有一种情况:新增节点后subLR的平衡因子为0,也就是说subRL本身为新增结点(即subR左子树为空的情况),旋转后三者的平衡因子都为0。。(解释:如果subRL不是新增结点,而其平衡因子又为0,那么父结点subR的平衡因子就没有必要更新,于是parent的平衡因子就不会变成2(悖论)。而正是subRL自身为新增,父结点subR平衡因子更新,之后导致parent的平衡因子变为2)。
根据subRL的平衡因子的3种情况,更新平衡因子的规则如下:
- 🚩 subRL的平衡因子为0,旋转后parent的平衡因子为0,subR的平衡因子为0
- 🚩 subRL的平衡因子为1,旋转后parent的平衡因子为-1,subR的平衡因子为0
- 🚩 subRL的平衡因子为-1,旋转后parent的平衡因子为0,subR的平衡因子为1
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//两次单旋后会更改subRL的平衡因子,之后还需判断,所以先备份下来
int bf = subRL->_bf;
RotateR(subR);//先以subR为根右旋
RotateL(parent);//后以parent为根左旋
//更新平衡因子
subRL->_bf = 0;
if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf=1;
}
else
{
assert(false);
}
}
左右双旋
插入点在parent左子结点的右子树,与右左双旋的情况正好呈镜像关系
这里直接给出三种平衡因子的情况
旋转规则:
- subL向左单旋,subLR上抬,使subL成为subLR的左子树;
- subLR原先的左子树处于subLR和subL之间,故使其成为subL的右子树。
- parent向右单旋,subLR上抬,使parent成为subLR的右子树;
- subLR原先的右子树处于parent和subLR之间,故同时使其成为parent的左子树
- 平衡因子的讨论
根据subRL的平衡因子的3种情况,更新平衡因子的规则如下:
- 🚩 subRL的平衡因子为0,旋转后parent的平衡因子为0,subR的平衡因子为0
- 🚩 subRL的平衡因子为1,旋转后parent的平衡因子为0,subR的平衡因子为-1
- 🚩 subRL的平衡因子为-1,旋转后parent的平衡因子为1,subR的平衡因子为0
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
//两次单旋后会更改subLR的平衡因子,之后还需判断,所以先备份下来
int bf = subLR->_bf;
RotateL(subL);
RotateR(parent);
subLR->_bf = 0;
if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
}
else
{
assert(false);
}
}
4.AVL树的验证
遍历
通过中序遍历可以查看我们的这棵AVL树是否满足基本的二叉搜索树的性质:
public:
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_kv.first<<':'<<root->_kv.second<<endl;
_InOrder(root->_right);
}
验证平衡性
步骤:
- 计算每棵子树的高度
- 通过递归返回时,先判断左右子树是否为AVL树,不是则返回false;
- 再计算当前树的左右子树的高度差,若差值的绝对值超过2,则不是AVL树返回false。若小于2,那么返回true,同时通过输出型参数返回当前子树的高度给上一层。
- 用高度差来验证每个结点的平衡因子是否计算有误
public:
bool IsAVLTree()
{
int height = 0;
return _IsBalance(_root,height);
}
private:
bool _IsBalance(Node* root,int& height)
{
if (root == nullptr)
{
height = 0;
return true;
}
//判断左子树是否为AVL树,同时通过输出型参数得到左子树高度
int leftHeight = 0;
if (_IsBalance(root->_left, leftHeight) == false)
return false;
//判断右子树是否为AVL树,同时通过输出型参数得到右子树高度
int rightHeight = 0;
if (_IsBalance(root->_right, rightHeight) == false)
return false;
//检查平衡因子是否有误
if (rightHeight - leftHeight != root->_bf)
{
cout << root->_kv.first << " 的平衡因子应为:" << rightHeight - leftHeight << ",实为:" << root->_bf << endl;
return false;
}
//计算当前树的高度,左右子树的较高者+1
height = max(leftHeight, rightHeight) + 1;
return abs(rightHeight - leftHeight) < 2;
}
5.AVL树的查找
查找的规则符合二叉搜索树中查找原则:
public:
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key > cur->_kv.first)
{
cur = cur->_right;
}
else if (key < cur->_kv.first)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
6.AVL树的删除
首先搜索到要删除的结点,结点可能处于3种情况
- 为叶子结点,直接删除即可,随后再更新平衡因子。
- 是度为1的结点,即左子树或者右子树为空,需将父结点连向不为空的一侧的子树后再删除,随后更新平衡因子。
- 结点的度为2,执行替换删除,找到删除结点的左子树的最右结点,或者右子树的最左结点来替换掉删除结点的值。注意这些替换结点只可能为叶子结点或者度为1,删除这个替换结点的问题可转变为前两种情况,当然删完后需更新平衡因子。
所以删除节点的情况只能是删除叶子结点或度为1的结点。
更新平衡因子的手法是插入结点时的逆向操作:
- 删除的结点在parent的右边,parent的平衡因子–
- 删除的结点在parent的左边,parent的平衡因子++
parent的平衡因子在更新后有如下情况
- 🚩parent平衡因子==0,那么parent在删除结点前平衡因子为 ±1,说明之前parent子树的高度差被抹平了,parent子树整体高度-1,那会影响到parent的父结点及其祖先结点的平衡因子,因此需往上更新平衡因子。
- 🚩parent平衡因子==±1,那么parent在删除结点前平衡因子为 0,删除结点后,parent单侧子树少了一个结点,虽产生高度差,但是parent子树的高度没有改变,并不会影响到parent的父结点的平衡因子,故删除成功,无需往上更新平衡因子。
- 🚩parent平衡因子==±2,那么parent在删除结点前平衡因子为 ±1,说明删除结点导致了parent子树的不平衡,需要进行旋转处理,当然旋转处理后,该子树的高度会发生变化,仍需往上更新平衡因子。
旋转规则:
- parent平衡因子为 2 (右树subR更高)
- parent的右孩子平衡因子为1,进行左单旋,parent平衡因子为0,subR平衡因子为0,树的高度降低1,说明还需往上继续更新。
- parent的右孩子平衡因子为0,左单旋,parent平衡因子为-1,subR平衡因子为-1,旋转后树的高度没有变化,无需往上更新了。
- parent的右孩子平衡因子为-1,进行右左双旋,右左双旋的更新平衡因子有三种情况,已在上文的双旋中讨论,按需往上更新平衡因子。
左侧为镜像情况,这里简述
- parent平衡因子为-2 (左树subL更高)
- parent左孩子平衡因子为-1,进行右单旋,还需往上继续更新。
- parent左孩子平衡因子为1,进行左右双旋,根据旋后情况按需往上继续更新。
- parent左孩子平衡因子为0,进行右单旋,无需往上更新。
删除结点的代码如下
bool erase(const K& key)
{
if (_root == nullptr)
{
return false;
}
Node* cur = _root;//遍历
Node* parent = nullptr;
Node* delPos=nullptr;//标记删除结点
Node* delParentPos=nullptr;
while (cur)
{
if (cur->_kv.first > key)//key比当前结点小
{
parent = cur;
cur = cur->_left;//往左子树走
}
else if(cur->_kv.first < key)//key比当前结点大
{
parent = cur;
cur = cur->_right;//往右子树走
}
else//找到删除结点
{
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = _root->_right;
if (_root != nullptr)
_root->_parent = nullptr;
delete cur;
return true;
}
else
{
delParentPos = parent;//标记删除结点的父结点
delPos = cur;//标记为删除结点
}
break;//找到了删除点,需更新平衡因子
}
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = _root->_left;
if (_root != nullptr)
_root->_parent = nullptr;
delete cur;
return true;
}
else
{
delParentPos = parent;
delPos = cur;
}
break;
}
else//左右均不为空
{
//替换法删除,找到右子树中key最小的结点
Node* minParent = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
minParent = minRight;
minRight = minRight->_left;
}
cur->_kv.first = minRight->_kv.first;
cur->_kv.second = minRight->_kv.second;//替换
delParentPos = minParent;
delPos = minRight;//实际删除结点
break;
}
}
}
//找不到删除结点
if (cur == nullptr)
{
return false;
}
//下面开始更新平衡因子
cur = delPos;
parent = delParentPos;
while (parent != nullptr)//最坏情况更新到根结点
{
//更新结点平衡因子
if (cur == parent->_left)
{
parent->_bf++;
}
else if (cur == parent->_right)
{
parent->_bf--;
}
//旋转情况讨论
if (parent->_bf == 1 || parent->_bf == -1)
{
break;//高度没有变化,更新完毕
}
else if (parent->_bf == 0)
{
//需往上继续更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//开始旋转处理
//前两种情况旋转会为我们处理平衡因子,
//但是subR平衡因子为0的情况需要我们手动修改
if (parent->_bf == 2)
{
Node* subR = parent->_right;
if (subR->_bf == 1)//左单旋
{
RotateL(parent);
parent = subR;
}
else if (subR->_bf == -1)//右左双旋
{
Node* subRL = subR->_left;
RotateRL(parent);
parent = subRL;
}
else if (subR->_bf == 0)//左单旋
{
RotateL(parent);
//手动修改平衡因子
parent->_bf = 1;
subR->_bf = -1;
//旋后树高不变,无需再往上更新
break;
}
}
else if (parent->_bf == -2)
{
Node* subL=parent->_left;
if (subL->_bf == -1)//右单旋
{
RotateR(parent);
parent = subL;
}
else if (subL->_bf == 1)//左右双旋
{
Node* subLR = subL->_right;
RotateLR(parent);
parent = subLR;
}
else if (subL->_bf == 0)
{
RotateR(parent);
//手动修改平衡因子
parent->_bf = -1;
subL->_bf = 1;
//旋后树高不变,无需再往上更新
break;
}
}
//到了此处,说明还需要往上更新
cur = parent;
parent = parent->_parent;
}
else
{
assert(false);//删除前平衡因子就有错误
}
}
//进行删除,现在的删除结点的度<=1
if (delPos->_left == nullptr)//实际删除结点的左子树为空
{
if (delPos == delParentPos->_left)
{
delParentPos->_left = delPos->_right;
}
else if(delPos==delParentPos->_right)
{
delParentPos->_right = delPos->_right;
}
//由于删除结点的右树可能为空,需要判断
if (delPos->_right != nullptr)
{
delPos->_right->_parent = delParentPos;
}
}
else if (delPos->_right == nullptr)
{
//走到这说明删除节点的左子树不为空,右子树为空
if (delPos == delParentPos->_left)
{
delParentPos->_left = delPos->_left;
}
else if (delPos == delParentPos->_right)
{
delParentPos->_right = delPos->_left;
}
delPos->_left->_parent = delParentPos;
}
delete delPos;
delPos = nullptr;
return true;
}
7.AVL树的键值访问及修改
对insert的函数进行复用:
V& operator[](const K& key)
{
Node* ptr=insert(make_pair(key, V())).first;
return ptr->_kv.second;
}
测试
测试代码如下
void TestAVLTree()
{
int a[] = { 18,14,20,12,16,15};
AVLTree<int, int> T;
cout << "------insert-------" << endl;
for (auto& e : a)
{
T.insert(make_pair(e, e));
cout << e << ":" << T.IsAVLTree() << endl;
}
cout << "------modify-------" << endl;
T[18]*=10;
T[14]*=10;
T[12]*=10;
T.InOrder();
cout << "-------erase-------" << endl;
for (auto& e : a)
{
T.erase(e);
cout << e << ":" << T.IsAVLTree() << endl;
}
}
int main()
{
TestAVLTree();
return 0;
}
得到结果:
总结
AVL树始终保持了高度平衡,这样保证了数据搜索的高效性(log2N),但是每次插入删除的旋转操作,也会使其效率大打折扣。之后我会介绍红黑树,在略微损失数据查找的效率下,减少了大量的旋转操作,使其真正成为STL中的map和set的底层数据结构。
— end —
青山不改 绿水长流