AVL树的概念
二叉搜索树虽然可以提高我们查找数据的效率,但如果插入二叉搜索树的数据是有序或接近有序的,此时二叉搜索树会退化为单支树,在单支树当中查找数据相当于在单链表当中查找数据,效率是很低下的。
因此,两位俄罗斯的数学家G.M.A delson-V elskii和E.M.L andis在1962年发明了解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
AVL树可以是一棵空树,也可以是具有以下性质的一棵二叉搜索树:
- 树的左右子树都是AVL树。
- 树的左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/01)。
如果一棵二叉搜索树的高度是平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
O
(
l
o
g
N
)
O(logN)
O(logN),搜索时间复杂度也是
O
(
l
o
g
N
)
O(logN)
O(logN)。
注意: 这里所说的二叉搜索树的高度是平衡的是指,树中每个结点左右子树高度之差的绝对值不超过1,因为只有满二叉树才能做到每个结点左右子树高度之差均为0。
AVL树结点的定义
我们这里直接实现KV模型的AVL树,为了方便后续的操作,这里将AVL树中的结点定义为三叉链结构,并在每个结点当中引入平衡因子(右子树高度-左子树高度)。除此之外,还需编写一个构造新结点的构造函数,由于新构造结点的左右子树均为空树,于是将新构造结点的平衡因子初始设置为0即可。
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;
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{}
};
注意: 给每个结点增加平衡因子并不是必须的,只是实现AVL树的一种方式,不引入平衡因子也可以实现AVL树,只不过会麻烦一点。
AVL树的插入
AVL树插入结点时有以下三个步骤:
- 按照二叉搜索树的插入方法,找到待插入位置。
- 找到待插入位置后,将待插入结点插入到树中。
- 更新平衡因子,如果出现不平衡,则需要进行旋转。
因为AVL树本身就是一棵二叉搜索树,因此寻找结点的插入位置是非常简单的,按照二叉搜索树的插入规则:
- 待插入结点的key值比当前结点小就插入到该结点的左子树。
- 待插入结点的key值比当前结点大就插入到该结点的右子树。
- 待插入结点的key值与当前结点的key值相等就插入失败。
如此进行下去,直到找到与待插入结点的key值相同的结点判定为插入失败,或者最终走到空树位置进行结点插入。
与二叉搜索树插入结点不同的是,AVL树插入结点后需要更新树中结点的平衡因子,因为插入新结点后可能会影响树中某些结点的平衡因子。
由于一个结点的平衡因子是否需要更新,是取决于该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的祖先结点的平衡因子可能需要更新。 所以我们插入结点后需要倒着往上更新平衡因子,更新规则如下:
- 新增结点在parent的右边,parent的平衡因子
+
+
++
++。
- 新增结点在parent的左边,parent的平衡因子
?
?
--
??。
每更新完一个结点的平衡因子后,都需要进行以下判断:
- 如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子。
- 如果parent的平衡因子等于0,表明无需继续往上更新平衡因子了。
- 如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。
判断理由说明:
parent更新后的平衡因子 | 分析 |
---|
-1或1 | 只有0经过
?
?
--
??/
+
+
++
++操作后会变成-1/1,说明新结点的插入使得parent的左子树或右子树增高了,即改变了以parent为根结点的子树的高度,从而会影响parent的父结点的平衡因子,因此需要继续往上更新平衡因子。 | 0 | 只有-1/1经过
+
+
++
++/
?
?
--
??操作后会变成0,说明新结点插入到了parent左右子树当中高度较矮的一棵子树,插入后使得parent左右子树的高度相等了,此操作并没有改变以parent为根结点的子树的高度,从而不会影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。 | -2或2 | 此时parent结点的左右子树高度之差的绝对值已经超过1了,不满足AVL树的要求,因此需要进行旋转处理。 |
注意: parent的平衡因子在更新前只可能是-1/0/1(AVL树中每个结点的左右子树高度之差的绝对值不超过1)。
而在最坏情况下,我们更新平衡因子时会一路更新到根结点。例如下面这种情况: 说明一下: 由于我们插入结点后需要倒着往上进行平衡因子的更新,所以我们将AVL树结点的结构设置为了三叉链结构,这样我们就可以通过父指针找到其父结点,进而对其平衡因子进行更新。当然,我们也可以不用三叉链结构,可以在插入结点时将路径上的结点存储到一个栈当中,当我们更新平衡因子时也可以通过这个栈来更新祖先结点的平衡因子,但是相对较麻烦。
若是在更新平衡因子的过程当中,出现了平衡因子为-2/2的结点,这时我们需要对以该结点为根结点的树进行旋转处理,而旋转处理分为四种,在进行分类之前我们首先需要进行以下分析:
我们将插入结点称为cur,将其父结点称为parent,那么我们更新平衡因子时第一个更新的就是parent结点的平衡因子,更新完parent结点的平衡因子后,若是需要继续往上进行平衡因子的更新,那么我们必定要执行以下逻辑:
cur = parent;
parent = parent->_parent;
这里我想说明的是:当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。
理由如下: 若cur的平衡因子是0,那么cur一定是新增结点,而不是上一次更新平衡因子时的parent,否则在上一次更新平衡因子时,会因为parent的平衡因子为0而停止继续往上更新。 而cur是新增结点的话,其父结点的平衡因子更新后一定是-1/0/1,而不可能是-2/2,因为新增结点最终会插入到一个空树当中,在新增结点插入前,其父结点的状态有以下两种可能:
- 其父结点是一个左右子树均为空的叶子结点,其平衡因子是0,新增结点插入后其平衡因子更新为-1/1。
- 其父结点是一个左子树或右子树为空的结点,其平衡因子是-1/1,新增结点插入到其父结点的空子树当中,使得其父结点左右子树当中较矮的一棵子树增高了,新增结点后其平衡因子更新为0。
综上所述,当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。
根据此结论,我们可以将旋转处理分为以下四类:
- 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋。
- 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。
- 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。
- 当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。
并且,在进行旋转处理后就无需继续往上更新平衡因子了,因为旋转后树的高度变为插入之前了,即树的高度没有发生变化,也就不会影响其父结点的平衡因子了。具体原因请看后面的旋转讲解。
代码如下:
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->_left;
}
else if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
cur = new Node(kv);
if (kv.first < parent->_kv.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
while (cur != _root)
{
if (cur == parent->_left)
{
parent->_bf--;
}
else if (cur == parent->_right)
{
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)
{
if (cur->_bf == -1)
{
RotateR(parent);
}
else
{
RotateLR(parent);
}
}
else
{
if (cur->_bf == -1)
{
RotateRL(parent);
}
else
{
RotateL(parent);
}
}
break;
}
else
{
assert(false);
}
}
return true;
}
AVL树的旋转
左单旋
动图演示: 动图演示说明: 由于插入新结点后,可能并不会立即进行旋转操作,而可能是在更新其祖先结点的过程中出现了不平衡而进行的旋转操作,因此用长方形表示下面的子树。
旋转示意图如下: 左单旋的步骤如下:
- 让subR的左子树作为parent的右子树。
- 让parent作为subR的左子树。
- 让subR作为整个子树的根。
- 更新平衡因子。
左单旋后满足二叉搜索树的性质:
- subR的左子树当中结点的值本身就比parent的值大,因此可以作为parent的右子树。
- parent及其左子树当中结点的值本身就比subR的值小,因此可以作为subR的左子树。
平衡因子更新如下: 可以看到,经过左单旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以左单旋后无需继续往上更新平衡因子。
代码如下:
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* parentParent = parent->_parent;
parent->_parent = subR;
subR->_left = parent;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
if (parentParent == nullptr)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
subR->_bf = parent->_bf = 0;
}
注意: 结点是三叉链结构,改变结点关系时需要跟着改变父指针的指向。
右单旋
动图演示: 动图演示说明: 由于插入新结点后,可能并不会立即进行旋转操作,而可能是在更新其祖先结点的过程中出现了不平衡而进行的旋转操作,因此用长方形表示下面的子树。
旋转示意图如下: 右单旋的步骤如下:
- 让subL的右子树作为parent的左子树。
- 让parent作为subL的右子树。
- 让subL作为整个子树的根。
- 更新平衡因子。
右单旋后满足二叉搜索树的性质:
- subL的右子树当中结点的值本身就比parent的值小,因此可以作为parent的左子树。
- parent及其右子树当中结点的值本身就比subL的值大,因此可以作为subL的右子树。
平衡因子更新如下: 可以看到,经过右单旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以右单旋后无需继续往上更新平衡因子。
代码如下:
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 (parentParent == nullptr)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
subL->_bf = parent->_bf = 0;
}
注意: 结点是三叉链结构,改变结点关系时需要跟着改变父指针的指向。
左右双旋
动图演示: 动图演示说明:
- 由于插入新结点后,可能并不会立即进行旋转操作,而可能是在更新其祖先结点的过程中出现了不平衡而进行的旋转操作,因此用长方形表示下面的子树。
- 动图中,在b子树当中新增结点,或是在c子树当中新增结点,均会引发左右双旋,动图中以在b子树当中新增结点为例。
动图当中的旋转示意图如下: 1、插入新结点。 2、以30为旋转点进行左单旋。 3、以90为旋转点进行右单旋。 左右双旋的步骤如下:
- 以subL为旋转点进行左单旋。
- 以parent为旋转点进行右单旋。
- 更新平衡因子。
左右双旋后满足二叉搜索树的性质: 左右双旋后,实际上就是让subLR的左子树和右子树,分别作为subL和parent的右子树和左子树,再让subL和parent分别作为subLR的左右子树,最后让subLR作为整个子树的根(结合图理解)。
- subLR的左子树当中的结点本身就比subL的值大,因此可以作为subL的右子树。
- subLR的右子树当中的结点本身就比parent的值小,因此可以作为parent的左子树。
- 经过步骤1/2后,subL及其子树当中结点的值都就比subLR的值小,而parent及其子树当中结点的值都就比subLR的值大,因此它们可以分别作为subLR的左右子树。
左右双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况: 1、当subLR原始平衡因子是-1时,左右双旋后parent、subL、subLR的平衡因子分别更新为1、0、0。 2、当subLR原始平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0。 1、当subLR原始平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0。 可以看到,经过左右双旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以左右双旋后无需继续往上更新平衡因子。
代码如下:
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(subL);
RotateR(parent);
if (bf == 1)
{
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
else if (bf == -1)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 0)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
右左双旋
动图演示: 动图演示说明:
- 由于插入新结点后,可能并不会立即进行旋转操作,而可能是在更新其祖先结点的过程中出现了不平衡而进行的旋转操作,因此用长方形表示下面的子树。
- 动图中,在b子树当中新增结点,或是在c子树当中新增结点,均会引发右左双旋,动图中以在c子树当中新增结点为例。
动图当中的旋转示意图如下: 1、插入新结点。 2、以90为旋转点进行右单旋。 3、以30为旋转点进行左单旋。 右左双旋的步骤如下:
- 以subR为旋转点进行右单旋。
- 以parent为旋转点进行左单旋。
- 更新平衡因子。
右左双旋后满足二叉搜索树的性质: 右左双旋后,实际上就是让subRL的左子树和右子树,分别作为parent和subR的右子树和左子树,再让parent和subR分别作为subRL的左右子树,最后让subRL作为整个子树的根(结合图理解)。
- subRL的左子树当中的结点本身就比parent的值大,因此可以作为parent的右子树。
- subRL的右子树当中的结点本身就比subR的值小,因此可以作为subR的左子树。
- 经过步骤1/2后,parent及其子树当中结点的值都就比subRL的值小,而subR及其子树当中结点的值都就比subRL的值大,因此它们可以分别作为subRL的左右子树。
右左双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况: 1、当subRL原始平衡因子是1时,左右双旋后parent、subR、subRL的平衡因子分别更新为-1、0、0。 2、当subRL原始平衡因子是-1时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、1、0。 3、当subRL原始平衡因子是0时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、0、0。 可以看到,经过右左双旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以右左双旋后无需继续往上更新平衡因子。
代码如下:
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(subR);
RotateL(parent);
if (bf == 1)
{
subRL->_bf = 0;
parent->_bf = -1;
subR->_bf = 0;
}
else if (bf == -1)
{
subRL->_bf = 0;
parent->_bf = 0;
subR->_bf = 1;
}
else if (bf == 0)
{
subRL->_bf = 0;
parent->_bf = 0;
subR->_bf = 0;
}
else
{
assert(false);
}
}
AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,也就是说AVL树也是二叉搜索树,因此我们可以先获取二叉树的中序遍历序列,来判断二叉树是否为二叉搜索树。
代码如下:
void Inorder()
{
_Inorder(_root);
}
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first << " ";
_Inorder(root->_right);
}
但中序有序只能证明是二叉搜索树,要证明二叉树是AVL树还需验证二叉树的平衡性,在该过程中我们可以顺便检查每个结点当中平衡因子是否正确。
采用后序遍历,变量步骤如下:
- 从叶子结点处开始计算每课子树的高度。(每棵子树的高度 = 左右子树中高度的较大值 + 1)
- 先判断左子树是否是平衡二叉树。
- 再判断右子树是否是平衡二叉树。
- 若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树了)
代码如下:
bool IsAVLTree()
{
int hight = 0;
return _IsBalanced(_root, hight);
}
bool _IsBalanced(Node* root, int& hight)
{
if (root == nullptr)
{
hight = 0;
return true;
}
int leftHight = 0;
if (_IsBalanced(root->_left, leftHight) == false)
return false;
int rightHight = 0;
if (_IsBalanced(root->_right, rightHight) == false)
return false;
if (rightHight - leftHight != root->_bf)
{
cout << "平衡因子设置异常:" << root->_kv.first << endl;
}
hight = max(leftHight, rightHight) + 1;
return abs(rightHight - leftHight) < 2;
}
AVL树的查找
AVL树的查找函数与二叉搜索树的查找方式一模一样,逻辑如下:
- 若树为空树,则查找失败,返回nullptr。
- 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
- 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
- 若key值等于当前结点的值,则查找成功,返回对应结点。
代码如下:
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key < cur->_kv.first)
{
cur = cur->_left;
}
else if (key > cur->_kv.first)
{
cur = cur->_right;
}
else
{
return cur;
}
}
return nullptr;
}
AVL树的修改
方法一
实现修改AVL树当中指定key值结点的value,我们可以实现一个Modify函数,该函数当中的逻辑如下:
- 调用查找函数获取指定key值的结点。
- 对该结点的value进行修改。
代码如下:
bool Modify(const K& key, const V& value)
{
Node* ret = Find(key);
if (ret == nullptr)
{
return false;
}
ret->_kv.second = value;
return true;
}
方法二
还有一种方法就是模仿C++STL库当中map的实现方式,将插入函数的返回值设置为pair类型的,插入函数的返回值逻辑如下:
- 若待插入结点的键值key在map当中不存在,则结点插入成功,并返回插入后结点的指针和true。
- 若待插入结点的键值key在map当中已经存在,则结点插入失败,并返回map当中键值为key的结点的指针和false。
我们只需要对插入函数的返回值做一点点修改即可,代码如下:
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 = nullptr;
while (cur)
{
if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return make_pair(cur, false);
}
}
cur = new Node(kv);
Node* newnode = cur;
if (kv.first < parent->_kv.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
while (cur != _root)
{
if (cur == parent->_left)
{
parent->_bf--;
}
else if (cur == parent->_right)
{
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)
{
if (cur->_bf == -1)
{
RotateR(parent);
}
else
{
RotateLR(parent);
}
}
else
{
if (cur->_bf == -1)
{
RotateRL(parent);
}
else
{
RotateL(parent);
}
}
break;
}
else
{
assert(false);
}
}
return make_pair(newnode, true);
}
然后我们再对运算符[ ]进行重载,[ ]的重载逻辑如下:
- 调用插入函数插入键值对。
- 拿出从插入函数获取到的结点。
- 返回该结点value的引用。
这样一来,当我们使用[key] 时,其返回值逻辑如下:
- 如果key不在树中,则先插入键值对<key, V()>,然后返回该键值对中value的引用。
- 如果key已经在树中,则返回键值为key结点value的引用。
如此一来,我们既可以用[ ]来进行指定key值结点value的修改,又可以用[ ]进行数据的插入了,并且插入时更方便。
代码如下:
V& operator[](const K& key)
{
pair<Node*, bool> ret = Insert(make_pair(key, V()));
Node* node = ret.first;
return node->_kv.second;
}
AVL树的删除
要进行结点的删除,首先需要在树中找到对应key值的结点,寻找待删除结点的方法和二叉搜索树相同:
- 先找到待删除的结点。
- 若找到的待删除结点的左右子树均不为空,则需要使用替换法进行删除。
替换法删除指的是:让待删除结点左子树当中key值最大的结点,或待删除结点右子树当中值最小的结点代替待删除结点被删除(代码中以后者为例),然后再将待删除结点的key值以及value值都改为代替其被删除的结点的值即可。
也就是说,我们最终找到的实际被删除的结点的左右子树当中至少有一个为空树。
在找到实际需要被删除的结点后,我们先不进行实际的删除,而是先进行平衡因子的更新,不然后续更新平衡因子时特别麻烦(已经尝试过),而更新平衡因子时的规则与插入结点时的规则是相反的,更新规则如下:
- 删除的结点在parent的右边,parent的平衡因子
?
?
--
??。
- 删除的结点在parent的左边,parent的平衡因子
+
+
++
++。
并且每更新完一个结点的平衡因子后,都需要进行以下判断:
- 如果parent的平衡因子等于-1或者1,表明无需继续往上更新平衡因子了。
- 如果parent的平衡因子等于0,表明还需要继续往上更新平衡因子。
- 如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。
判断理由说明:
parent更新后的平衡因子 | 分析 |
---|
-1或1 | 只有0经过
?
?
--
??/
+
+
++
++操作后会变成-1/1,说明原来parent的左子树和右子树高度相同,现在我们删除一个结点,并不会影响以parent为根结点的子树的高度,从而变化影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。 | 0 | 只有-1/1经过
+
+
++
++/
?
?
--
??操作后会变成0,说明本次删除操作使得parent的左右子树当中较高的一棵子树的高度降低了,即改变了以parent为根结点的子树的高度,从而会影响parent的父结点的平衡因子,因此需要继续往上更新平衡因子。 | -2或2 | 此时parent结点的左右子树高度之差的绝对值已经超过1了,不满足AVL树的要求,因此需要进行旋转处理。 |
注意:parent的平衡因子在更新前只可能是-1/0/1(AVL树中每个结点的左右子树高度之差的绝对值不超过1)。
而在最坏情况下,删除结点后更新平衡因子时也会一路更新到根结点。例如下面这种情况: 在更新完平衡因子后,我们再进行实际删除结点的操作,因为实际删除结点的左右子树当中至少有一个为空树,因此我们实际删除结点时的逻辑如下:
- 若实际删除结点的左子树为空,则让其parent链接到实际删除结点的右子树,最后再删除结点即可。
- 若实际删除结点的右子树为空,则让其parent链接到实际删除结点的左子树,最后再删除结点即可。
但是要注意,因为结点是三叉链结构,因此在链接结点的过程中需要建立两个结点之间的双向关系。
在进行旋转处理时,我们的分类规则如下:
- 当parent的平衡因子为-2,parent的左孩子的平衡因子为-1时,进行右单旋。
- 当parent的平衡因子为-2,parent的左孩子的平衡因子为1时,进行左右双旋。
- 当parent的平衡因子为-2,parent的左孩子的平衡因子为0时,也进行右单旋,但此时平衡因子调整与之前有所不同,具体看代码。
- 当parent的平衡因子为2,parent的右孩子的平衡因子为-1时,进行右左双旋。
- 当parent的平衡因子为2,parent的右孩子的平衡因子为1时,进行左单旋。
- 当parent的平衡因子为2,parent的右孩子的平衡因子为0时,也进行左单旋,但此时平衡因子调整与之前有所不同,具体看代码。
与插入结点不同的是,删除结点时若是进行了旋转处理,那么在进行旋转处理后我们必须继续往上更新平衡因子,因为旋转的本质就是降低树的高度,旋转后树的高度降低了,就会影响其父结点的平衡因子,因此我们还需要继续往上更新平衡因子。
代码如下:
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
Node* delParentPos = nullptr;
Node* delPos = nullptr;
while (cur)
{
if (key < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = _root->_right;
if (_root)
_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)
_root->_parent = nullptr;
delete cur;
return true;
}
else
{
delParentPos = parent;
delPos = cur;
}
break;
}
else
{
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 (delParentPos == nullptr)
{
return false;
}
Node* del = delPos;
Node* delP = delParentPos;
while (delPos != _root)
{
if (delPos == delParentPos->_left)
{
delParentPos->_bf++;
}
else if (delPos == delParentPos->_right)
{
delParentPos->_bf--;
}
if (delParentPos->_bf == 0)
{
delPos = delParentPos;
delParentPos = delParentPos->_parent;
}
else if (delParentPos->_bf == -1 || delParentPos->_bf == 1)
{
break;
}
else if (delParentPos->_bf == -2 || delParentPos->_bf == 2)
{
if (delParentPos->_bf == -2)
{
if (delParentPos->_left->_bf == -1)
{
Node* tmp = delParentPos->_left;
RotateR(delParentPos);
delParentPos = tmp;
}
else if(delParentPos->_left->_bf == 1)
{
Node* tmp = delParentPos->_left->_right;
RotateLR(delParentPos);
delParentPos = tmp;
}
else
{
Node* tmp = delParentPos->_left;
RotateR(delParentPos);
delParentPos = tmp;
delParentPos->_bf = 1;
delParentPos->_right->_bf = -1;
}
}
else
{
if (delParentPos->_right->_bf == -1)
{
Node* tmp = delParentPos->_right->_left;
RotateRL(delParentPos);
delParentPos = tmp;
}
else if(delParentPos->_right->_bf == 1)
{
Node* tmp = delParentPos->_right;
RotateL(delParentPos);
delParentPos = tmp;
}
else
{
Node* tmp = delParentPos->_right;
RotateL(delParentPos);
delParentPos = tmp;
delParentPos->_bf = -1;
delParentPos->_left->_bf = 1;
}
}
delPos = delParentPos;
delParentPos = delParentPos->_parent;
}
else
{
assert(false);
}
}
if (del->_left == nullptr)
{
if (del == delP->_left)
{
delP->_left = del->_right;
if (del->_right)
del->_right->_parent = parent;
}
else
{
delP->_right = del->_right;
if (del->_right)
del->_right->_parent = parent;
}
}
else
{
if (del == delP->_left)
{
delP->_left = del->_left;
if (del->_left)
del->_left->_parent = parent;
}
else
{
delP->_right = del->_left;
if (del->_left)
del->_left->_parent = parent;
}
}
delete del;
return true;
}
AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个结点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即
l
o
g
N
logN
logN。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。 因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但当一个结构经常需要被修改时,AVL树就不太适合了。
|