前言
二叉搜索树是具有特殊存储结构的树,
任意根节点的左子树的所有节点值都比根节点的值小,右子树的所有节点值都比根节点大。
这种特殊的存储结构使得查找的效率大大提升,为logN
。但是还有缺陷。
因为二叉搜索树的构建是一个节点一个节点的插入,每次插入会找到合适的位置,但如果插入的节点的大小是顺序插入
的,就会出现歪脖子树
这样的查找效率还是N
,就失去了特性。
而AVL树,平衡二叉搜索树
就是在二叉搜索树的基础上,解决了歪脖子树这一特例的树。
接下来,我们就来学习AVL树
文章目录
- 前言
- 一. AVL树
- 二. AVL树节点的插入
- 1. 节点的定义
- 2. 节点的插入
- 3. 平衡因子更新
- 4. 左单旋/右单旋
- 5. 左右双旋
- 6. 完整代码
- 四. 完整代码
- 结束语
一. AVL树
AVL树就是平衡二叉搜索树。为了解决歪脖子的二叉搜索树,我们规定二叉搜索树的每个节点的左右子树高度差的绝对值不超过1。
节点上的红色数字
就是每个节点的高度
而左右子树的高度差就是拿左子树的高度-右子树的高度,或者右子树的高度-左子树的高度。
本篇博客规定,高度差是右子树-左子树
。所以15节点的高度差是-1,6节点的高度差是0,7节点的高度差也是-1。AVL树存储的也是KV值
而所有节点的高度差的绝对值都不大于1,那这棵树就是平衡的。
二. AVL树节点的插入
1. 节点的定义
AVL树的实现方式有很多种,本篇博客仅介绍一种。
节点的插入跟二叉搜索树的插入一致,但是当插入节点后,有节点的高度差不符合规定,那么我们需要对这棵树进行调整,所以首先我们可以有一个成员变量,存储当前节点的高度差,我们把它叫作平衡因子
。
高度的其中一种调整如下图
节点上黑色的数字是平衡因子
,也就是高度差。
可以看到,在插入节点10后,7节点的高度差变成了2,不符合规定,需要调整,我们把这种调整称为旋转
所以新节点的插入会引起祖先节点的平衡因子的改变,所以我们需要回溯
,为此我们使用三叉链
的树结构。
即节点的定义中,有左孩子指针,右孩子指针,还有双亲指针
节点的定义如下:
//三叉链
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode<K,V>*_left;//左指针
AVLTreeNode<K,V>*_right;//右指针
AVLTreeNode<K, V>*_parent;//双亲指针
pair<K, V>_kv;//KV值
int _bf;//平衡因子
//构造
AVLTreeNode(const pair<K,V>&kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
_bf(0)
{
}
};
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode Node;
private:
Node*_root=nullptr;
};
这就是AVL树的基本结构。
AVL树,节点的插入同二叉搜索树。但是AVL树要保持平衡,所以在插入后还要根据情况调整节点。
所以AVL节点的插入可以分为3步
1.节点插入
2.平衡因子更新
3.旋转
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->_left;
}
else if (cur->_kv.first < kv.first)
{
parent = cur;
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;
return true;
}
3. 平衡因子更新
我们规定平衡因子是右子树的高度-左子树的高度
所以,如果插入节点是父亲节点的右节点,那么父亲节点的平衡因子就+1
是父亲节点的左节点,那么父亲节点的平衡因子就-1
而插入一个新节点,受影响的只有其祖先
如果插入4,那么影响的只有左图圈出的部分;如果插入13,那么影响的只有右图圈出的部分。
改变插入节点的父亲节点后,还要不要继续向上调整,有以下三种情况:
- 改变后,父节点的平衡因子变成
1/-1
,代表父节点的平衡因子原先是0
。
因为原先父节点的平衡因子是0,变成1/-1后,说明高度变了
,那么父节点所在子树也变了,所以需要继续向上更新
。 - 改变后,父节点的平衡因子变成
2/-2
。
虽然高度也变了,但是已经不平衡
了。不需要继续向上更新
,而是直接进行旋转
,调整高度 - 改变后,节点的平衡因子变成0
说明当前子树变得更平衡了
,不需要往上更新
插入节点之后更新平衡因子
//更新平衡因子
while (parent)
{
//左减右加
if (cur == parent->_left)
{
--parent->_bf;
}
else
{
++parent->_bf;
}
//判断是否需要继续向上更新
if (parent->_bf == 1 || parent->_bf == -1)
{
//需要继续向上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//不需要继续更新,需要旋转
//旋转:1.让AVL树变得更平衡 2.降高度
break;
}
else if (parent->_bf == 0)
{
//更平衡了,不需要继续向上更新
return true;
}
else
{
//出现别的情况,说明当先AVL树出问题了,直接报错
assert(false);
}
}
4. 左单旋/右单旋
节点的插入和平衡因子的更新都完成后,当parent的平衡因子变成2/-2时,我们还需要根据情况进行不同的旋转
首先是单旋
我们先使用抽象图进行分析
我们举例h=0/1/2三种情况
在b位置插入也会改变高度,但不是左单旋,这里先不作讨论
在b位置插入不是左单旋,此处先不作讨论
h==2的时候,c的位置一定是x形的
证明如下
假设c是y形的,那么插入节点有这三种情况
第一种情况变得更平衡了,不需要单旋
第二种情况在子树就已经出现-2,子树就不是AVL树了,更新不到30,不符合
第三种情况同第二种
所以单旋中,c的位置一定是x形
,而a/b是任意一种
,所以h为2时,树的结构有9种,插入位置有4种,共36种可能,但是都可以利用左单旋解决
接下来,我们就来讲解左单旋的操作步骤
我们以h==1作例子
首先,b是比30大,比60小的节点
将b变成30的右孩子
再让30变成60的左孩子
最后再更新平衡因子
左单旋就结束了
我们用代码实现一下
我们将需要改变的节点定义一下
//左单旋
void RotateL(Node*parent)
{
Node*subR = parent->_right;
Node*subRL = subR->_left;
//1. 将subRL变成parent的右节点
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
//记录当前子树的父节点
Node*ppnode = parent->_parent;
//2. 将parent变成subR的左节点
subR->_left = parent;
parent->_parent = subR;
//3. 链接ppnode
if (ppnode == nullptr)
{
//如果是ppnode是空,代表parent是根节点
//更新根
_root = subR;
_root->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subR;
}
else
{
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
//4. 更新平衡因子
parent->_bf = 0;
subR->_bf = 0;
}
右单旋的原理根左单旋基本一致
将subLR变成parent的左节点
再将parent变成subL的右节点
再更新平衡因子
。
代码如下:
//右单旋
void RotateR(Node*parent)
{
Node*subL = parent->_left;
Node*subLR = subL->_right;
//1.将subLR变成parent的左节点
parent->_left = subLR;
//subLR可能是NULL,不是NULL才链接
if (subLR)
subLR->_parent = parent;
//2.再将parent变成subL的右节点
Node*ppnode = parent->_parent;//因为parent不一定是根节点,所以需要记录爷爷节点
subL->_right = parent;
parent->_parent = subL;
//3.链接parent指针
if (ppnode == nullptr)
{
//如果是根节点
_root = subL;
_root->_parent = nullptr;
}
else
{
//反之不是
if (ppnode->_left == parent)
{
ppnode->_left = subL;
}
else
{
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
//4.修改平衡因子
//parent和subL的平衡因子都变成0
parent->_bf=subL->_bf=0;
}
左右单旋的使用时机是
if (parent->_bf == 2 && cur->_bf == 1)
{
//右边比较高,左单旋
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
//左边比较高,右单旋
RotateR(parent);
}
5. 左右双旋
双旋的抽象图是这样的
接下来,我们照样分为h=0/1/2,三种情况分析
h==0的情况其实就是单旋时,在b位置插入节点的情况。此时如果只是左单旋,无法解决问题
同样,只左单旋无法解决问题
h==2的情况和单旋时讲解的类似
双旋
左右双旋
我们举h=1的情况,插入节点后,AVL树变得不平衡。
我们先对30进行左旋
,将左边变得更高
然后再对90右旋
,让右边变平衡
最后还需要更新平衡因子
因为旋转后的平衡因子不一定都为0,所以两次单旋后,还需要再更新平衡因子
有这样三种情况
对应的代码是这样的
//左右双旋
void RotateLR(Node*parent)
{
Node*subL = parent->_left;
Node*subLR = subL->_right;
int bf = subLR->_bf;
//先对subL进行左旋
RotateL(subL);
//再对parent右旋
Rotate(parent);
//更新平衡因子
if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
//出现其他情况代表出问题了
assert(false);
}
}
右左双旋
右左双旋就是先对subR右旋
再对parent左旋
最后更新平衡因子
对应代码如下:
//右左双旋
void RotateRL(Node*parent)
{
Node*subR = parent->_right;
Node*subRL = subR->_left;
int bf = subRL->_bf;
//先对subR进行右旋
RotateR(subR);
//再对parent左旋
RotateL(parent);
//更新平衡因子
if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else
{
//出现其他情况代表出问题了
assert(false);
}
}
二者的使用情况如下:
if (parent->_bf == 2 && cur->_bf == -1)
{
//右左双旋
RotateRL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
//左右双旋
RotateLR(parent);
}
else
{
//出现别的情况
assert(false);
}
6. 完整代码
//插入节点
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->_left;
}
else if (cur->_kv.first < kv.first)
{
parent = cur;
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 == 1 || parent->_bf == -1)
{
//需要继续向上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//不需要继续更新,需要旋转
//旋转:1.让AVL树变得更平衡 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);
}
else
{
//出现别的情况
assert(false);
}
break;
}
else if (parent->_bf == 0)
{
//更平衡了,不需要继续向上更新
return true;
}
else
{
//出现别的情况,说明当先AVL树出问题了,直接报错
assert(false);
}
}
return true;
}
//左单旋
void RotateL(Node*parent)
{
Node*subR = parent->_right;
Node*subRL = subR->_left;
//1. 将subRL变成parent的右节点
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
//记录当前子树的父节点
Node*ppnode = parent->_parent;
//2. 将parent变成subR的左节点
subR->_left = parent;
parent->_parent = subR;
//3. 链接ppnode
if (ppnode == nullptr)
{
//如果是ppnode是空,代表parent是根节点
//更新根
_root = subR;
_root->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subR;
}
else
{
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
//4. 更新平衡因子
parent->_bf = 0;
subR->_bf = 0;
}
//右单旋
void RotateR(Node*parent)
{
Node*subL = parent->_left;
Node*subLR = subL->_right;
//1.将subLR变成parent的左节点
parent->_left = subLR;
//subLR可能是NULL,不是NULL才链接
if (subLR)
subLR->_parent = parent;
//2.再将parent变成subL的右节点
Node*ppnode = parent->_parent;//因为parent不一定是根节点,所以需要记录爷爷节点
subL->_right = parent;
parent->_parent = subL;
//3.链接parent指针
if (ppnode == nullptr)
{
//如果是根节点
_root = subL;
_root->_parent = nullptr;
}
else
{
//反之不是
if (ppnode->_left == parent)
{
ppnode->_left = subL;
}
else
{
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
//4.修改平衡因子
//parent和subL的平衡因子都变成0
parent->_bf=subL->_bf=0;
}
//左右双旋
void RotateLR(Node*parent)
{
Node*subL = parent->_left;
Node*subLR = subL->_right;
int bf = subLR->_bf;
//先对subL进行左旋
RotateL(subL);
//再对parent右旋
RotateR(parent);
//更新平衡因子
if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
//出现其他情况代表出问题了
assert(false);
}
}
//右左双旋
void RotateRL(Node*parent)
{
Node*subR = parent->_right;
Node*subRL = subR->_left;
int bf = subRL->_bf;
//先对subR进行右旋
RotateR(subR);
//再对parent左旋
RotateL(parent);
//更新平衡因子
if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else
{
//出现其他情况代表出问题了
assert(false);
}
}
四. 完整代码
#pragma once
#include<iostream>
#include<cassert>
using namespace std;
//三叉链
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>*_left;//左指针
AVLTreeNode<K, V>*_right;//右指针
AVLTreeNode<K, V>*_parent;//双亲指针
pair<K, V>_kv;//KV值
int _bf;//平衡因子 高度差 右子树高度-左子树高度
//构造
AVLTreeNode(const pair<K,V>&kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_bf(0)
{
}
};
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K,V> Node;
public:
//插入节点
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->_left;
}
else if (cur->_kv.first < kv.first)
{
parent = cur;
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 == 1 || parent->_bf == -1)
{
//需要继续向上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//不需要继续更新,需要旋转
//旋转:1.让AVL树变得更平衡 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);
}
else
{
//出现别的情况
assert(false);
}
break;
}
else if (parent->_bf == 0)
{
//更平衡了,不需要继续向上更新
return true;
}
else
{
//出现别的情况,说明当先AVL树出问题了,直接报错
assert(false);
}
}
return true;
}
//中序遍历
void InOrder()
{
_InOrder(_root);
cout << endl;
}
//任意节点的高度
int Height(Node*root)
{
if (root == nullptr)
return 0;
int HeightL = Height(root->_left);
int HeightR = Height(root->_right);
return HeightL > HeightR ? HeightL + 1 : HeightR + 1;
}
//是否是平衡二叉树
bool IsBalance()
{
return _IsBalance(_root);
}
private:
//左单旋
void RotateL(Node*parent)
{
Node*subR = parent->_right;
Node*subRL = subR->_left;
//1. 将subRL变成parent的右节点
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
//记录当前子树的父节点
Node*ppnode = parent->_parent;
//2. 将parent变成subR的左节点
subR->_left = parent;
parent->_parent = subR;
//3. 链接ppnode
if (ppnode == nullptr)
{
//如果是ppnode是空,代表parent是根节点
//更新根
_root = subR;
_root->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subR;
}
else
{
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
//4. 更新平衡因子
parent->_bf = 0;
subR->_bf = 0;
}
//右单旋
void RotateR(Node*parent)
{
Node*subL = parent->_left;
Node*subLR = subL->_right;
//1.将subLR变成parent的左节点
parent->_left = subLR;
//subLR可能是NULL,不是NULL才链接
if (subLR)
subLR->_parent = parent;
//2.再将parent变成subL的右节点
Node*ppnode = parent->_parent;//因为parent不一定是根节点,所以需要记录爷爷节点
subL->_right = parent;
parent->_parent = subL;
//3.链接parent指针
if (ppnode == nullptr)
{
//如果是根节点
_root = subL;
_root->_parent = nullptr;
}
else
{
//反之不是
if (ppnode->_left == parent)
{
ppnode->_left = subL;
}
else
{
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
//4.修改平衡因子
//parent和subL的平衡因子都变成0
parent->_bf=subL->_bf=0;
}
//左右双旋
void RotateLR(Node*parent)
{
Node*subL = parent->_left;
Node*subLR = subL->_right;
int bf = subLR->_bf;
//先对subL进行左旋
RotateL(subL);
//再对parent右旋
RotateR(parent);
//更新平衡因子
if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
//出现其他情况代表出问题了
assert(false);
}
}
//右左双旋
void RotateRL(Node*parent)
{
Node*subR = parent->_right;
Node*subRL = subR->_left;
int bf = subRL->_bf;
//先对subR进行右旋
RotateR(subR);
//再对parent左旋
RotateL(parent);
//更新平衡因子
if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else
{
//出现其他情况代表出问题了
assert(false);
}
}
//中序遍历
void _InOrder(Node*root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << " ";
_InOrder(root->_right);
}
//验证是否平衡
bool _IsBalance(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;
//高度差
int diff = rightHeight - leftHeight;
if (diff != root->_bf || diff > 1 || diff < -1)
return false;
//任意节点都要平衡
return _IsBalance(root->_left) && _IsBalance(root->_right);
}
private:
Node*_root=nullptr;
};
结束语
本篇博客没有详细讲解AVL树节点的删除
删除本身跟插入类似,大致步骤是先找到节点,然后中和二叉搜索树的删除和AVL树的插入
如果删除的不是叶子节点,要考虑托孤
,即找其他节点替代原先位置
;然后再调节平衡因子
注意:平衡因子的调节,如果调节后parent的平衡因子为1或-1,说明高度没有变
,为0才说明高度改变
,需要继续向上更新。
本篇内容到此就结束了,感谢你的阅读!
如果有补充或者纠正的地方,欢迎评论区补充,纠错。如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。