C++ - AVL平衡二叉树
目录
AVL树的概念与特点
定义与特点
平衡因子与高度
AVL树的操作
AVL树的旋转
AVL树的结构(这里开始讲代码)
AVL树的插入
更新平衡因子
旋转
右单旋
左单旋
左右双旋
右左双旋
AVL树的查找
AVL的平衡检测
AVL树的删除
AVL树的概念与特点
AVL树是一种自平衡的二叉查找树,平衡二叉树且搜索二叉树
定义与特点
定义:
AVL树是最先发明的自平衡二叉查找树。在AVL树中,任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树
特点:
- 它本身首先是一棵二叉搜索树。
- 带有平衡条件:每个节点的左右子树的高度之差的绝对值(平衡因子)最多为1。
- AVL树的基本操作一般涉及运做同在不平衡的二叉查找树所运做的同样的算法,但是要进行预先或随后做一次或多次所谓的“AVL旋转”。
平衡因子与高度
平衡因子:
每个节点都有一个平衡因子,任何节点的平衡因子等于其右子树的高度减去左子树的高度,也就是说,任何节点的平衡因子等于0/1/-1。虽然AVL树并不必须要平衡因子,但是有了平衡因子可以更方便观察和控制树是否平衡。
高度要求:
AVL树是高度平衡搜索二叉树,通过控制高度差去控制平衡。要求高度差不超过1,而不是高度差为0,是因为在某些情况下(如节点数为2或4时),高度差最好就是1,无法做到高度差为0。
AVL树的操作
插入操作:
- 插入一个值按二叉搜索树规则进行插入。
- 新增节点以后,只会影响祖先节点的高度,也就是可能会影响部分祖先节点的平衡因子。所以需要更新从新增节点到根节点路径上的平衡因子。
- 在更新平衡因子的过程中出现不平衡,需要对不平衡子树进行旋转。旋转后本质调平衡的同时,降低了子树的高度,不会再影响上一层,所以插入结束。
删除操作:
- 可以通过把要删除的节点向下旋转成一个叶子节点,接着直接剪除这个叶子节点来完成。
- 因为在旋转成叶子节点期间最多有logn个节点被旋转,而每次AVL旋转耗费恒定的时间,所以删除处理在整体上耗费O(logn)时间。
查找操作:
- 在AVL树中查找同在一般二叉搜索树(BST)中一样,所以耗费O(logn)时间。
- AVL树总是保持平衡的,不需要特殊的准备,树的结构不会由于查询而改变(与伸展树查找相对立,它会因为查找而变更树结构)。
AVL树的旋转
假设由于在二叉排序树上插入节点而失去平衡的最小子树根节点的指针为a(即a是离插入点最近,且平衡因子绝对值超过1的祖先节点),则失去平衡后进行的规律可归纳为下列四种情况:
- 单向右旋平衡处理LL:由于在a的左子树根节点的左子树上插入节点,a的平衡因子由1增至2,致使以a为根的子树失去平衡,则需进行一次右旋转操作。
- 单向左旋平衡处理RR:由于在a的右子树根节点的右子树上插入节点,a的平衡因子由-1变为-2,致使以a为根的子树失去平衡,则需进行一次左旋转操作。
- 双向旋转(先左后右)平衡处理LR:由于在a的左子树根节点的右子树上插入节点,a的平衡因子由1增至2,致使以a为根的子树失去平衡,则需进行两次旋转(先左旋后右旋)操作。
- 双向旋转(先右后左)平衡处理RL:由于在a的右子树根节点的左子树上插入节点,a的平衡因子由-1变为-2,致使以a为根的子树失去平衡,则需进行两次旋转(先右旋后左旋)操作。
AVL树的结构(这里开始讲代码)
首先,它的底层是搜索二叉树的基础上进行的,所以会像搜索二叉树
struct AVLTreeNode
{
AVLTreeNode(const T& data = T())
: _pLeft(nullptr)
, _pRight(nullptr)
, _pParent(nullptr)
, _data(data)
, _bf(0)
{}
AVLTreeNode<T>* _pLeft;
AVLTreeNode<T>* _pRight;
AVLTreeNode<T>* _pParent;
T _data;
int _bf; // 节点的平衡因子
};
// AVL: 二叉搜索树 + 平衡因子的限制
template<class T>
class AVLTree
{
typedef AVLTreeNode<T> Node;
public:
//功能...
//我会写删除的,就在本篇一起写了
private:
Node* _pRoot;
};
AVL树的插入
- 像搜索二叉树一样直接插入
- 新增节点后更新平衡因子
- 检测平衡因子,若不平衡开始旋转
- 根据平衡因子对不平衡子树开始向上旋转
- 进行左旋转、右旋转、左右旋转、右左旋转来平衡平衡因子
更新平衡因子
- 平衡因子的值=右子树高度-左子树高度
- 只有子树高度会影响父节点的平衡因子
- 插入节点会新增高度,会改变插入节点的父节的平衡因子
- 插入右节点,平衡因子++
- 插入左节点,平衡因子- -
- 根据父亲节点所在子树高度决定是否向上更新
- 父亲节点平衡因子归零 不再更新 因为说明父亲节点的左右子树都平衡了 祖宗不变
- 父亲节点变为1或-1 向上更新 父亲节点不平衡 从平衡变成不平衡 对于祖宗来说也会有不平衡
- 父亲节点变为2或者-2 此时需要旋转
- 当变为2/-2的时候,说明已经不符合平衡条件了(1 0 -1),说明子树的高度差已经是2了,就需要进行旋转来更新旋转因子
- (这里到旋转的时候再分类,分了四类)左旋,右旋,左右双旋,右左双旋
- 不断更新到平衡因子是 1 -1 的就可以时候停止
bool Insert(const T& data){
if(_pRoot == nullptr)
{
_pRoot = new Node(data);
return true;
}
Node* parent = nullptr;
Node* cur = _pRoot;
while (cur)
{
if (data > cur->_data)
{
parent = cur;
cur = cur->_pRight;
}
else if (data < cur->_data)
{
parent = cur;
cur = cur->_pLeft;
}
else//不支持相同
{
return false;
}
}
cur = new Node(data);
if (parent->_pLeft == cur)
parent->_pLeft = cur;
else
parent->_pRight = cur;
cur->_pParent = parent;
//这里就插进去了 然后更新平衡因子
while (parent)
{
if (parent->_pLeft == cur)
parent->_bf--;
else
parent->_bf++;
if (parent->_bf == 0)
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
//向上更新!
cur = parent;
parent = parent->_pParent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//旋转 分情况了
break;
}
else
{
assert(false);
}
}
return true;
}
测试代码
AVLTree<pair<int, int>> A;
A.Insert({ 8,1 });A.Insert({ 3,1 });A.Insert({ 10,1 });A.Insert({ 1,1 });A.Insert({ 6,1 });A.Insert({ 14,1 });
旋转
- 旋转是难点,无论是插入还是删除的时候
- 旋转要保证旋转完还是搜索树
- 旋转分四类
- 左单选/右单旋/左右双旋/右左双旋
右单旋
- 右单旋是因为“子树集”(可以从根开始)在左边的左边插入导致左倾
- 也就是parent的平衡因子是-1 parent的parent的平衡因子是-2
- 右旋的方法(这里有点绕口,我用这样指代)(这里的谁的爹指向确定不因过程改变)
- parent作为父亲 parent的parent作为祖宗
- 祖宗的左节点指向父亲的右节点
- 父亲的右节点指向祖宗
- 祖宗的父亲指向父亲
- 父亲的父亲指向之前祖宗的父亲
- 孩子也要换爹
- 祖宗的爹开始换节点
- 左单旋也是这样,不过相反左单旋就不提了
- 更新平衡因子 父亲和祖宗都是0
void RotateR(Node* pParent){
//我pParent是祖宗!
Node* subL = pParent->_pLeft;//找到父亲
Node* subLR = subL->_pRight;//找到右孩子
//祖宗的左节点指向父亲的右节点
pParent->_pLeft = subLR;//换孩子
if (subLR)
subLR->_pParent = pParent;//孩子不为空给孩子换个爹
Node* parentParent = pParent->_pParent;
subL->_pRight = pParent;//父亲的右节点指向祖宗
pParent->_pParent = subL;//祖宗换个爹
//给祖宗的爹换节点
if (parentParent == nullptr)//这里要考虑是否是根节点 换根
{
_pRoot = subL;
subL->_pParent = parentParent;//空爹也是爹
}
else
{
if (pParent == pParent->_pLeft)//原来左,还是左
parentParent->_pLeft = subL;
else
parentParent->_pRight = subL;
subL->_pParent = parentParent;//给爹换个爹
}
//左倾旋转完后更新平衡因子 都为0
pParent->_bf = subL->_bf = 0;
}
左单旋
- 与右边单旋完全相反
- parent的平衡因子是1,祖宗的平衡因子是2
- 指向要反过来
void RotateL(Node* pParent){
//我pParent是祖宗! X标记为更改处
Node* subR = pParent->_pRight;//找到父亲 X
Node* subRL = subR->_pLeft;//找到左边孩子 X
//
祖宗的右节点指向父亲的左节点
pParent->_pRight = subRL;//换孩子 X
if (subRL != nullptr)
subRL->_pParent = pParent;//孩子不为空给孩子换个爹
Node* parentParent = pParent->_pParent;
subR->_pLeft = pParent;//父亲的右节点指向祖宗X
pParent->_pParent = subR;//祖宗换个爹X
//给祖宗的爹换节点
if (parentParent == nullptr)//这里要考虑是否是根节点 换根
{
_pRoot = subR;
subR->_pParent = parentParent;//空爹也是爹X
}
else
{
if (pParent == parentParent->_pLeft)//原来左,还是左X
parentParent->_pLeft = subR;
else
parentParent->_pRight = subR;
subR->_pParent = parentParent;//给爹换个爹
}
//左倾旋转完后更新平衡因子 都为0
pParent->_bf = subR->_bf = 0;
}
左右双旋
左右双旋,就是先进行左旋再进行右旋
- 这里直接调用之前的左单旋 然后再调用 右单旋就行
- 情况:在左子树的右节点插入导致该子树左倾
- parent的平衡因子变成了1 但是祖宗的平衡因子为-2
- 需要先在parent进行一个左单旋,这个时候从(更新的)parent就变为-1了
- 然后在祖宗节点就符合了右单旋的条件,右单旋就平衡了
- 更新平衡因子(分三类)
- 当父亲节点的左右子树高度都大于等于1的时候
- 插入在父亲节点的右子树的左边
- 父亲节点更新为0 祖宗为0 祖宗的右节点为1
- 插入在父亲节点的右子树的右边
- 父亲节点更新为-1 祖宗为0 祖宗的右节点为1
- 当父亲节点没有孩子的时候
- 父亲 祖宗 祖宗的右孩子都为0
void RotateLR(Node* pParent){ //爷爷变右父亲了
Node* subL = pParent->_pLeft;//记住原来的父亲节点 位置不变还是父亲
Node* subLR = subL->_pRight;//记住原来的父亲节点的有右边的孩子节点 孙子变爷爷了
int bf = subLR->_bf;//这里用于分情况记录平衡因子
//先以父亲节点为祖宗左旋
RotateL(pParent->_pLeft);
//以祖宗节点右旋
RotateR(pParent);
//更新平衡因子
if (bf == 0)
{
subL->_bf = 0;
subLR->_bf = 0;
pParent->_bf = 0;
}
else if (bf == -1)
{
subL->_bf = 0;
subLR->_bf = 0;
pParent->_bf = 1;
}
else if (bf == 1)
{
subL->_bf = -1;
subLR->_bf = 0;
pParent->_bf = 0;
}
else
{
assert(false);
}
}
右左双旋
同左右双旋类似
- 当右父亲节点的左节点插入导致树不平衡
- 这个时候祖宗节点平衡因子为2 父亲平衡因子为-1
- 先让父亲节点右单旋,然后祖宗节点左单旋
- 更新平衡因子
- 与左右双旋相反
- 当子树都大于等于1的时候
- 插入左边
- 祖宗为0 左父亲0 右父亲1
- 插入右边
- 祖宗为0 左父亲-1 右父亲为0
- 当没有子树时候三个零
void RotateRL(Node* pParent){//爷爷变右父亲X
Node* subR = pParent->_pRight;//记住原来的父亲节点 位置不变还是父亲
Node* subRL = subR->_pLeft;//记住原来的父亲节点的有右边的孩子节点 孙子变爷爷了X
int bf = subRL->_bf;//这里用于分情况记录平衡因子
//先以父亲节点为祖宗右旋
RotateR(pParent->_pRight);
//以祖宗节点左旋
RotateL(pParent);
//更新平衡因子
if (bf == 0)
{
subR->_bf = 0;
subRL->_bf = 0;
pParent->_bf = 0;
}
else if (bf == 1)
{
subR->_bf = 0;
subRL->_bf = 0;
pParent->_bf = -1;
}
else if (bf == -1)
{
subR->_bf = 1;
subRL->_bf = 0;
pParent->_bf = 0;
}
else
{
assert(false);
}
}
AVL树的查找
就是搜索二叉树的查找
Node* Find(const T& data)
{
Node* cur = _pRoot;
while (cur)
{
if (cur->_data < data)
{
cur = cur->_pRight;
}
else if (cur->_data > data)
{
cur = cur->_pLeft;
}
else
{
return cur;
}
}
return nullptr;
}
AVL的平衡检测
利用高度差检测
bool _IsAVLTree(Node* pRoot)
{
if (pRoot == nullptr)
return true;
int LHigh = _Height(pRoot->_pLeft);
int RHigh = _Height(pRoot->_pRight);
int D = RHigh - LHigh;
if (D < -1 || D > 1)
{
cout << pRoot->_data.first <<"高度异常" << endl;
return false;
}
if (pRoot->_bf != D)
{
cout << pRoot->_data.first << "平衡因子异常" << endl;
}
return _IsAVLTree(pRoot->_pLeft) && _IsAVLTree(pRoot->_pRight);
}
size_t _Height(Node* pRoot){
if (pRoot == nullptr)
return 0;
int LHigh = _Height(pRoot->_pLeft);
int RHigh = _Height(pRoot->_pRight);
return LHigh > RHigh ? LHigh + 1 : RHigh + 1;
}
AVL树的删除
AVL树的删除和插入一样复杂,下篇目分开讲解了