AVL树
本章思维导图:
注:本章思维导图对应的
.xmind
和.png
文件都已同步导入至资源
文章目录
1. AVL树的来历
我们之前学习过一种搜索结构——搜索二叉树,它可以以O(logN)
的时间复杂度找到一个值,但是如果遇到某些极端情况(单边二叉树),查找的效率就会将为O(N)
:
为了解决这一问题,我们就要尽可能的避免类似单边二叉树的出现,也即要尽可能保证每个节点的左右子树的高度要尽可能相等,也就是说要将搜索二叉树优化为平衡搜索二叉树
在这种背景下,AVL树出现了:
- AVL树是最早被发明的自平衡搜索二叉树
- AVL树的发明者为G. M. Adelson-Velsky和E. M. Landis,因此被叫做AVL树
2. AVL树的性质与特点
上图就是一棵AVL树,AVL树具有如下的特点:
-
AVL树是一棵搜索二叉树,因此右节点的键值大于根节点的;左节点的键值小于根节点的。并且每个节点的键值都不同
-
每个节点的左右子树都是一棵AVL树
-
每个节点的左右子树的高度差的绝对值不超过1
-
为了确保
第3点
,一般的对于每个节点都有一个平衡因子``bf(balance factor)`来记录该节点左右子树的高度差,如果bf的绝对值大于2,就要对该树进行调整- 在本篇博客中,规定:
bf = 右子树高度 - 左子树高度
- 在本篇博客中,规定:
可能有小伙伴会疑惑,为什么平衡的条件是左右子树的高度差的绝对值不超过1,为什么不能是0,做到完全平衡呢?
我们可以考虑一些特殊情况,例如两个节点和四个节点的二叉树,无论如何他们都无法做到任意节点的左右子树的高度差为0:
因此AVL树只能保证尽可能的平衡,而不能做到绝对的平衡
2. 了解操作
注:本篇博客的AVL树统一采用KV(key_value)模型
2.1 AVL树节点类
AVL树节点类定义如下:
template<class K, class V>
struct AVLTreeNode
{
typedef AVLTreeNode<K, V> Node;
Node* _left = nullptr;
Node* _right = nullptr;
Node* _parent = nullptr;
pair<K, V> _kv;
int _bf = 0; //bf为平衡因子,即[右子树高度 - 左子树高度]
AVLTreeNode(const pair<K, V>& kv)
: _kv(kv)
{}
};
这是一个三叉链链式结构:
_left
:指向节点的左子树_right
:指向节点的右子树_parent
:指向节点的父节点_kv
:存储key
和value
_bf
:平衡因子,为右子树高度与左子树高度的差
2.2 insert 插入节点
2.2.1 找到插入位置
作为一棵搜索二叉树,AVL树的插入首先同样要找到一个插入位置:
-
如果新节点的键值大于当前节点,则去该节点的右子树寻找
-
如果新节点的键值小于当前节点,则去该节点的左子树寻找
-
如果新节点的键值等于当前节点,插入失败
-
如果遍历到空,则该位置就是插入位置
bool insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
//找到插入位置
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
parent = cur;
if (cur->_kv.first > kv.first)
cur = cur->_left;
else if (cur->_kv.first < kv.first)
cur = cur->_right;
else
return false;
}
//连接新节点
cur = new Node(kv);
if (parent->_kv.first > kv.first)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
//……………………
}
2.2.2 更新平衡因子
一个新节点插入成功后,首先更新的应该是它的父节点:
- 如果新节点为父节点的右,bf++
- 如果新节点为父节点的左,bf– –
之后,对于更新后父节点bf的值,我们要分以下三种情况:
情况一:父节点bf == 0
首先我们应该清楚,AVL树规定左右子树的高度差不会超过1,因此在一个节点的bf只能能为0,1,-1
如果插入一个节点之后,父节点的bf为0,这就说明:
- 父节点之前的bf一定为1或者-1
- 新节点一定插入在较矮的子树
如果不明白,我们可以方向思考:
- 如果父节点bf之前为0,那么插入节点后必然会使左右子树的一棵变高,从而使插入后的bf变为1或-1
- 如果新节点插入在较高的子树,那么bf的绝对值(左右子树的高度差)并不会变小,而是会变大
因此,这种情况可以用下图总结:
可以看出,由于新节点插入在父节点较矮的子树,因此插入后并没有改变以父节点为根的树的高度,从而也就没有父节点的上级节点左右子树的高度差,例如:
因此,对于插入节点后
父节点 bf == 0
的情况,由于没有破坏整棵树的平衡,插入过程直接结束
情况二:父节点bf == ±1
经过和上面类似的分析,要使插入后父节点的
bf == ±1
,插入前,父节点的bf
必须等于0:
可以看出,如果插入后父节点的
bf == ±1
,那么以父节点为根节点的树的高度必然发生变化,从而会影响到父节点的上级节点:
因此,针对插入后父节点
bf == ±1
这种情况,我们需要通过循环来不断调节上级节点,直至平衡
情况三:父节点bf == ±2
同样的分析,如果插入后父节点
bf == ±2
,那么插入之前,父节点的bf一定满足:bf == ±1
,并且新节点插入在较高的子树:
针对这种情况,我们需要通过旋转来调节树的平衡
分为以下四种情况:
首先规定,链接新节点的节点称为cur,cur的父节点称为parent
2.2.2.1 左单旋
当插入节点后满足 cur->bf == 1
并且 parent->bf == 2
时,需要使用左单旋来调节以parent为根的树的平衡:
旋转步骤为:
cur
的左子树链接给parent的右子树parent
链接给cur的左子树
编写代码时需要注意以下几个细节问题:
- 这是一个三叉链结构,除了左右子树的链接,还需要注意父节点的链接
- 在将cur的左子树链接给parent时,需要注意cur的左子树为空的情况,否则可能会导致空节点的访问。例如:
- parent可能就是整棵树的根节点,因此在旋转过后需要注意
_root
根节点的更新- 旋转过后需要注意更新平衡因子
bf
:parent->bf = 0
,cur->bf = 0
- 由于旋转过后新的根(即cur)的bf等于0,因此可以直接结束插入过程
代码实现:
void rotateL(Node* parent)
{
Node* pparent = parent->_parent;
Node* parentR = parent->_right;
Node* parentRL = parentR->_left;
parent->_right = parentRL;
//考虑cur->_left为空的情况,防止空节点的访问
if (parentRL)
parentRL->_parent = parent;
parentR->_left = parent;
parentR->_parent = pparent;
parent->_parent = parentR;
//如果cur就是_root根节点,那么就要更新根节点
if (pparent == nullptr)
_root = parentR;
else //否则就要将新的根(即cur)链接给上级节点
{
if (parent == pparent->_left)
pparent->_left = parentR;
else
pparent->_right = parentR;
}
//更新平衡因子
parent->_bf = 0;
parentR->_bf = 0;
}
2.2.2.2 右单旋
当插入节点后满足 cur->bf == -1
并且 parent->bf == -2
时,需要使用右单旋来调节以parent为根的树的平衡:
旋转步骤为:
cur
的右子树链接给parent的左子树parent
链接给cur的右子树
编写代码时需要注意以下几个细节问题:
和左单旋类似:
- 这是一个三叉链结构,除了左右子树的链接,还需要注意父节点的链接
- 在将cur的右子树链接给parent时,需要注意cur的右子树为空的情况,否则可能会导致空节点的访问。例如:
- parent可能就是整棵树的根节点,因此在旋转过后需要注意
_root
根节点的更新- 旋转过后需要注意更新平衡因子
bf
:parent->bf = 0
,cur->bf = 0
- 由于旋转过后新的根(即cur)的bf等于0,因此可以直接结束插入过程
代码实现:
void rotateR(Node* parent)
{
Node* pparent = parent->_parent;
Node* parentL = parent->_left;
Node* parentLR = parentL->_right;
parent->_left = parentLR;
//考虑cur->_right为空的情况
if (parentLR)
parentLR->_parent = parent;
parentL->_right = parent;
parent->_parent = parentL;
parentL->_parent = pparent;
//如果cur就是_root根节点,那么就要更新根节点
if (pparent == nullptr)
_root = parentL;
else //否则就要将新的根(即cur)链接给上级节点
{
if (parent == pparent->_left)
pparent->_left = parentL;
else
pparent->_right = parentL;
}
//更新平衡因子
parent->_bf = 0;
parentL->_bf = 0;
}
2.2.2.3 右左双旋
当插入节点后,如果出现 cur->bf == -1
并且 parent->bf == 2
,如图:
此时,如果我们仍然像情况cur->bf == 1 && parent->bf == 2
一样,将parent进行左单旋:
可以发现,新的父节点cur
的平衡因子仍为-2
,仍没有达到平衡,可见,仅仅靠单旋并不能解决问题。
针对这种情况,要将树调整平衡,需要用到双旋操作:
为了便于分析,我们先将高度为
h + 1
的子树分为两部分,我们称这两部分的父节点为parentRL
,如图:
这里还需要考虑一个特殊情况:
h == 0
,即这棵树只有两个节点parent和cur
:
双旋的步骤为:
- 先对cur为根的树进行右单旋
- 再对parent为根的树进行左单旋
旋转过后需要对树的节点的平衡因子进行调整,可以根据新节点插入在
parentRL
的左子树还是右子树进行分析:
新节点插入在
parentRL
的左子树,即parentRL->bf == -1
:
- 则旋转过后,
parnet->bf = 0
、parentRL->bf = 0
、cur->bf = 1
新节点插入在
parentRL
的右子树,即parentRL->bf == 1
:
- 则旋转过后,
parent->bf = -1
、parentRL = 0
、cur->bf = 0
最后要注意上面提到的特殊情况:
parentRL->bf == 0
:
- 则旋转过后,
parent->bf = 0
、parentRL = 0
、cur->bf = 0
实现代码:
void rotateRL(Node* parent)
{
Node* parentR = parent->_right; //即为图中提到的cur节点
Node* parentRL = parentR->_left; //即为图中提到的parentRL节点
int bf = parentRL->_bf;
rotateR(parentR); //先对cur为根的树进行右单旋
rotateL(parent); //再对parent为根的树进行左单旋
//通过对旋转前parentRL平衡因子的大小区分插入情况
if (bf == 0)
{
parent->_bf = 0;
parentR->_bf = 0;
parentRL->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = -1;
parentR->_bf = 0;
parentRL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
parentR->_bf = 1;
parentRL->_bf = 0;
}
else //如果以上三张情况都不符合,说明发生错误
assert(false);
}
2.2.2.4 左右双旋
当插入节点后,如果出现 cur->bf == 1
并且 parent->bf == -2
,如图:
此时,如果我们仍然像情况cur->bf == 1 && parent->bf == 2
一样,将parent进行右单旋:
可以发现,新的父节点cur
的平衡因子仍为2
,仍没有达到平衡,可见,仅仅靠单旋并不能解决问题。
针对这种情况,要将树调整平衡,需要用到双旋操作:
为了便于分析,我们先将高度为
h + 1
的子树分为两部分,我们称这两部分的父节点为parentLR
,如图:
同样,这里还需要考虑一个特殊情况:
h == 0
,即这棵树只有两个节点parent和cur
:
双旋的步骤为:
- 先对cur为根的树进行左单旋
- 再对parent为根的树进行右单旋
旋转过后需要对树的节点的平衡因子进行调整,用相同的方法,根据新节点插入在
parentLR
的左子树还是右子树进行分析:
新节点插入在
parentLR
的左子树,即parentLR->bf == -1
:
- 则旋转过后,
parnet->bf = 1
、parentLR->bf = 0
、cur->bf = 0
新节点插入在
parentLR
的右子树,即parentLR->bf == 1
:
- 则旋转过后,
parent->bf = 0
、parentRL = 0
、cur->bf = -1
最后要注意上面提到的特殊情况:
parentRL->bf == 0
:
- 则旋转过后,
parent->bf = 0
、parentRL = 0
、cur->bf = 0
实现代码;
void rotateLR(Node* parent)
{
Node* parentL = parent->_left; //即图中的cur节点
Node* parentLR = parentL->_right; //即图中的parentLR节点
int bf = parentLR->_bf;
rotateL(parentL); //先对cur为根的树进行左单旋
rotateR(parent); //再对parent为根的树进行右单旋
//通过对旋转前parentLR平衡因子的大小区分插入情况
if (bf == 1)
{
parent->_bf = 0;
parentL->_bf = -1;
parentLR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
parentL->_bf = 0;
parentLR->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
parentL->_bf = 0;
parentLR->_bf = 0;
}
else //如果以上三张情况都不符合,说明发生错误
assert(false);
}
2.2.5 insert 完整代码
bool insert(const pair<K, V>& kv)
{
//树为空的情况单独讨论
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
//找到插入位置
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
parent = cur;
if (cur->_kv.first > kv.first)
cur = cur->_left;
else if (cur->_kv.first < kv.first)
cur = cur->_right;
else
return false;
}
//连接新节点
cur = new Node(kv);
if (parent->_kv.first > kv.first)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
//更新平衡因子,调节树的平衡
while (parent)
{
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
if (parent->_bf == 0)
break;
else if (abs(parent->_bf) == 1)
{
cur = parent;
parent = parent->_parent;
}
else if (abs(parent->_bf) == 2)
{
if (parent->_bf == 2 && cur->_bf == 1)
rotateL(parent);
else if (parent->_bf == -2 && cur->_bf == -1)
rotateR(parent);
else if (parent->_bf == 2 && cur->_bf == -1)
rotateRL(parent);
else
rotateLR(parent);
break;
}
else
{
assert(false);
}
}
return true;
}
2.3 isbalance 检查一棵树是否为AVL树
尽管我们写出了一个AVL树的插入算法,并可以通过这个插入算法创建一棵AVL树,但是我们并不能确保我们写的insert
算法是正确定,也即我们不能保证自己创建的树就是AVL树,因此有必要写一个函数isbalance()
来验证这棵树是否平衡,并满足AVL树的条件。
isbalance()
应该验证以下两个方面:
- 任意节点的左右子树的高度差是否小于2
- 任意节点的右子树与左子树的高度差是否等于该节点的平衡因子bf
可以很容易想到一个实现方法:
前序遍历每一个节点,求得该节点的左子树高度和右子树高度,验证上面的两个条件:
//求树的高度 int height(Node* root) { if (root == nullptr) return 0; return 1 + max(height(root->_left), height(root->_right)); } //前序遍历每个节点,验证: //1. 左右子树的**高度差是否小于2 //2. 右子树与左子树的高度差是否等于该节点的平衡因子bf bool _isbalance(Node* root) { if (root == nullptr) return true; int leftHeight = height(root->_left); int rightHeight = height(root->_right); if (abs(root->_bf) >= 2) { cout << "高度异常" << root->_kv.first << endl; return false; } if (rightHeight - leftHeight != root->_bf) { cout << "平衡因子异常" << root->_kv.first << endl; return false; } return _isbalance(root->_left) && _isbalance(root->_right); }
但是,上面这种方法是浪费了许多时间的——每一次继续向下遍历节点时,都要重新求一次该节点的左右子树的高度,但这显然是没必要的,我们应该像一种方法来省去这些不必要的计算
我们知道,树的高度等于
左右子树高度的最大值 + 1
,因此只要左右子树的高度确定了,这棵树的高度也就确定了所以,我么可以转变我们的思路:先判断左右子树的平衡,再判断根即整棵树的平衡。也就是说,要利用后序的思想来解决这个问题
int height(Node* root) { if (root == nullptr) return 0; return 1 + max(height(root->_left), height(root->_right)); } //利用引用来记录高度 //height必须是引用,如果只是一个整形变量,那么当递归退出后,并不会改变原来的值 bool _isbalance(Node* root, int& height) { if (root == nullptr) return true; //leftHeight表示左子树高度,rightHeight表示右子树高度 int leftHeight = 0, rightHeight = 0; //如果左子树或右子树发生错误,则返回false if (!_isbalance(root->_left, leftHeight) || !_isbalance(root->_right, rightHeight)) return false; if (abs(rightHeight - leftHeight) >= 2) { cout << "不平衡" << root->_kv.first << endl; return false; } if (rightHeight - leftHeight != root->_bf) { cout << "平衡因子异常" << root->_kv.first << endl; return false; } //走到这里,就说明左右子树已经验证完毕 //求得树的高度 height = max(leftHeight, rightHeight) + 1; return true; }
3. 总结
看到这里,相信大家已经对AVL树的结构和特点有了较为深刻的了解,对于AVL树的其他操作,例如删除节点erase()
,这里不再讲述,如有兴趣大家可以自行研究。
最后,我给出自己写的AVLTree类,如有错误,欢迎大家指出:
#pragma once
#include <iostream>
#include <cmath>
#include <cassert>
#include <ctime>
using namespace std;
template<class K, class V>
struct AVLTreeNode
{
typedef AVLTreeNode<K, V> Node;
Node* _left = nullptr;
Node* _right = nullptr;
Node* _parent = nullptr;
pair<K, V> _kv;
int _bf = 0; //bf为平衡因子,即[右子树高度 - 左子树高度]
AVLTreeNode(const pair<K, V>& kv)
: _kv(kv)
{}
};
template<class K, class V>
class AVLTree
{
public:
typedef AVLTreeNode<K, V> Node;
AVLTree() = default;
bool insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
//找到插入位置
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
parent = cur;
if (cur->_kv.first > kv.first)
cur = cur->_left;
else if (cur->_kv.first < kv.first)
cur = cur->_right;
else
return false;
}
//连接新节点
cur = new Node(kv);
if (parent->_kv.first > kv.first)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
//更新平衡因子,调节搜索二叉树的平衡
while (parent)
{
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
if (parent->_bf == 0)
break;
else if (abs(parent->_bf) == 1)
{
cur = parent;
parent = parent->_parent;
}
else if (abs(parent->_bf) == 2)
{
if (parent->_bf == 2 && cur->_bf == 1)
rotateL(parent);
else if (parent->_bf == -2 && cur->_bf == -1)
rotateR(parent);
else if (parent->_bf == 2 && cur->_bf == -1)
rotateRL(parent);
else
rotateLR(parent);
break;
}
else
{
assert(false);
}
}
return true;
}
void inOrder()
{
_inOrder(_root);
cout << endl;
}
bool isbalance()
{
int i = 0;
return _isbalance(_root, i);
}
private:
int height(Node* root)
{
if (root == nullptr)
return 0;
return 1 + max(height(root->_left), height(root->_right));
}
bool _isbalance(Node* root, int& height)
{
if (root == nullptr)
return true;
int leftHeight = 0, rightHeight = 0;
if (!_isbalance(root->_left, leftHeight) || !_isbalance(root->_right, rightHeight))
return false;
if (abs(rightHeight - leftHeight) >= 2)
{
cout << "不平衡" << root->_kv.first << endl;
return false;
}
if (rightHeight - leftHeight != root->_bf)
{
cout << "平衡因子异常" << root->_kv.first << endl;
return false;
}
height = max(leftHeight, rightHeight) + 1;
return true;
}
/*
* 左单旋:
* 将parent的右孩子的左孩子链接给parent的右
* 再将parent链接给parent右孩子的左
* 需要注意:parent的右孩子的左孩子为空的情况,parent为根节点的情况、parent->_parent的链接
* 平衡因子的调节
*/
void rotateL(Node* parent)
{
Node* pparent = parent->_parent;
Node* parentR = parent->_right;
Node* parentRL = parentR->_left;
parent->_right = parentRL;
if (parentRL)
parentRL->_parent = parent;
parentR->_left = parent;
parentR->_parent = pparent;
parent->_parent = parentR;
if (pparent == nullptr)
_root = parentR;
else
{
if (parent == pparent->_left)
pparent->_left = parentR;
else
pparent->_right = parentR;
}
parent->_bf = 0;
parentR->_bf = 0;
}
/*
* 右单旋:
* 将parent的左孩子的右孩子链接给parent的左
* 再将parent链接给parent的左孩子的右
*/
void rotateR(Node* parent)
{
Node* pparent = parent->_parent;
Node* parentL = parent->_left;
Node* parentLR = parentL->_right;
parent->_left = parentLR;
if (parentLR)
parentLR->_parent = parent;
parentL->_right = parent;
parent->_parent = parentL;
parentL->_parent = pparent;
if (pparent == nullptr)
_root = parentL;
else
{
if (parent == pparent->_left)
pparent->_left = parentL;
else
pparent->_right = parentL;
}
parent->_bf = 0;
parentL->_bf = 0;
}
/*
* 右左双旋:
* 先parent的右子树右旋,再parent左旋
* 平衡因子的更新:根据parentRL的平衡因子进行判断
*/
void rotateRL(Node* parent)
{
Node* parentR = parent->_right;
Node* parentRL = parentR->_left;
int bf = parentRL->_bf;
rotateR(parentR);
rotateL(parent);
if (bf == 0)
{
parent->_bf = 0;
parentR->_bf = 0;
parentRL->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = -1;
parentR->_bf = 0;
parentRL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
parentR->_bf = 1;
parentRL->_bf = 0;
}
else
assert(false);
}
/*
* 左右双旋:
* 先parent的左子树左旋,再parent右旋
* 平衡因子的更新:根据parentLR的平衡因子进行判断
*/
void rotateLR(Node* parent)
{
Node* parentL = parent->_left;
Node* parentLR = parentL->_right;
int bf = parentLR->_bf;
rotateL(parentL);
rotateR(parent);
if (bf == 1)
{
parent->_bf = 0;
parentL->_bf = -1;
parentLR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
parentL->_bf = 0;
parentLR->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
parentL->_bf = 0;
parentLR->_bf = 0;
}
else
assert(false);
}
void _inOrder(Node* root)
{
if (root == nullptr)
return;
_inOrder(root->_left);
cout << root->_kv.first << ' ';
_inOrder(root->_right);
}
private:
Node* _root = nullptr;
};