2. AVL树
目录
1.AVL树的概念
2.AVL树的实现
2.1 AVL树节点的定义
2.2 AVL树的插入结构
2.3 AVL树的旋转
2.3.1 左单旋
2.3.2 右单旋
2.3.3 右左双旋
2.3.4 左右双旋
2.4 总体的插入逻辑
1.AVL树的概念
二叉搜索树虽可以增加查找的效率,但如果数据有序或接近有序,二叉搜索树将会退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因为两位俄罗斯的数学家发明了一种解决上述问题的方法:当向二叉搜索树中插入新节点后,如果能保证每个节点的左右子树高度之差的绝对值不超过1,即可降低树的高度,从而减少平均搜索长度。
一颗AVL树或者是空树,是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树"高度平衡二叉树"
- 左右子树高度之差(平衡因子)的绝对值不超过1(-1、0、1)
|右子树高度-左子树高度|≤1,平衡为什么不是高度相等呢?有时候是真的做不到。
2.AVL树的实现
2.1 AVL树节点的定义
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv;
AVLTreeNode<K, V> *left;
AVLTreeNode<K, V> *right;
AVLTreeNode<K, V> *parent;
int _bf;
explicit AVLTreeNode(const pair<K, V>& kv)
: _kv(kv), left(nullptr), right(nullptr), parent(nullptr), _bf(0)
{}
};
2.2 AVL树的插入结构
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
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;
else
parent->right = cur;
cur->_parent = parent;
//控制平衡,更新平衡因子_bf
}
private:
Node* _root = nullptr;
};
上面是插入了结点,下面就该考虑如何平衡了:需要分情况讨论:
插入结点就会改变 cur 的 _parent 的 _bf:
新增在左:parent 的因子 --
20(-1)
/
10(0)
新增在右:parent 的因子 ++
20(1)
\
30(0)
更新后 parent 的因子 == 0,parent的子树高度不变,不会影响祖先,不再向上更新:
20(0)
/ \
10 30
更新后 parent 因子 == 1 or -1,说明parent的平衡因子原来是0,现在高度变了,需要沿parent逐级向上更新,因为当前树变的不平衡,一定会影响更高层的_bf,也就是根据parent的变化,更新parent->_parent的 _bf
20 (0)
/ \
10 30(1) parent
\
40 cur
向上更新时,相当于cur新增在下面这个parent的右边,所以 parent 的 _bf --,由0变-1
20 parent (0)->(-1)
/ \
10 30(1) cur
\
40
更新后 parent 因子 == 2 or -2,说明 parent 原来就不平衡,此时更不平衡了,需要对parent所在子树进行旋转
20 (2)
/ \
10 30 (2) parent
\
40 (1) cur
\
50
更新到 cur 为根结点,也就是 parent 为 nullptr 时结束,说明更新到了最祖宗的结点
//控制平衡,更新平衡因子_bf
while(parent)
{
if(cur == parent->left)
parent->_bf--;
else
parent->_bf++;
//检查更新后的_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)
{
//旋转
}
else//万一不小心更新出了3,4,5...说明2的时候就出错了
assert(false);
}
2.3 AVL树的旋转
插入函数的结构我们很清楚了,下面来分析一下旋转的具体过程:
旋转的时候要注意两个问题:
- 保持树是搜索树
- 变成平衡树的同时降低树的高度
2.3.1 左单旋
新结点插入较高右子树的右侧-右右:左单旋
20
/ \
10 30 (2) parent
\
40 (1) cur
\
50
核心操作:
parent->right = cur->left;
cur->left = parent;
30 (2) parent
\
40 (1) cur
\
50
parent->right = cur->left;
(parent)30 40 cur
\ / \
null 50
cur->left = parent;
40 cur
/ \
(parent)30 50
\
null
但是这时是有问题的,每个结点有三个方向,left, right, parent。
我们断开的是parent→right; cur→left, 但是只有这两步是不行的,他们之间复杂的指向关系还需要更新。
我们可以画个图:
所以我们的两个核心操作改变了三个结点之间的指向,cur、parent、cur→left
void rotateL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
parent->_right = curleft;
if(curleft)
curleft->_parent = parent;
cur->_left = parent;
Node* ppnode = parent->_parent;
parent->_parent = cur;
if(parent == _root)//如果旋转放下去的是根结点,那么cur就是新根,它的父结点就是nullptr
{
_root = cur;
cur->_parent = nullptr;
}
else//如果parent之上还有结点,那么需要在前面修改parent的_parent之前保存ppnode,现在cur->_parent=ppnode
{
cur->_parent = ppnode;
//然后我们还不知道ppnode的left还是right链接的parent,所以也不知道是left还是right链接cur
if(ppnode->_left == parent)
ppnode->_left = cur;
else
ppnode->_right = cur;
}
parent->_bf = cur->_bf = 0;
}
- parent为根结点,放到cur的left了,cur成为新的根节点,cur→_parent = nullptr
- parent之上还有父节点ppnode,ppnode需要连接子树新的根节点cur,就需要判断ppnode的左还是右去连接cur。
- 最后更新parent 和 cur 的_bf
2.3.2 右单旋
新结点添加在较高左子树的左侧-左左:右单旋
和左单旋类似,基本就是翻转过来,核心操作为:
parent->_left = cur->_right;
cur->_right = parent;
void rotateR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
parent->_left = curright;
if(curright)
curright->_parent = parent;
cur->_right = parent;
Node* ppnode = parent->_parent;
parent->_parent = cur;
if(_root == parent)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
cur->_parent = ppnode;
if(ppnode->_left == parent)
ppnode->_left = cur;
else
ppnode->_right = cur;
}
parent->_bf = cur->_bf = 0;
}
2.3.3 右左双旋
新增结点在较高右子树的左边:右左
20 (1)
/ \
10 30
/ \
25 40
20 (2)
/ \
10 30 (-1)
/ \
25 40
/
24
若像原来,右子树高,parent 的 _bf = 2,走左单旋:
20 (2) parent
/ \
10 30 (-1) cur
/ \
25 40
/
24
20 30 cur
/ \ / \
10 25 40
/
24
30
/ \
20 40
/ \
10 25
/
24
还是不平衡,所以要先对cur子树进行右单旋,再对parent进行左单旋,即为双旋:
我们看黄线就可以明显的观察出双旋的本质:
第一个旋转是将 折线 变 直线,第二个旋转是将 直线 变 平衡。
void RotateRL(Node* parent)
{
RotateR(parent->right);
RotateL(parent);
}
但是在双旋中,因为我们调用的单旋最后都是平衡的,所以cur,curleft,parent 的 _bf 最后都修改为了0,在上图中我们可以看出这是不对的。
我们需要分情况讨论_bf的情况。
上例中我们在 60 的左边插入了新结点 40 导致的双旋,所以 60 的_bf 为-1,这种情况下,双旋完成后
cur->_bf = 1;
curleft->_bf = 0;
parent->_bf = 0;
如果在 60 的右插入新结点 50 导致双旋,那么 60 的_bf 为 1,这种情况下:
cur->_bf = 0;
curleft->_bf = 0;
parent->_bf = -1;
最后就是 60自己 引发的双旋,60 的_bf 是0,这种情况比较简单,旋转之后三个结点的平衡因子都是0
30 30
\ \
90 → 60 → 60
/ \ / \
60 90 30 90
void rotateRL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
int bf = curleft->_bf;
rotateR(parent->_right);
rotateL(parent);
if(bf == 1)
{
curleft->_bf = 0;
parent->_bf = -1;
cur->_bf = 0;
}
else if(bf == -1)
{
curleft->_bf = 0;
parent->_bf = 0;
cur->_bf = 1;
}
else
{
curleft->_bf = parent->_bf = cur->_bf = 0;
}
}
2.3.4 左右双旋
新增结点在较高左子树的右边,与有左旋是大致相同的,基本镜像
void rotateLR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
int bf = curright->_bf;
rotateL(parent->_left);
rotateR(parent);
if(bf == 1)
{
curright->_bf = 0;
parent->_bf = 0;
cur->_bf = -1;
}
else if(bf == -1)
{
curright->_bf = 0;
parent->_bf = 1;
cur->_bf = 0;
}
else
{
curright->_bf = parent->_bf = cur->_bf = 0;
}
}
2.4 总体的插入逻辑
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;
else
parent->right = cur;
cur->_parent = parent;
//控制平衡,更新平衡因子_bf
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)
{
if(cur->_bf == 1)
rotateL(parent);
else
rotateRL(parent);
}
else
{
if(cur->_bf == -1)
rotateR(parent);
else
rotateLR(parent);
}
break;
}
else
assert(false);
}
}