文章目录
1、AVL树
1.1、AVL树的概念
二叉搜索树(binary search tree)虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树(最坏的情况如下图左单支所示),查找元素相当于在顺序表中搜索元素,效率低下
。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度
。 一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(高度差:-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)。
1.2 AVL树节点的定义
AVL树的节点采用三叉链结构,其中包含指向左右子节点的指针和指向父亲的指针。数据存储在键值对中,使用pair对象表示。为了保持树的平衡,引入了平衡因子来判断是否需要进行平衡操作。
// AVL树节点的定义(KV模型)
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv; // 键值对
int _bf; // 平衡因子(balance factor) = 右子树高度 - 左子树高度
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent; // 双亲指针
// 构造函数
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
,_bf(0)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
{}
};
// AVL树的定义(KV模型)
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
private:
Node* _root;
public:
// 成员函数
}
1.3 AVL树 - 插入节点
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为3步:
- 插入新节点
- 更新树的平衡因子
- 根据更新后树的平衡因子的情况,来控制树的平衡(旋转操作)
1.3.1 插入新节点
和二叉搜索树插入方式一样,先查找,再插入。
// 插入节点
bool AVLTree::Insert(const pair<K, V>& kv)
{
// 如果树为空,则直接插入节点
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
// 如果树不为空,找到适合插入节点的空位置
Node* parent = nullptr; // 记录当前节点的父亲
Node* cur = _root; // 记录当前节点
while (cur)
{
if(kv.first > cur->_kv.first) // 插入节点键值k大于当前节点
{
parent = cur;
cur = cur->_right;
}
else if(kv.first < cur->_kv.first) // 插入节点键值k小于当前节点
{
parent = cur;
cur = cur->_left;
}
else // 插入节点键值k等于当前节点
{
return false;
}
}
// while循环结束,说明找到适合插入节点的空位置了
// 插入新节点
cur = new Node(kv); // 申请新节点
// 判断当前节点是父亲的左孩子还是右孩子
if (cur->_kv.first > parent->_kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
//...................................
// 这些写更新平衡因子,和控制树的平衡的代码
//...................................
// 插入成功
return true;
}
1.3.2 更新树的平衡因子
插入新节点后,该节点到根节点之间的所有祖先节点的平衡因子可能会受到影响。根据不同的情况,需要更新它们的平衡因子。
- 如果插入在「新节点父亲」的右边,父亲的平衡因子++( _bf++ )
- 如果插入在「新节点父亲」的左边,父亲的平衡因子–( _bf-- )
「新节点父亲」的平衡因子更新以后,又会分为 3 种情况:
- 如果更新以后,平衡因子是 1 或者 -1(则之前一定为 0),说明父亲所在子树高度变了,需要继续往上更新。(最坏情况:往上一直更新到根节点)
2、如果更新以后,平衡因子是 0(则之前一定为 1 或者 -1),说明父亲所在子树高度没变(因为把矮的那边给填补上了),不需要继续往上更新。
3、如果更新以后,平衡因子是 2 或者 -2,说明父亲所在子树出现了不平衡,需要旋转处理,让它平衡。
代码如下:
while (parent) // 最坏情况:更新到根节点
{
// 更新新节点父亲的平衡因子
if (cur == parent->_left) // 新节点插入在父亲的左边
{
parent->_bf--;
}
else // 新节点插入在父亲的右边
{
parent->_bf++;
}
// 检查新节点父亲的平衡因子
// 1、父亲所在子树高度变了,需要继续往上更新
if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = cur->_parent;
}
// 2、父亲所在子树高度没变,不用继续往上更新
else if (parent->_bf == 0)
{
break;
}
// 3、父亲所在子树出现了不平衡,需要旋转处理
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 这里写对树进行平衡化操作,旋转处理的代码,分为4种情况:
/*................................................*/
// 3.1、父节点的左边高,右边低,需要往右旋
if (parent->_bf == -2 && cur->_bf == -1)
{
// 右单旋
treeRotateRight(parent);
}
// 3.2、父节点的右边高,左边低,需要往左旋
else if (parent->_bf == 2 && cur->_bf == 1)
{
// 左单旋
treeRotateLeft(parent);
}
// 3.3、父节点的左边高,且父节点左孩子的右边高
else if(parent->_bf == -2 && cur->_bf == 1)
{
// 左右双旋
treeRotateLR(parent);
}
// 3.4、父节点的右边高,且父节点右孩子的左边高
else if(parent->_bf == 2 && cur->_bf == -1)
{
// 右左双旋
treeRotateRL(parent);
}
else // 只有上述4种情况,没有其它情况,所以这里直接报错处理
{
assert(false);
}
break; // 旋转完成,树已平衡,退出循环
/*................................................*/
}
// 4、除了上述3种情况,平衡因子不可能有其它的值,报错处理
else
{
assert(false);
}
}
1.3.3 根据更新后BF的情况,进行平衡化操作
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为4种:
旋转的本质:在遵循二叉搜索树的规则下,让左右均衡,降低整棵树的高度。
该进行哪种旋转操作?– 引发旋转的路径是直线就是单旋,如果是折线就是双旋。
👇注意:此处看到的树,可能是一颗完整的树,也可能是一颗子树。
① 右单旋 - 新节点插入较高左子树的最左侧
将新的节点插入到了 parent 左孩子的左子树上,导致的不平衡的情况。
上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左子树高度增加了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能让其成为30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能让其成为60的左子树,旋转完成后,更新节点的平衡因子即可。
引发右单旋的条件:
- 父亲左边高,右边低,所以要让父亲往右旋。
- parent 的平衡因子为 -2,parent 左孩子平衡因子为 -1,观察发现,平衡因子都是负数,说明是左边高,也说明了==【引发旋转的路径是一条直线】==,所以我们要右旋操作。
右单旋操作: - 让 subL 的右子树 subLR 成为 parent 的左子树(因为 subLR 的右子树根节点值大于30,小于60)
- 让 parent 成为 subL 的右子树(因为60大于30)
- 让 subL 变成这个子树的根
这一步操作前需要先判断下:parent 是根节点,还是一个普通子树
如果是根节点,则更新 subL 为新的根
如果是普通子树(可能是某个节点的左子树,也可能是右子树,这里需要判断下),然后更新 subL 为这个子树的根节点 - 根据树的结构,更新 parent 和 subL 的平衡因子为0
在旋转过程中,更新双亲指针的指向,有以下几种情况需要考虑: - subL 的右子树 subLR 可能存在,也可能为空。(当不为空时才更新 subL 右子树 subLR 的双亲指针指向)
- 旋转完成后,subL 的双亲节点,可能是空,也可能是 parent 原先的父节点。(所以更新 subL 的双亲指针前需要判断下)
代码如下:
总的来说,就是依次调整 subLR、parent、subL 的位置和双亲指针的指向。
// 右单旋
void treeRotateRight(Node* parent)
{
// subL:parent的左孩子
// subLR:parent左孩子的右孩子
Node* subL = parent->_left;
Node* subLR = parent->_left->_right;
// 1、让subL的右子树subLR成为parent的左子树
parent->_left = subLR;
// 1.1、如果subLR不为空
if (subLR)
{
subLR->_parent = parent; // 更新subLR的双亲指针,指向parent
}
// 2、让parent成为subL的右子树
subL->_right = parent;
// 2.1、记录下parent的父节点
Node* ppNode = parent->_parent;
// 2.2、更新parent的双亲指针,指向subL
parent->_parent = subL;
// 2.3、判断parent是不是根节点
// 是根节点
if (parent == _root)
{
_root = subL; // 更新subL为新的根
subL->_parent = nullptr; // 更新subL的双亲指针,指向空
}
// 不是根节点,就是一个普通子树
else
{
// 判断parent原先是左孩子还是右孩子
if (ppNode->_left == parent)
{
ppNode->_left = subL; // parent原先的双亲节点接管subL,subL为这个子树的根
}
else
{
ppNode->_right = subL;
}
subL->_parent = ppNode; // 更新subL的双亲指针
}
// 根据调整后的结构更新parent和subL的平衡因子
parent->_bf = subL->_bf = 0;
}
② 左单旋 - 新节点插入较高右子树的最右侧
将新的节点插入到了 parent 右孩子的右子树上,导致的不平衡的情况。
引发左单旋的条件:
父亲右边高,左边低,所以要让父亲往左旋。
parent 的平衡因子为 2,parent 右孩子平衡因子为 1,观察发现,平衡因子都是正数,说明是右边高,也说明了==【引发旋转的路径是一条直线】==,所以我们要左旋操作。
左单旋操作:
- 让 subR 的左子树 subRL 成为 parent 的右子树(因为 subRL 的左子树根节点值大于30,小于60)
- 让 parent 成为 subR 的左子树(因为30小于60)
- 让 subR 变成这个子树的根
这一步操作前需要先判断下:parent 是根节点,还是一个普通子树
如果是根节点,则更新 subR 为新的根
如果是普通子树(可能是某个节点的左子树,也可能是右子树,这里需要判断下),然后更新 subR 为这个子树的根节点
根据树的结构,更新 parent 和 subR 的平衡因子为0
在旋转过程中,更新双亲指针的指向,有以下几种情况需要考虑:
subR 的左子树 subRL 可能存在,也可能为空。(当不为空时才更新 subR 左子树 subRL 的双亲指针指向)
旋转完成后,subR 的双亲节点,可能是空,也可能是 parent 原先的父节点。(所以更新 subR 的双亲指针前需要判断下)
代码如下:
总的来说,就是依次调整 subRL、parent、subR 的位置和双亲指针的指向。
// 左单旋
void treeRotateLeft(Node* parent)
{
// subR:父亲的右孩子
// subRL:父亲的右孩子的左孩子(大于父亲,小于subR)
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 1、让subRL成为父亲的右子树
parent->_right = subRL;
// 如果subRL不为空
if (subRL)
{
subRL->_parent = parent; // 更新subRL双亲指针,指向parent
}
// 2、让parent成为subR的左子树
subR->_left = parent;
// 2.1、先记录下parent的双亲节点
Node* ppNode = parent->_parent;
// 2.2、更新parent双亲指针的指向
parent->_parent = subR;
// 2.3、判断parent是不是根节点
// 是根节点
if (parent == _root)
{
_root = subR; // subR为新的根
subR->_parent = nullptr; // subR双亲指针指向空
}
// 不是根节点,就是一个普通子树
else
{
// 判断parent原先是左孩子还是右孩子
if (ppNode->_left == parent)
{
ppNode->_left = subR; // parent原先的双亲节点接管subR,subR为这个子树的根
}
else
{
ppNode->_right = subR;
}
subR->_parent = ppNode; // 更新subR的双亲指针
}
// 根据树的结构,更新parent和subR的平衡因子
parent->_bf = subR->_bf = 0;
}
③ 左右双旋 - 新节点插入较高左子树的右侧
将新的节点插入到了 parent 左孩子的右子树上,导致的不平衡的情况。
这时我们需要的是先对 parent 的右孩子进行一次左旋,再对 parent 进行一次右旋。
这里可以观察到一个现象: 节点60的左右子树被分走了,左子树最终成了30的右子树,右子树最终成了90的左子树。
将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再
考虑平衡因子的更新。
// 旋转之前,60的平衡因子可能是-1/0/1,旋转完成之后,根据情况对其他节点的平衡因子进
行调整
void _RotateLR(PNode pParent)
{
PNode pSubL = pParent->_pLeft;
PNode pSubLR = pSubL->_pRight;
// 旋转之前,保存pSubLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节
点的平衡因子
int bf = pSubLR->_bf;
// 先对30进行左单旋
_RotateL(pParent->_pLeft);
// 再对90进行右单旋
_RotateR(pParent);
if(1 == bf)
pSubL->_bf = -1;
else if(-1 == bf)
pParent->_bf = 1;
}
*** 新节点插入较高右子树的左侧—右左:先右单旋再左单旋***
参考右左双旋。
总结:
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
- pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
当pSubR的平衡因子为1时,执行左单旋
当pSubR的平衡因子为-1时,执行右左双旋 - pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
当pSubL的平衡因子为-1是,执行右单旋
当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
2 AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
- 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树 - 验证其为平衡树
每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
节点的平衡因子是否计算正确
int _Height(PNode pRoot);
bool _IsBalanceTree(PNode pRoot)
{
// 空树也是AVL树
if (nullptr == pRoot) return true;
// 计算pRoot节点的平衡因子:即pRoot左右子树的高度差
int leftHeight = _Height(pRoot->_pLeft);
int rightHeight = _Height(pRoot->_pRight);
int diff = rightHeight - leftHeight;
// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
// pRoot平衡因子的绝对值超过1,则一定不是AVL树
if (diff != pRoot->_bf || (diff > 1 || diff < -1))
return false;
// pRoot的左和右如果都是AVL树,则该树一定是AVL树
return _IsBalanceTree(pRoot->_pLeft) && _IsBalanceTree(pRoot-
>_pRight);
}
AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
验证其是否为二叉搜索树
如果中序遍历可以得到一个有序的序列,就说明为二叉搜索树。
验证其是否为平衡树
每个节点子树高度差的绝对值不超过1
节点的平衡因子是否计算正确
(1)首先写一个计算当前树高度的函数
// 计算当前树的高度
int Height(Node* root)
{
// 当前树为空,则高度为0
if (root == nullptr)
return 0;
// 当前树不为空,计算左右子树的高度
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
// 当前树的高度 = 左右子树中高度最大的那个加1
return max(leftHeight, rightHeight) + 1;
}
(2)检查AVL树是否平衡,思路一:自顶向下的暴力解法
// 检查AVL树是否平衡,思路一
bool IsBalance1()
{
return _IsBalance1(_root);
}
bool _IsBalance1(Node* root)
{
// 当前树为空,说明是平衡的
if (root == nullptr)
return true;
// 当前树不为空,计算左右子树的高度
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
if (rightHeight - leftHeight != root->_bf) // 检查当前树的平衡因子是否计算正确
{
cout << "平衡因子异常:" << root->_kv.first << endl;
}
// 左右子树高度相减的绝对值小于2,说明当前树是平衡的,则继续往下判断其它子树
return abs(leftHeight - rightHeight) < 2
&& _IsBalance1(root->_left)
&& _IsBalance1(root->_right);
}
(3)检查AVL树是否平衡,思路二:自底向上的高效解法(动态规划,前一个子问题的解,能够用于后一个问题求解)
// 检查AVL树是否平衡,思路二
bool IsBalance2()
{
return _IsBalance2(_root) != -1;
}
int _IsBalance2(Node* root)
{
// 先判断当前树的左、右子树是否平衡,再判断当前树是否平衡
// 不平衡返回-1,平衡则返回当前树的高度
// 当前树为空,返回高度0
if (root == nullptr)
return 0;
// 当前树不为空,分别计算左右子树的高度
int leftHeight = _IsBalance2(root->_left);
int rightHeight = _IsBalance2(root->_right);
if (rightHeight - leftHeight != root->_bf) // 检查当前树的平衡因子是否计算正确
{
cout << "平衡因子异常:" << root->_kv.first << endl;
}
// 左子树高度等于-1、右子树高度等于-1、左右子树高度差的绝对值大于1,说明当前树不平衡
if (leftHeight == -1 || rightHeight == -1 || abs(leftHeight - rightHeight) > 1)
return -1;
// 运行到这里来了,说明当前树是平衡的,返回当前树的高度
return max(leftHeight, rightHeight) + 1;
}
2.1 AVL树 - 删除节点(了解)
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,如果出现不平衡树,进行旋转。只不过与二叉搜索树不同的是,AVL树删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
2.2 AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,接近于完全二叉树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 O(log2N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。