目录
1. AVL概念
2. AVL树节点的定义
3. AVL树的插入
4. AVL树的旋转
5. AVL树的验证
6. AVL树的删除
7. AVL树的性能
暴力搜索、二分搜索、二叉搜索树、二叉平衡搜索树(AVL、红黑树)、多叉平衡搜索树(B树)、哈希表
1. AVL概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差 (简称平衡因子) 的绝对值不超过1 (-1 / 0 / 1) ,不是绝对的0是因为做不到绝对的平衡(例如,非满二叉树)
平衡因子:右子树高度 - 左子树高度
满二叉树:2^h - 1 = N-> 约等于logNAVL树:2^h - X = N (X范围 [ 1, 2^(h - 1) - 1 ] )
2. AVL树节点的定义
直接写key—value版本,因为它与单key版本,只是多了一个对应value(pair就解决了),可以看二叉搜索树一文再理解两个版本的关系
加入平衡因子会更加方便
template<class K, class V>
struct AVLTreeNode
{
int _bf; //balance factor
pair<K, V> _kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
{}
};
3. AVL树的插入
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:
-
按照二叉搜索树的方式找到合适点插入新节点
-
调整节点的平衡因子
- 新增在左边,parent平衡因子--
- 新增在右边,parent平衡因子++
- 如果插入后,节点的平衡因子变成了0,那么说明这个节点的左右子树高度平衡了,高度没有增大或减小,所以不会对往上的祖先产生影响,就不需要往上更新祖先的平衡因子
- 如果插入后,节点的平衡因子变成了1 或 -1,那么说明这个节点的左右子树高度不平衡,高度增大了或减小了,会对其祖先的平衡因子产生影响,所以使用 parent 指针向上更新平衡因子(这就体现了 parent 指针的用武之地),直到某一个祖先的平衡因子变为 0 或根节点就停止!!!(因为如果一个节点的高度没变,即平衡因子为0,那么它就不影响parent节点)
- 如果插入后,节点的平衡因子变成了2 或 -2,说明parent所在的子树的高度变化且不平衡了!这时就要体现 AVL 平衡的特点,对 parent 所在子树进行旋转
- 不会出现3 或 -3以上情况,因为它们必然会经历2 或 -2的情况,那时我们早就已经处理完了
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
// ... 控制平衡
// 更新平衡因子
while (parent)
{
if (cur == parent->_left)
{
parent->_bf--;
}
else // if (cur == parent->_right)
{
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 && 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 if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
break;
}
else //正常代码不会执行到这里,所以抛异常
{
assert(false);
}
}
return true;
}
4. AVL树的旋转
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
新节点插入较高右子树的右侧 --- 右右:左单旋
cur 的 left 链接到 parent 的 right,再将 parent 链接到 cur 的 left
在操作时,不仅要更新_bf平衡因子,还要额外注意父节点指针,一不留心就会出现三叉链,而且还要注意,parent 是不是 root,如果不是根节点,那么还要提前记录 parent 的 parent,以便使cur 的 parent 指向修改
符合左单旋的场景无穷尽,下图直接概括了左单旋的抽象总结图:
但是无论h高度是多少,节点的操作是一样的
//左单旋
void RotateL(Node* parent)
{
++_rotateCount;
Node* cur = parent->_right;
Node* curleft = cur->_left;
//开始链,注意父亲链
parent->_right = curleft;
if (curleft) //这里判断cur的right是不是为空,为空的话就不能使用指针链接
{
curleft->_parent = parent;
}
cur->_left = parent;
Node* ppnode = parent->_parent;
parent->_parent = cur;
//判断parent的父亲的情况
if (parent == _root)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
//记得父亲链
cur->_parent = ppnode;
}
//不要忘了平衡因子
parent->_bf = cur->_bf = 0;
}
新节点插入较高左子树的左侧---左左:右单旋
简单分析后即可总结出上图:
//右单旋
void RotateR(Node* parent)
{
++_rotateCount;
Node* cur = parent->_left;
Node* curright = cur->_right;
//开始链,注意父亲链
parent->_left = curright;
if (curright) //这里判断cur的right是不是为空,为空的话就不能使用指针链接
{
curright->_parent = parent;
}
Node* ppnode = parent->_parent;
cur->_right = parent;
parent->_parent = cur;
//判断parent的父亲的情况
if (ppnode == nullptr)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
//记得父亲链
cur->_parent = ppnode;
}
//不要忘了平衡因子
parent->_bf = cur->_bf = 0;
}
新节点插入较高右子树的左侧--- 右左:先右单旋再左单旋
//右左双旋
void RotateRL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
int bf = curleft->_bf;
RotateR(parent->_right);
RotateL(parent);
//分情况更新平衡因子
if (bf == 0)
{
cur->_bf = 0;
curleft->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
cur->_bf = 0;
curleft->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
cur->_bf = 1;
curleft->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
代码解析:在插入一个节点之后,向上更新平衡因子,直到遇见以上四种情况,开始旋转处理
新节点插入较高左子树的右侧 --- 左右:先左单旋再右单旋
//左右双旋
void RotateLR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
int bf = curright->_bf;
RotateL(parent->_left);
RotateR(parent);
//分情况更新平衡因子
if (bf == 0)
{
parent->_bf = 0;
cur->_bf = 0;
curright->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
cur->_bf = 0;
curright->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = 0;
cur->_bf = -1;
curright->_bf = 0;
}
}
如果程序出错了,数据量很大调试就很难受了,这时候可以一段一段注释调试,对于不同场景可能不合适,所以还可以手写一个判断函数,让计算机协助你找到错误,打开监视窗口、打断点,要带着思路、有预测结果的去调试,中间可以使用条件断点(可以手写if,里面写一些语句,程序走到这里时停下)
总结:
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
- pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR,当pSubR的平衡因子为1时,执行左单旋;当pSubR的平衡因子为-1时,执行右左双旋
- pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL,当pSubL的平衡因子为-1是,执行右单旋;当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
5. AVL树的验证
//递归求高度,测试代码是否正确
int Height()
{
return Height(_root);
}
int Height(Node* root)
{
if (root == nullptr)
return 0;
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
//测试代码是否正确,是不是AVL结构
bool IsBalance()
{
return IsBalance(_root);
}
bool IsBalance(Node* root)
{
if (root == nullptr)
return true;
int leftHight = Height(root->_left);
int rightHight = Height(root->_right);
if (rightHight - leftHight != root->_bf)
{
cout << "平衡因子异常:" << root->_kv.first << "->" << root->_bf << endl;
return false;
}
return abs(rightHight - leftHight) < 2
&& IsBalance(root->_left)
&& IsBalance(root->_right);
}
我们使用随机生成的数来测试
#include "AVLTree.h"
#include <vector>
const int N = 1000000;
int main()
{
AVLTree<int, int> at;
vector<int> v;
v.reserve(N);
srand(time(0));
for (int i = 0; i < N; i++)
{
v.push_back(rand());
}
for (auto e : v)
{
at.Insert(make_pair(e, e));
}
cout << at.IsBalance() << endl;
return 0;
}
6. AVL树的删除
按搜索树的规则查找节点进行删除,与插入时对节点的平衡因子更新相反,如果删除节点之后 parent 平衡因子为0,那么需要继续往上更新,如果是 1 或 -1,那么不需要往上更新
而且删除时,双旋情况更多,平衡因子的更新更复杂
7. AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 logN 但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合