⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C++进阶
⭐代码仓库:C++进阶
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!
AVL树(C++实现)
- 一、概念
- 二、AVL树结点的定义
- 三、AVL树的插入(难)
- 1、找
- 2、插
- 3、控制平衡 -- 看是否需要修改平衡因子
- 4、旋转(判断需不需要旋转,判断是左旋还是右旋,判断是单旋还是双旋)
- 插入总代码
- 新节点插入较高左子树的左侧---左左:右单旋
- 右单旋步骤:
- 新结点插入较高右子树的右侧--右右:左单旋
- 左单旋旋转步骤:
- 左右双旋
- 左右双旋的步骤
- 左右双旋后满足二叉搜索树的性质
- 更新平衡因子
- 代码
- 右左双旋
- 右左双旋的步骤
- 右左双旋后满足二叉搜索树的性质
- 更新平衡因子
- 代码
- 四、AVL树的查找
- 五、AVL树的修改
- 直接修改法
- 利用插入函数修改
- 六、AVL树的重载
- 七、AVL树的删除(难)
- 八、验证AVL树
- 先验证是否为二叉搜索树
- 再验证是否为平衡树(两边子树的高度)
- 九、AVL树的性能
一、概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
我们利用AVL树的概念:
AVL树可以是一棵空树,也可以是具有以下性质的一颗二叉搜索树:
1、它的左右子树都是AVL树,都满足AVL树的性质
2、左右子树的高度差(平衡因子)不超过1(-1/0/1)
3、平衡因子=右子树高度-左子树高度(人为规定)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(logN),搜索时间复杂度也是O(logN)。
因为AVL树控制平衡因子,这就使我们的AVL树查找的时候很方便,特别是数据量很大的情况下,时间复杂度是O(logN),10亿个数据只需要查找30次而已,效率得到很大的提升。
二、AVL树结点的定义
我们在这里实现K-V模型的AVL树,为了后续方便操作,我们这里是将AVL树定义为三叉链,也就是一个结点链接左右子树,这个结点的孩子结点再链接parent结点链接上它的父亲结点,这就是我们定义的三叉链的结构,此外我们需要在每个结点中引入平衡因子,即右子树减左子树的大小,而这个平衡因子刚开始为0即可,如下定义:
//Key-Value模型
template<class K, class V>
struct AVLTreeNode
{
//构造函数
AVLTreeNode(const pair<K,V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{}
// 定义三叉链
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
//存储键位值
pair<K, V> _kv;
//平衡因子
int _bf;
};
三、AVL树的插入(难)
AVL树的插入有四大秘诀,分别是:
1、找(先找到需要插入的位置)
2、插(插入操作)
3、更新(更新平衡因子)
4、旋转(根据平衡因子的不满足条件进行左/右旋转)
1、找
找的方法很简单,和我们之前写的搜索二叉树是一样的,我们在下面直接写下模板:
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
// 插入
bool Insert(const pair<K, V>& kv)
{
// 找
// 1、待插入结点key比当前结点小就往左子树跑
// 2、待插入结点key比当前结点大就往右子树跑
// 3、待插入结点key和当前结点的值相等就显示插入失败
// 刚开始进去为空树
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
// 不是空树
while (cur)
{
if (cur->_kv.first < kv.first)
{
parent = cur; // 保存一下cur原本的结点
cur = cur->_right; // 往左跑
}
else if (cur->_kv.first > kv.first)
{
parent = cur; // 保存一下cur原本的结点
cur = cur->_left;
}
else
{
// 发现待插入的结点的值和当前节点相同
return false;
}
}
}
private:
Node* _root = nullptr;
};
2、插
我们前面利用的parent结点这时候就发挥出奇效了,因为我们的cur最后一步绝对是走到nullptr,也就是空结点,所以接下来我们就利用parent和cur的构造来进行链接插入关系:
// 插入数值
cur = new Node(kv);
// 父亲结点小于待插入结点
if (parent->_kv.first < kv.first)
{
// 往右子树插入
parent->_right = cur;
}
// 父亲结点小于待插入结点
else
{
// 往左子树插入
parent->_left = cur;
}
// 当前节点的父节点为parent结点,更新一下防止关系乱
cur->_parent = parent;
3、控制平衡 – 看是否需要修改平衡因子
判断完插入成功与否,是不是就要判断平衡因子的更新了?
平衡因子是否更新取决于:该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的 祖先结点的平衡因子可能需要更新。
我们来一个图来解释一下更新:
我们插入结点以后,发现有些更新的结点不仅仅会影响父结点,还会影响其父节点的父节点,甚至再往上影响,也就是说这个更新结点会影响父系的结点。
更新平衡因子的规则:
1、新增在右,parent->_bf++
2、新增在左,parent->_bf–
然而我们发现一个现象,这个新增在左在右对爷结点似乎影响有不同,我新增的结点在单独一颗左/右子树的分支,那么这个爷结点也要更新,假如说我这颗树的高度并没有发生改变,爷爷结点的平衡因子不会发生任何改变,所以我们需要判断parent结点的平衡因子!我们有如下结论:
1、如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子
2、如果parent的平衡因子等于0;表明无需往上更新平衡因子
3、如果parent的平衡因子等于-2或者2;就已经不平衡了,需要旋转处理!
4、如果parent的平衡因子大于2或者小于-2;就说明之前插入的就不是AVL树了,往前检查,肯定有步骤错了
因为是更新完parent的结点如果需要继续往上更新的话,需要提供一个迭代的代码继续往上更新,所以继续往上更新的逻辑如下代码:
cur = parent;
parent = parent->_parent;
4、旋转(判断需不需要旋转,判断是左旋还是右旋,判断是单旋还是双旋)
当平衡因子出现了2/-2的情况,要对子树进行旋转处理,但也要遵守原则:
旋转成平衡树
保持搜索树的规则
而旋转有四种大情况,对此我们要进行分类:
1、当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋
2、当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋
3、当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋
4、当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋
注意:进行完旋转以后就无需往上更新平衡因子了,因为这个子树本身的高度已经没有发生变化了,根本不会影响到父节点的平衡因子。
// 控制平衡
// 平衡因子的平衡
while (parent)
{
if (cur == parent->_right)
{
parent->_bf++;
}
else
{
parent->_bf--;
}
if (parent->_bf == 1 || parent->_bf == -1)
{
//继续往上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 0)
{
//over
break;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//旋转
if (parent->_bf == 2 && cur->_bf == 1)
{
// 左单旋
RoateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
// 右单旋
RoateR(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
// 左右单旋
RoateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
// 右左单旋
RoateRL(parent);
}
}
else
{
assert(false);
}
}
插入总代码
// 插入
bool Insert(const pair<K, V>& kv)
{
// 找
// 1、待插入结点key比当前结点小就往左子树跑
// 2、待插入结点key比当前结点大就往右子树跑
// 3、待插入结点key和当前结点的值相等就显示插入失败
// 刚开始进去为空树
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 = cur->_right; // 往右跑
}
else if (cur->_kv.first > kv.first)
{
parent = cur; // 保存一下cur原本的结点
cur = cur->_left;
}
else
{
// 发现待插入的结点的值和当前节点相同
return false;
}
}
// 插入数值
cur = new Node(kv);
// 父亲结点小于待插入结点
if (parent->_kv.first < kv.first)
{
// 往右子树插入
parent->_right = cur;
}
// 父亲结点小于待插入结点
else
{
// 往左子树插入
parent->_left = cur;
}
// 当前节点的父节点为parent结点,更新一下防止关系乱
cur->_parent = parent;
// 控制平衡
// 平衡因子的平衡
while (parent)
{
if (cur == parent->_right)
{
parent->_bf++;
}
else
{
parent->_bf--;
}
if (parent->_bf == 1 || parent->_bf == -1)
{
//继续往上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 0)
{
//over
break;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//旋转
if (parent->_bf == 2 && cur->_bf == 1)
{
// 左单旋
RoateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
// 右单旋
RoateR(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
// 左右单旋
RoateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
// 右左单旋
RoateRL(parent);
}
break;
}
else
{
assert(false);
}
}
return true;
}
新节点插入较高左子树的左侧—左左:右单旋
下面抽象图:
右单旋步骤:
1、sublr成为parent的左子树(sublr和parent的关系)
2、parent成为subl的右子树(parent和subl的关系,要注意subl可能为空)
3、subl成为根节点(要注意parent是子树的根还是整棵树的根)
4、最后更新平衡因子
更新平衡因子:
代码:
// 右单旋
void RoateR(Node* parent)
{
// 三叉链
Node* subl = parent->_left;
Node* sublr = subl->_right;
Node* ppnode = parent->_parent;
//sublr和parent之间的关系
parent->_left = sublr;
if (sublr)
sublr->_parent = parent;
//subl和parent的关系
subl->_right = parent;
parent->_parent = subl;
//ppnode 和 subl的关系
if (ppnode == nullptr)
{
_root = subl;
subl->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subl;
}
else
{
ppnode->_right = subl;
}
subl->_parent = ppnode;
}
//更新平衡因子
subl->_bf = parent->_bf = 0;
}
新结点插入较高右子树的右侧–右右:左单旋
下面抽象图:
左单旋旋转步骤:
1、subrl变成parent的右子树(subl和parent的关系,要注意subl可能为空)
2、parent成为subr的左子树(parent和sublr的关系)
3、subr成为根节点(要注意parent是子树的根还是整棵树的根)
4、最后更新平衡因子
三个问题:
1、parent是整棵树的根还是只是子树的根的结点?
2、判断parent原本在ppnode的左还是右?
3、subrl为空的情况?
平衡因子更新:
代码:
// 左单旋
void RoateL(Node* parent)
{
// 三叉链
Node* subr = parent->_right;
Node* subrl = subr->_left;
Node* ppnode = parent->_parent;
// subrl与parent的关系
parent->_right = subrl;
if (subrl)
subrl->_parent = parent;
// subl和parent的关系
subr->_left = parent;
parent->_parent = subr;
// ppnode和subr的关系
if (ppnode == nullptr)
{
_root = subr;
subr->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subr;
}
else
{
ppnode->_right = subr;
}
subr->_parent = ppnode;
}
// 更新平衡因子
subr->_bf = parent->_bf = 0;
}
左右双旋
结点形状形同如下形式的我们更新的是需要先捋直,再折掉上面的那一段。
1、先插入新结点
2、捋直 – 以30为旋转点左单旋
3、折 – 以90为旋转点右单旋
左右双旋的步骤
1、以subl为旋转点左单旋
2、以parent为旋转点右单旋
3、更新平衡因子
左右双旋后满足二叉搜索树的性质
左右双旋后,实际上就是让subLR的左子树和右子树,分别作为subl和parent的右子树和左子树,再让subl和parent分别作为sublr的左右子树,最后让sublr作为整个子树的根(结合图理解)。
1、sublr的左子树当中的结点本身就比subL的值大,因此可以作为subl的右子树。
2、sublr的右子树当中的结点本身就比parent的值小,因此可以作为parent的左子树。
3、经过步骤1/2后,subL及其子树当中结点的值都就比sublr的值小,而parent及其子树当中结点的值都就比sublr的值大,因此它们可以分别作为sublr的左右子树。
更新平衡因子
平衡因子的更新随着sublr原始平衡因子的不同分为以下三种情况:
1、当sublr原始平衡因子是-1时,左右双旋后parent、subl、sublr的平衡因子分别更新为1、0、0。
2、当sublr原始平衡因子是1时,左右双旋后parent、subl、sublr的平衡因子分别更新为0、-1、0。
3、当sublr原始平衡因子是0时,左右双旋后parent、subl、sublr的平衡因子分别更新为0、0、0。
经过左右双旋后,即树的高度没有发生变化,所以无需继续往上更新平衡因子。
代码
// 左右双旋
void RoateLR(Node* parent)
{
Node* subl = parent->_left;
Node* sublr = subl->_right;
int bf = sublr->_bf;
//subl节点左单旋
RoateL(subl);
//parent节点进行右单旋
RoateR(parent);
//更新平衡因子
if (bf == 1)
{
sublr->_bf = 0;
subl->_bf = -1;
parent->_bf = 0;
}
else if (bf == -1)
{
sublr->_bf = 0;
subl->_bf = 0;
parent->_bf = 1;
}
else if (bf == 0)
{
sublr->_bf = 0;
subl->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
右左双旋
1、插入新结点
2、捋直 — 以90为旋转结点右单旋
3、折下来—以30为旋转结点左单旋
右左双旋的步骤
1、以subr的结点右单旋
2、以parent的结点左单旋
3、控制平衡因子
右左双旋后满足二叉搜索树的性质
右左双旋后,实际上就是让subrl的左子树和右子树,分别作为parent和subr的右子树和左子树,再让parent和subr分别作为subrl的左右子树,最后让subrl作为整个子树的根
1、subrl的左子树当中的结点本身就比parent的值大,因此可以作为parent的右子树。
2、subrl的右子树当中的结点本身就比subr的值小,因此可以作为subr的左子树。
3、经过步骤1/2后,parent及其子树当中结点的值都就比subrl的值小,而subr及其子树当中结点的值都就比subrl的值大,因此它们可以分别作为subrl的左右子树。
更新平衡因子
平衡因子的更新随着sublr原始平衡因子的不同分为以下三种情况:
1、当subrl原始平衡因子是-1时,右左双旋后parent、subr、subrl的平衡因子分别更新为0、1、0。
2、当subrl原始平衡因子是1时,左右双旋后parent、subr、subrl的平衡因子分别更新为-1、0、0。
3、当subrl原始平衡因子是0时,左右双旋后parent、subr、subrl的平衡因子分别更新为0、0、0。
代码
// 右左双旋
void RoateRL(Node* parent)
{
Node* subr = parent->_right;
Node* subrl = subr->_left;
int bf = subrl->_bf;
//subR右单旋
RoateR(subr);
//parent左单旋
RoateL(parent);
// 更新平衡因子
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);
}
}
四、AVL树的查找
AVL树的查找就是二叉搜索树的查找:
1、如果当前树为空,返回nullptr
2、待查找的结点小于当前结点,往左子树走
3、待查找的结点大于当前结点,往右子树走
4、待查找结点的值和当前结点的值相同,返回当前结点
// AVL树的查找
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
// 待查找的结点小于当前结点,往左子树走
if (key < cur->_kv.first)
{
cur = cur->_left;
}
// 待查找的结点大于当前结点,往右子树走
else if (key > cur->_kv.first)
{
cur = cur->_right;
}
// 结点值相等,找到了
else
{
return cur;
}
}
// 没这个结点,找不到
return nullptr;
}
五、AVL树的修改
两种方法:
直接修改法
1、用我们写的Find函数找到当前键值位为key的点
2、修改当前键值位为value的值
// AVL树的修改
bool Modify(const K& key, const V& value)
{
// 1、先找到利用Find函数
Node* ret = Find(key);
if (ret == nullptr)
return false;
// 2、修改value值
ret->_kv.second = value;
return true;
}
利用插入函数修改
为什么要利用插入函数修改,这是因为我们在插入的时候是有查找,指定位置插入,再进行判断平衡因子是否需要进行旋转!
// AVL树的修改2
pair<Node*, bool> Modify(const pair<K, V>& kv)
{
// 找
// 1、待插入结点key比当前结点小就往左子树跑
// 2、待插入结点key比当前结点大就往右子树跑
// 3、待插入结点key和当前结点的值相等就显示插入失败
// 刚开始进去为空树
if (_root == nullptr)
{
_root = new Node(kv);
return make_pair(_root, true);
}
Node* parent = nullptr;
Node* cur = _root;
// 不是空树
while (cur)
{
// 待插入结点比当前结点小,往右子树跑
if (cur->_kv.first < kv.first)
{
parent = cur; // 保存一下cur原本的结点
cur = cur->_right; // 往右跑
}
else if (cur->_kv.first > kv.first)
{
parent = cur; // 保存一下cur原本的结点
cur = cur->_left;
}
else
{
// 发现待插入的结点的值和当前节点相同
return make_pair(cur, false);
}
}
// 插入数值
cur = new Node(kv);
Node* newnode = cur;
// 父亲结点小于待插入结点
if (parent->_kv.first < kv.first)
{
// 往右子树插入
parent->_right = cur;
}
// 父亲结点小于待插入结点
else
{
// 往左子树插入
parent->_left = cur;
}
// 当前节点的父节点为parent结点,更新一下防止关系乱
cur->_parent = parent;
// 控制平衡
// 平衡因子的平衡
while (parent)
{
if (cur == parent->_right)
{
parent->_bf++;
}
else
{
parent->_bf--;
}
if (parent->_bf == 1 || parent->_bf == -1)
{
//继续往上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 0)
{
//over
break;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//旋转
if (parent->_bf == 2 && cur->_bf == 1)
{
// 左单旋
RoateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
// 右单旋
RoateR(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
// 左右单旋
RoateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
// 右左单旋
RoateRL(parent);
}
break;
}
else
{
assert(false);
}
}
return make_pair(newnode, true);
}
六、AVL树的重载
1、调用插入函数的键对值
2、拿出结点的key
3、返回value的引用
V()是匿名对象变量,因为不确定到底是什么,不确定第二个键对值是false还是true,所以用匿名,因为倘若是key不在树中的时候,就先插入key,V(),再返回value的引用,倘若是key已经在树中的时候,返回键对值对key的value引用!
// AVL[]重载
V& operator[](const K& key)
{
// 键对值
pair<Node*, bool> ret = Insert(make_pair(key, V()));
// 拿出结点的node
Node* node = ret.first;
// 返回该结点的value引用值
return node->_kv.second;
}
七、AVL树的删除(难)
删除的前期操作与我们的插入中的前期操作是一样的,我们首先要先找到这个要删除的结点,也就是需要在树中找到对应key值的结点,寻找待删除结点的方法和二叉搜索树相同:
1、先找到待删除的结点。
2、若找到的待删除结点的左右子树均不为空,则需要使用替换法(左子树最右结点或者是右子树最左结点)进行删除。
替换法:找到当前所需要删除的结点的位置,在其左子树中找到最大值的结点,或者是在其右子树中找到其值最小的结点,然后再将待删除结点的key值以及value值都改为代替其被删除的结点的值。
我们在找到结点要删除的时候先更新一下平衡因子,因为如果先删除了以后平衡因子会非常的乱,所以就需要先更新一下平衡因子,再进行删除操作。
更新平衡因子如下规则:
删除的结点在parent的右边,parent的平衡因子− −。
删除的结点在parent的左边,parent的平衡因子+ + 。
每更新完一个结点的平衡因子后,都需要进行以下判断:
如果parent的平衡因子等于-1或者1,无需继续往上更新平衡因子。
如果parent的平衡因子等于0,还需要继续往上更新平衡因子。
如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。
判断是否往上更新以及是否需要旋转处理的图例:
在将其旋转的时候,我们将其分为下面六种情况:
1、当parent的平衡因子为-2,parent的左孩子的平衡因子为-1时,进行右单旋。
2、当parent的平衡因子为-2,parent的左孩子的平衡因子为1时,进行左右双旋。
3、当parent的平衡因子为-2,parent的左孩子的平衡因子为0时,也进行右单旋,无需向上更新平衡因子,更新当前!
4、当parent的平衡因子为2,parent的右孩子的平衡因子为-1时,进行右左双旋。
5、当parent的平衡因子为2,parent的右孩子的平衡因子为1时,进行左单旋。
6、当parent的平衡因子为2,parent的右孩子的平衡因子为0时,也进行左单旋,无需向上更新平衡因子,更新当前。
旋转处理后我们需要更新平衡因子!
更新完平衡因子就是我们实际需要删除的操作了:
1、实际删除的结点的左子树为空,parent链接到实际删除结点的右子树,删除实际需要删除的结点。
2、实际删除的结点的右子树为空,parent链接到实际删除结点的左子树,删除实际需要删除的结点。
3、实际删除的结点的左右子树都不为空,则需要使用替代法(前面已经替代了)将这个结点给替换掉,然后再删除的步骤。
// AVL树的删除
bool Erase(const K& key)
{
// 用于遍历二叉树找结点
Node* parent = nullptr;
Node* cur = _root;
// 用于标记实际的删除结点及其父结点
Node* delparentpos = nullptr;
Node* delpos = nullptr;
// 先找到
while (cur)
{
// 所给key值小于当前节点的值 -- 往左树走
if (key < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
// 所给key值大于当前结点的值 -- 往右树走
else if (key > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
// 找到了
else
{
// 左子树为空
if (cur->_left == nullptr)
{
// 待删除结点是根节点
if (cur == _root)
{
// 让根节点的右子树作为新的结点
_root = _root->_right;
if (_root)
_root->_parent = nullptr;
delete cur; // 删除原节点
return true;
}
else // 不是根节点
{
delparentpos = parent; // 标记当前待删除结点的父节点
delpos = cur; // 标记当前待删除的结点
}
break; // 删除结点有祖先的结点,需要更新平衡因子
}
// 右子树为空
else if (cur->_right == nullptr)
{
// 待删除结点是根节点
if (cur == _root)
{
// 让根节点的左子树作为新的结点
_root = _root->_left;
if (_root)
_root->_parent = nullptr;
delete cur; // 删除原节点
return true;
}
else // 不是根节点
{
delparentpos = parent; // 标记当前待删除结点的父节点
delpos = cur; // 标记当前待删除的结点
}
break; // 删除结点有祖先的结点,需要更新平衡因子
}
// 左右子树都不为空
else
{
// 替换法
// 寻找待删除结点的右子树中的最小值
Node* minparent = cur;
Node* minright = cur->_right;
while (minright->_left)
{
minparent = minright; // 记录一下父节点
minright = minright->_left; // 往左子树走
}
cur->_kv.first = minright->_kv.first;// 将待删除结点first替换为右子树的最小值
cur->_kv.second = minparent->_kv.second;// 将待删除结点second替换为右子树的最小值
// 记录一下要删除的父节点
delparentpos = minparent;
// 记录一下实际要删除的结点
delpos = minright;
break; // 祖先结点的平衡因子需要改变
}
}
}
// 没有被修改过,说明没找到当前要删除的结点
if (delparentpos == nullptr)
return false;
// 记录当前要删除结点和当前要删除结点的父节点
Node* del = delpos;
Node* delP = delparentpos;
// 更新平衡因子
while (delpos != _root) // 最坏情况是一路更新到根节点
{
// 删除结点在右子树
if (delpos == delparentpos->_right)
delparentpos->_bf--;
// 删除结点在左子树
else if(delpos == delparentpos->_left)
delparentpos->_bf++;
// 判断是否需要旋转
if (delparentpos->_bf == 0)
{
// 向上更新
delpos = delparentpos;
delpos = delpos->_parent;
}
else if (delparentpos->_bf == -1 || delparentpos->_bf == 1)
{
// 要删除的父节点的平衡因子为1/-1,无需向上更新平衡因子
break;
}
else if(delparentpos->_bf == 2 || delparentpos->_bf == -2)
{
// 旋转
// 1、右旋
if (delparentpos->_bf == -2 && delparentpos->_left->_bf == -1)
{
// 记录一下右旋后的根节点
Node* right_root = delparentpos->_left;
// 右旋
RoateR(delparentpos);
// 更新根节点
delparentpos = right_root;
}
// 2、左右双旋
else if (delparentpos->_bf == -2 && delparentpos->_left->_bf == 1)
{
// 记录一下左右双旋后的根节点
Node* right_left_root = delparentpos->_left->_right;
// 左右双旋
RoateLR(delparentpos);
// 更新根节点
delparentpos = right_left_root;
}
// 3、右单旋
else if (delparentpos->_bf == -2 && delparentpos->_left->_bf == 0)
{
// 不需要往上更新节点
Node* right_root = delparentpos->_left;
RoateR(delparentpos);
delparentpos = right_root;
// 更新当前平衡因子
delparentpos->_bf = 1;
delparentpos->_right->_bf = -1;
break;
}
// 4、右左单旋
else if (delparentpos->_bf == -1 && delparentpos->_right->_bf == -1)
{
// 记录一下当前要删除的结点
Node* right_left_root = delparentpos->_right->_left;
RoateRL(delparentpos);
delparentpos = right_left_root;
}
// 5、左单旋
else if (delparentpos->_bf == 2 && delparentpos->_right->_bf == 1)
{
Node* left_root = delparentpos->_right;
RoateL(delparentpos);
delparentpos = left_root;
}
// 6、左单旋
else if (delparentpos->_bf == 2 && delparentpos->_parent == 0)
{
// 不需要向上更新节点
Node* left_root = delparentpos->_right;
RoateL(delparentpos);
delparentpos = left_root;
// 更新当前平衡因子
delparentpos->_bf = -1;
delparentpos->_left->_bf = 1;
break;
}
}
else
{
assert(false);
}
// 继续向上更新
delpos = delparentpos;
delparentpos = delparentpos->_parent;
}
// 实际删除操作
// 左子树为空
if (del->_left == nullptr)
{
// 实际删除的结点刚好是其父节点的左孩子
if (del == delP->_left)
{
delP->_left = del->_right;
if (del->_right)
del->_right->_parent = parent;
}
// 实际删除的结点刚好是其父节点的右孩子
else
{
delP->_right = del->_right;
if (del->_right)
del->_right->_parent = parent;
}
}
// 右子树为空
else
{
// 实际删除的结点刚好是其父节点的左孩子
if (del == delP->_left)
{
delP->_left = del->_left;
if (del->_left)
del->_left->_parent = parent;
}
// 实际删除的结点刚好是其父节点的右孩子
else
{
delP->_right = del->_left;
if (del->_left)
del->_left->_parent = parent;
}
}
// 删除实际结点
delete del;
return true;
}
八、验证AVL树
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
- 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树 - 验证其为平衡树
每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
节点的平衡因子是否计算正确
// 中序遍历验证是个搜索树
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
_InOrder(root->_right);
cout << root->_kv.first << ":" << root->_kv.second << endl;
}
void InOrder()
{
_InOrder(_root);
}
先验证是否为二叉搜索树
AVL树是在二叉搜索树的基础上加入了平衡性的限制,也就是说AVL树也是二叉搜索树,因此我们可以先获取二叉树的中序遍历序列,来判断二叉树是否为二叉搜索树。
// 中序遍历验证是个搜索树
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
}
再验证是否为平衡树(两边子树的高度)
中序有序只能证明是二叉搜索树,要证明二叉树是AVL树还需验证二叉树的平衡性,在该过程中我们可以顺便检查每个结点当中平衡因子是否正确。
采用后序遍历,遍历步骤如下:
1、从叶子结点处开始计算每课子树的高度(每棵子树的高度 = 左右子树中高度的较大值 + 1)并记录!
2、先判断左子树是否是平衡二叉树。
3、再判断右子树是否是平衡二叉树。
4、若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树了)
// 判断是不是平衡树
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;
}
else
{
cout << "没问题" << endl;
}
// 递归判断条件
return abs(rightHight - leftHight) < 2
&& IsBalance(root->_left)
&& IsBalance(root->_right);
}
九、AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2(N) log2(N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。