文章目录
- AVL树概念
- 平衡因子
- 旋转
- 左单旋
- 更新父节点与孩子节点的连接
- 右单旋
- 左右双旋 (先左单旋再右单旋)
- 右左双旋 (先右单旋再左单旋)
- 验证是否为AVL树
- ALV树的删除操作
- 一. 高度不变删除叶子节点和单孩子节点
- 1.1高度不变删除叶子节点
- 1.2删除单孩子节点
- 二. 高度变化 - 旋转
- 2.1 左单旋
- 2.2 右单旋
- 小总结:
- 2.3左右双旋(先左旋再右旋)
- 2.4右左双旋(先右旋再左旋)
- 删除节点右两个孩子的情况下
- 注意
- 代码实现:
AVL树概念
前面我们讲了二叉树搜索树,它的特点为:左子树的值 < 根
右子树的值 > 根
,同时二叉搜索树里面并不支持出现重复值。由于这样的特性呢,我们对二叉搜索树进行中序遍历可以得到一个有序数组。同时查找的效率平常状态下位
L
o
g
N
LogN
LogN,但是在极端情况下会退化为单叉链,所以这就有了我们接下来要学习的内容AVL树
。
AVL树
也叫平衡二叉搜索树,就在于它对于普通搜索二叉树的区别在于,当我们左右子树的高度差,的绝对值大于 1 的时候,就会进行一个旋转操作,使二叉树恢复平衡,举个简单栗子:
如上图,此时我们左子树的高度太高了,进行旋转后,左右子树高度差相等。
平衡因子
AVL树控制平衡主要观察的是平衡因子这个概念,这里我们以右子树-左子树
作为我们平衡因子的计算方式。由于我们插入节点之后需要更新父亲节点的平衡因子,所以我们这里使用三叉链。
如图:
只有当我们的平衡因子的绝对值大于1的时候才需要进行旋转处理。同时我们的树节点定义根二叉搜索树的定义不一样,AVL树节点定义:
template<class K>
struct AVLTreeNode
{
K _data;
AVLTreeNode<K>* _left;
AVLTreeNode<K>* _right;
AVLTreeNode<K>* _parent; //新增父亲节点。
int _bf; // blance factor
AVLTreeNode(const K& key)
:_data(key)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
};
旋转
旋转分为以下四种情况:
- 左单旋
- 右单旋
- 先左单旋再右单旋 (双旋)
- 先右单旋再左单旋 (双旋)
左单旋
更新父节点与孩子节点的连接
这里来说明一下名称的含义:
pparent
:parent的父节点parent
:需要旋转的节点
原来节点连接:parent->_parent ==pparent
,child->_parent == parent
,childLeft->_parent == child
。
现在节点连接:parent->_parent == child
,child->_parent == pparent
,childLeft->_parent == parent
。
同时还要更新child
与parent
的平衡因子:直接复制为0即可。
注意特殊情况:(旋转根节点)
代码实现:
//左单旋
void RotateL(Node* parent)
{
Node* pparent = parent->_parent;
Node* child = parent->_right;
Node* childLeft = child->_left;
parent->_right = childLeft;
if (childLeft) //可能为空
childLeft->_parent = parent;
child->_left = parent;
//更新父节点的连接
if (pparent) //如果原parent的父节点不为空
{
if (pparent->_left == parent)
{
pparent->_left = child;
}
else
{
pparent->_right = child;
}
//别忘了child的父节点
child->_parent = pparent;
}
else //说明旋转的是根节点
{
child->_parent = nullptr;
_root = child;
}
parent->_parent = child;
//更新平衡因子
child->_bf = parent->_bf = 0;
}
右单旋
右单旋
和左单旋类似,这里就不在多赘述。
代码实现:
//右单旋
void RotateR(Node* parent)
{
Node* pparent = parent->_parent;
Node* child = parent->_left;
Node* childRight = child->_right;
parent->_left = childRight;
if (childRight) //可能为空
childRight->_parent = parent;
child->_right = parent;
//更新父节点的连接
if (pparent) //如果原parent的父节点不为空
{
if (pparent->_left == parent)
{
pparent->_left = child;
}
else
{
pparent->_right = child;
}
//别忘了child的父节点
child->_parent = pparent;
}
else //说明旋转的是根节点
{
child->_parent = nullptr;
_root = child;
}
parent->_parent = child;
child->_bf = parent->_bf = 0;
}
左右双旋 (先左单旋再右单旋)
这时候就需要对child
节点进行一次右单旋然后再对parent
进行左单旋,就可以了。
平衡因子的更新:双旋可不是跟我们单旋一样,child
和parent
的平衡因子都置为0就可以了。
看如下特殊情况:
如上图,我们看到当我们新增节点插入在childRight
的左右子树的时候,结果是不一样的,但是有一个规律:childRight
的左子树的值会给child
,而右子树的值会给我们的parent
,这会带来什么影响呢?
在我们左单旋和右单旋的时候,我们都是直接把parent
和child
的平衡因子赋值为0,但是这里不一样,如上图,当我们新增节点为14
的时候,我们14
这个节点最后会给child
,那么此时child
左右子树平衡了,但是parent
由于左子树缺失所以平衡因子为1
。反之也是这样。所以这里的平衡因子我们需要特殊处理。
- 只要新增节点为
childRight
的左边,parent
的平衡因子我们置为1。 - 只要新增节点为
childRight
的右边,chlid
的平衡因子我们置为-1。
两边都没有就像我们一开始画的那一种情况,全为0。
这种双旋旋转的时候我们呢可以把他们统统画为一种抽象图:
左右双旋:
新增节点在左侧: PLeft->_bf = 0
、parent->_bf = 1
新增节点在右侧:PLeft->_bf = -1
、parent->_bf = 0
代码实现:
//先左旋再右旋
void RotateLR(Node* parent)
{
Node* child = parent->_left;
Node* childRight = child->_right;
RotateL(child);
RotateR(parent);
if (childRight->_left) //左子树给child
{
parent->_bf = 1;
child->_bf = 0;
}
else if (childRight->_right)//右子树给child
{
parent->_bf = 0;
child->_bf = -1;
}
else //两边为空
{
parent->_bf = 0;
child->_bf = 0;
}
}
右左双旋 (先右单旋再左单旋)
规律和左右双旋不同,这次我们childLeft
的左子树会给我们的parent
,而右子树会给child
。
所以我们的平衡因子处理规则为:
- 只要新增节点为
childLeft
的左边,chlid
的平衡因子我们置为1。 - 只要新增节点为
childLeft
的右边,parent
的平衡因子我们置为-1。
chileLeft
左右子树都为空,parent
和child
的平衡因子都置为0。
这种双旋旋转的时候我们呢可以把他们统统画为一种抽象图:
右左双旋:
新增节点在右侧: PRight->_bf = 0
、parent->_bf = -1
新增节点在左侧:PRight->_bf = 1
、parent->_bf = 0
代码实现:
//先右旋再左旋
void RotateRL(Node* parent)
{
Node* child = parent->_right;
Node* childLeft = child->_left;
int bf = childLeft->_bf;
RotateR(child);
RotateL(parent);
if (bf == -1) //左子树给child
{
parent->_bf = 0;
child->_bf = 1;
}
else if (bf == 1)//右子树给child
{
parent->_bf = -1;
child->_bf = 0;
}
else //两边为空
{
parent->_bf = 0;
child->_bf = 0;
}
}
验证是否为AVL树
只需要检查每个节点的左右子树高度差不超过1即可。
代码实现:
int TreeHigh(Node* root)
{
if (root == nullptr)
return 0;
return max(TreeHigh(root->_left), TreeHigh(root->_right)) + 1;
}
bool _isAVLTree(Node* root)
{
if (root == nullptr)
return true;
int left = TreeHigh(root->_left);
int right = TreeHigh(root->_right);
int diff = left - right;
if (abs(diff) > 1)
return false;
return _isAVLTree(root->_left) && _isAVLTree(root->_right);
}
ALV树的删除操作
AVL树的删除根二叉搜索树的删除类似,还是那四种情况,同时还需要更新平衡因子。
一. 高度不变删除叶子节点和单孩子节点
1.1高度不变删除叶子节点
同时这里需要特殊处理一下删除根节点:
如果左右子树都为空,往哪边走都是一样的,所以这种情况可以和下面的情况一起处理。
注意我们的顺序:
- 先更新平衡因子
- 再删除节点
为什么要先更新呢?我们删除某个节点的时候,可能会让当前的子树高度发生变化,上述图并没有。当我们删除的节点为parent(父节点)
的左子树的时候,由于我们平衡因子 = Height(右子树) - Height(左子树)
,所以我们parent
的
b
f
bf
bf(平衡因子)会加一。上图虽然平衡因子加了一,但是子树的高度并没有发生变化,所以无需继续向上更新。
1.2删除单孩子节点
注意我们更新平衡因子是在删除节点上一个步骤,所以当我们更新平衡因子的时候其实是看作我们parent删除了cur这个子节点。
然后再进行删除与相应的连接,这对我们的结果并不影响。
二. 高度变化 - 旋转
旋转的部分我们都把parent
叫做我们需要旋转的节点,parent
左边的节点称为:PLeft
,parent
右边的节点称为:PRight
,要删除的节点称为:cur
。
2.1 左单旋
同时在删除操作种有一种特殊情况需要注意一下:
由于我们parent
的bf是2,这代表我们的右边高,所以每次我们都以PRight
作为新的根,删除操作PRight
可能会出现bf == 0
的情况,这种时候我们呢就需要对parent
和PRight
的平衡因子进行特殊处理了。
当我们进行左旋的时候,我们PRight
的的左孩子节点赋值给parent
,同时parent
作为PRight
的左子树,我们正常情况下,我们parent
和PRight
都是赋值为0,这种情况下,由于parent
的右子树多了一个节点,那么parent
的平衡因子变为1,又因为parent
是作为PRight
的左子树,所以PRight
的平衡因子为-1。
2.2 右单旋
同时右单旋跟左单旋类似,也会出现PLeft
的平衡因子为0的情况,这个时候PLeft
的右孩子节点给parent
的左边,同时右孩子指向parent
,所以parent
的左孩子多了一个节点,平衡因子为-1,又因为parent
作为PLeft
的右孩子,所以PLeft
的平衡因子为1。
小总结:
- 删除跟我们插入的不一样再与,我们可能会遇见平衡因子为:
2 和 0
或者-2 和 0
的组合,这个时候就需要对parent
和PRight
、PLeft
平衡因子进行特殊处理。 - 之前我们插入过后旋转玩就可以直接跳出循环,不需要再进行更新平衡因子了,但是删除不是,当我们旋转完之后还要继续往上更新平衡因子。因为我们旋转其实就是为了去平衡左右子树的高度差,例如原来平衡因子为:2,这代表我们右子树比左子树要高2,那么我们去旋转完之后可能变为 1 或者 0,所以我们还需要继续往上更新,直到
parent
的bf为1或者遍历到根节点了。
什么时候需要更新平衡因子呢?
- 更新完平衡因子为0
- 这说明原来子树的平衡因子为1或者-1,就是我们只有一个节点,当我们把这个节点删除过后,那么我们子树的高度就减小了,此时我们需要继续往上更新
- 更新完平衡因子为1 或者 -1
- 这说明我们呢原来的平衡因子为0,我们删除了一个左节点或者右节点,由于子树的高度并没有发生变化,所以此时并不需要更新
- 更新完平衡因子为2
- 树已经不平衡,需要旋转处理,同时旋转过后树的高度可能会发生变化,所以也要继续往上处理。
2.3左右双旋(先左旋再右旋)
PLright有另外的节点
,这里新增节点表示有另外的节点,并不是新插入的。同时这里的平衡因子在我们插入操作时候说过了,这里就不再赘述了。
平衡因子更新:
-
新增节点在左侧:
PLeft->_bf = 0
、parent->_bf = 1
-
新增节点在右侧:
PLeft->_bf = -1
、parent->_bf = 0
2.4右左双旋(先右旋再左旋)
PRLeft
的右子树会给PRight
的左子树,PRLeft
的左子树会给parent
的右子树。
平衡因子更新:
-
新增节点在右侧:
PRight->_bf = 0
、parent->_bf = -1
-
新增节点在左侧:
PRight->_bf = 1
、parent->_bf = 0
删除节点右两个孩子的情况下
跟我们二叉搜索树删除含有两个节点情况一样,去找一个可以替代当前位置的元素,这个元素要满足:
- 大于当前节点的左子树,小于当前节点的右子树
所以我们可以选择:左子树的最大节点
或者右子树的最小节点
。
这里我选择的是右子树的最小节点。找到之后,还是先更新一下平衡因子,不过这个时候我们就需要用RighfMinP
和RightMin
来依次更新平衡因子了。
注意
这里需要注意: 我们旋转时的更新平衡因子和普通的平衡因子更新时不一样的。
普通更新:
旋转更新:
可以看到,如果我们旋转更新还是依次更新的话,那么就出错了,当我们旋转完之后,parent
的平衡因子已经更新完毕,所以我们下一次要更新的节点平衡因子为parent->_parent
。就是更新爷爷节点
了。
代码实现:
void UpDateBFactor(Node* cur,Node* parent)
{
while (parent)
{
if (parent->_left == cur)
{
parent->_bf += 1;
}
else
{
parent->_bf -= 1;
}
if (parent->_bf == 1 || parent->_bf == -1)
break;
else if (parent->_bf == 0)
{
cur = parent;
parent = cur->_parent;
}
else if (parent->_bf == 2)
{
if (parent->_bf == 2)
{
Node* PRight = parent->_right;
if (PRight->_bf == 1)
{
RotateL(parent);
}
else if (PRight->_bf == 0)
{
RotateL(parent);
parent->_bf = 1;
PRight->_bf = -1;
break;
}
else
{
//双旋
RotateRL(parent);
}
}
cur = parent->_parent; //父节点已经修改过了,直接跳到爷爷节点
parent = cur->_parent;
}
else if (parent->_bf == -2)
{
Node* PLeft = parent->_left;
if (PLeft->_bf == -1)
{
RotateR(parent);
}
else if (PLeft->_bf == 0)
{
RotateR(parent);
parent->_bf = -1;
PLeft->_bf = 1;
break;
}
else
{
//双旋
RotateLR(parent);
}
//旋转完之后直接更新爷爷节点的平衡因子。
cur = parent->_parent;
parent = cur->_parent;
}
else
{
//平衡因子出错的时候打印错误信息。
cout << __LINE__ << " : " << " 平衡因子错误" << endl;
exit(-1);
}
}
}
bool erase(const K& key)
{
if (_root == nullptr)
return false;
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (key < cur->_data)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_data)
{
parent = cur;
cur = cur->_right;
}
else
{
if (cur->_left == nullptr)
{
//特殊处理删除根节点的情况
if (parent == nullptr)
{
_root = _root->_right;
return true;
}
//先更新平衡因子
UpDateBFactor(cur, parent);
//再删除节点
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
delete cur;
return true;
}
else if (cur->_right == nullptr)
{
//特殊处理删除根节点的情况
if (parent == nullptr)
{
_root = _root->_left;
return true;
}
UpDateBFactor(cur, parent);
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
delete cur;
return true;
}
else
{
//以右孩子的最小节点作为替换节点
Node* RightMinP = cur;
Node* RightMin = cur->_right;
//找到右子树的最小节点
while (RightMin && RightMin->_left)
{
RightMinP = RightMin;
RightMin = RightMin->_left;
}
//把替换节点的值拷贝给del(删除)节点.
cur->_data = RightMin->_data;
UpDateBFactor(RightMin, RightMinP);
if (RightMin->_right)
{
RightMin->_right->_parent = RightMinP;
}
//删除此时的替换节点。
if (RightMinP->_right == RightMin) // 说明没找到最小节点
{
RightMinP->_right = RightMin->_right;
}
else //找到最小节点
{
RightMinP->_left = RightMin->_right;
}
delete RightMin;
return true;
}
}
}
return false;
}