目录
前言
一、AVL树的性质
二、AVL树的实现
1、AVL树节点的定义
2、AVL树的基本框架
3、AVL树的查找
4、AVL树的插入
5、AVL树的检测
三、总结
AVL树就是我们的二叉搜索树的一个进阶版本,我们称它为平衡搜索二叉树,本文带着各位了解一下这是一颗怎么样的树;
AVL树是二叉搜索树的一个特殊版本,他在二叉搜索树的版本上新增了平衡因子这一概念;AVL树具有以下特性;
1、每一个结点的左子树与右子树均为AVL树(若左右子树存在的情况下)
2、每一个结点都额外储存了一个平衡因子,这个平衡因子等于左右子树的高度差,其高度差不能超过2(本文计算平衡因子是用左子树的高度减右子树的高度)
3、搜索一个节点的时间复杂度为O(log N)
为了方便后面的插入,这里我们维护一种三叉链的关系,多了一个指向父节点的指针,当然,我们同时也必须维护这个指向父节点的指针;
// 结点
template
struct AVLTreeNode
{
AVLTreeNode(const std::pair& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_bf(0)
{}
AVLTreeNode* _left;
AVLTreeNode* _right;
AVLTreeNode* _parent;
std::pair _kv;
// 平衡因子
int _bf;
};
树中我们只需保存一个根节点即可;
// AVLTree
template
class AVLTree
{
public:
typedef AVLTreeNode Node;
private:
// 根节点
Node* _root = nullptr;
};
AVL树的查找与我们前面写的二叉搜索树的查找一模一样;查找的key小于当前结点的key则去左子树查找,查找的key大于当前节点的key,则去右子树查找;
bool find(const K& key)
{
if (_root == nullptr)
return false;
Node* cur = _root;
while (cur)
{
if (key < cur->_kv.first)
{
cur = cur->_left;
}
else if(key > cur->_kv.first)
{
cur = cur->_right;
}
else
{
return true;
}
}
return false;
}
AVL树的插入相比则有很大的挑战性了;我们要插入结点,首先的找到插入的位置,与之前二叉搜索树一样,我们首先找到插入位置,并进行插入,然后再对树进行调整;插入部分代码如下(不完整,缺少调整代码)
bool insert(const std::pair& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
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 if (kv.first > parent->_kv.first)
{
parent->_right = cur;
}
// 维护三叉链的关系
cur->_parent = parent;
// 走到这一步,我们还需要做两步
// 1、更新平衡因子
// 2、查看是否需要调整使树平衡
}
return true;
}
接下来,我们一起探究平衡因子的更新,以及调整,首先,我们关于平衡因子的更新有如下三种情况;
我们主要要弄清向上调整平衡因子的本质就是因为当前层树的高度发生了变化,影响了上一次的平衡因子;
此种情况则无需继续向上更新父节点的循环银子了,直接退出循环即可;
调整完该父节点后,父节点所在子树平衡因子正常了,同时也不会影响上层的结点;因此退出循环,我们不难补充以下逻辑,使插入代码进一步完善;
bool insert(const std::pair& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
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 if (kv.first > parent->_kv.first)
{
parent->_right = cur;
}
// 维护三叉链的关系
cur->_parent = parent;
// 1、更新平衡因子
while (parent)
{
if (cur == parent->_left)
{
parent->_bf--;
}
else if (cur == parent->_right)
{
parent->_bf++;
}
// 更新平衡因子后的三种情况
if (parent->_bf == 1 || parent->_bf == -1)
{
// 继续往上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 0)
{
// 平衡了,无需继续往上更新了
break;
}
else if(parent->_bf == 2 || parent->_bf == -2)
{
// 2、就是当前子树出现了问题,需要进行旋转处理
break;
}
}
return true;
}
我们还有最后一步,调整结点结构,恢复平衡因子;这里又有四种情况,分别对应四种调整策略;
以上是三张具象图,那么都为右单旋的情况,那么右单旋如何操作呢?
void rotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 1、将subLR挂到parent的左边
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
// 提前保存parent的父亲
Node* pparnet = parent->_parent;
// 2、让subL做parent的父亲
subL->_right = parent;
parent->_parent = subL;
if (parnet == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (pparnet->_left == parent)
{
pparnet->_left = subL;
subL->_parent = pparnet;
}
else
{
pparnet->_right = subL;
subL->_parent = pparnet;
}
}
// 更新平衡因子
subL->_bf = parent->_bf = 0;
}
接着是第二种情况,左单旋,与右单旋类似;
void rotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 1、subRL挂到parent的右边
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
// 提前保存parent的父节点
Node* pparent = parent->_parent;
// 2、subR做父亲节点,左边挂parent
subR->_left = parent;
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = subR;
subR->_parent = pparent;
}
else
{
pparent->_right = subR;
subR->_parent = pparent;
}
}
// 更新平衡因子
subR->_bf = parent->_bf = 0;
}
接着是情况三,这种情况也有点复杂,我们首先画出抽象图与三张具象图供大家理解;
接着依次放出演示三张具象图供大家参考;
我们可以分为三种情况,分别为subLR结点的bf为0、1、-1时;我们分别设置不同的平衡因子;
void rotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
// 1、subL左单旋
rotateL(subL);
// 2、parent右单旋
rotateR(parent);
// 3、设置bf值
if (bf == -1)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
else if(bf == 0)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
最后是情况四了,情况四与情况三类似,只不过是先右单旋再左单旋;此处就不画具象图了。我们用抽象图推导;
void rotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
// 1、右单旋
rotateR(subR);
// 2、左单旋
rotateL(parent);
// 更新bf
if (bf == 1)
{
subRL->_bf = 0;
subR->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
subRL->_bf = 0;
subR->_bf = 1;
parent->_bf = 0;
}
else if (bf == 0)
{
subRL->_bf = 0;
subR->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
四个旋转写完了,我们可以分别判断在不同情况下用不同的旋转策略;融入插入代码,如下;
bool insert(const std::pair& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
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);
// err
if (kv.first < parent->_kv.first)
{
parent->_left = cur;
}
else if (kv.first > parent->_kv.first)
{
parent->_right = cur;
}
// 维护三叉链的关系
cur->_parent = parent;
// 更新平衡因子
while (parent)
{
if (cur == parent->_left)
{
parent->_bf--;
}
else if (cur == parent->_right)
{
parent->_bf++;
}
// 更新平衡因子后的三种情况
if (parent->_bf == 1 || parent->_bf == -1)
{
// 继续往上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 0)
{
// 平衡了,无需继续往上更新了
break;
}
else if(parent->_bf == 2 || parent->_bf == -2)
{
// 就是当前子树出现了问题,需要进行旋转处理
if (parent->_bf == -2 && cur->_bf == -1)
{
rotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == 1)
{
rotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
rotateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
rotateRL(parent);
}
break;
}
}
return true;
}
AVL树的插入写完了,我们需要测试一下我们的插入代码是否正确,我们分别写如下代码进行测试;
// 测试数据:
// {16, 3, 7, 11, 9, 26, 18, 14, 15}
// {4, 2, 6, 1, 3, 5, 15, 7, 16, 14}
public:
bool is_balance()
{
return _is_balance(_root);
}
private:
bool _is_balance(Node* root)
{
if (root == nullptr)
{
return true;
}
int left_height = _height(root->_left);
int right_height = _height(root->_right);
if (right_height - left_height != root->_bf)
{
std::cout << root->_kv.first << "平衡因子异常" << std::endl;
}
return abs(left_height - right_height) < 2
&& _is_balance(root->_left)
&& _is_balance(root->_right);
}
除了上述数据我们还可以拿一些随机数进行测试,代码如下;
void test_AVLTree2()
{
srand(time(nullptr));
const int N = 500000;
MySpace::AVLTree t1;
for (int i = 0; i < N; i++)
{
int num = rand() + i;
t1.insert(make_pair(num, num));
}
cout << "is_balance: " << t1.is_balance() << endl;
}
能通过该代码,这个AVL树的插入才是真的没问题了;
AVL树是一种绝对平衡的二叉搜索树,其查找效率为O(log N),无论什么恶劣情况,都不会降低效率,但是由于其过于绝对,因此我们STL库中对于set与map的实现采用了另一种方案;那就是我们后面介绍的红黑树