数据结构进阶(C++) -- AVL树的实现
AVL树
概念
1962年,前苏联的科学家G.M.Adelson-Velsky和E.M.Landis 在论文《An algorithm for the organization of information》中发表了AVL树。AVL树是最先发明的自平衡二叉搜索树,AVL树可以是一棵空树,也可以是具备下列性质的二叉搜索树:1、它的左右子树都是AVL树。2、左右子树的高度差的绝对值不超过1。由此可见,AVL树是一棵高度平衡的二叉搜索树,通过控制高度差来控制平衡。
AVL树的实现中引入了一个平衡因子(balance factor)的概念,每个节点都有一个平衡因子,任何节点的平衡因子都等于该节点的右子树的高度减去左子树的高度,也就是说任何节点的平衡因子只有 0 / 1 / -1 三个取值,AVL树并不是必须要求使用平衡因子,但是有了平衡因子我们可以更方便的去观察和控制AVL树的平衡。
这里要求平衡因子的绝对值不超过1,是因为有些情况下我们无法使高度差为0,比如一棵树只有 2 个节点时,根节点左右子树的高度差只能为 1 / -1 。AVL树整体节点数量和分布与完全二叉树类似,高度控制在logN,因此增删查改的效率也控制在了logN,相比于二叉搜索树有了本质上的提升。
AVL树的实现
结构框架
#include<iostream>
#include<assert.h>
using namespace std;
template<class K, class V>
class AVLTreeNode
{
public:
pair<K, V> _kv;
AVLTreeNode<K, V>* _parent;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
int _bf;
AVLTreeNode(const pair<K,V>& kv)
:_kv(kv)
,_parent(nullptr)
,_left(nullptr)
,_right(nullptr)
,_bf(0)
{}
};
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
private:
Node* _root = nullptr;
};
- 我们首先定义了一个泛型的AVL树节点类(AVLTreeNode)和一个AVL树类(AVLTree)的基本框架,主要是用来构建一个支持自平衡功能的二叉树搜索树,依次结构与二叉搜索树相似。
- AVLTreeNode 节点类的目的是作为AVL树的一个节点,存储键值对以及树结构的相关信息。其中成员变量有 _kv(存储键值对 pair<K, V>,用于二叉搜索树的查找和排序)、_parent(指向父节点的指针)、_left 和 _right(分别指向左右子节点的指针)、_bf(节点的平衡因子,表示左右子树的高度差)。构造函数(初始化成员变量)。
- AVLTree 树类目的是管理整个AVL树的结构,提供对节点值的插入、删除、平衡调整等操作。其中成员变量只有一个 _root(指向AVL树的根节点,缺省值为nullptr)。
AVL树的插入
这里的插入和二叉搜索树的思路一致,但因为引入了 _bf 平衡因子和 _parent 父节点的指针,所以会更加复杂。
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 (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else // kv.first == cur->_kv.first
{
return false;
}
}
//插入
cur = new Node(kv);
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
return true;
}
插入过程
- 插入一个值按二叉搜索树规则进行插入。
- 新增节点以后,只会影响祖先节点的高度,也就是可能会影响部分祖先节点的平衡因子,所以更新从新增节点到根节点这一路径上的所有平衡因子,实际中最坏情况就是更新到根节点,有些情况下更新到中间就会停止,下面会详细分析。
- 更新平衡因子过程没有出现问题就插入结束。
- 更新平衡因子过程出现不平衡,对不平衡子树旋转,旋转后再调平衡的同时,降低了子树的高度,不会再影响上一层,插入结束。
平衡因子
- 平衡因子 = 右子树高度 - 左子树高度
- 只有子树高度变化才会影响当前节点的平衡因子。
- 插入节点会增加高度,新增节点在 parent 的右子树,则 parent 的平衡因子++,新增节点在 parent 的左子树,则 parent 的平衡因子--。
- parent 所在子树的高度是否变化决定了是否会继续往上更新。
平衡因子更新结束条件
- 更新后的 parent 的平衡因子等于 0 ,即更新中 parent 的平衡因子变化为 -1 -> 0 或 1 -> 0,说明更新前 parent 子树一边高一边低,新增的节点插入在低的那一边,插入后 parent 所在的子树高度不变,不会影响父亲节点的平衡因子,更新结束。
- 更新后的 parent 的平衡因子等于 1 或 -1,即更新中 parent 的平衡因子变化为 0 -> 1 或 0 -> -1,说明更新前 parent 子树两边一样高,新增插入节点后,parent 所在的子树一辩稿一边低,parent 所在的子树符合平衡要求,但是高度增加了1,会影响 parent 的父节点的平衡因子,所以要继续向上更新。
- 更新后 parent 的平衡因子等于 2 或 -2,即更新中 parent 的平衡因子变化为 1 -> 2 或 -1 -> -2 ,说明更新前 parent 子树一边高一边低,新增插入节点在高的那一边,parent 所在的子树高度差大于1,破坏了平衡,需要旋转处理,旋转的目的有两个:1、把 parent 子树旋转平衡。2、降低 parent 子树的高度,恢复到插入节点之前的高度。所以旋转后不需要继续向上更新,插入结束。
- 如果全程没有遇到更新结束条件,更新到根节点后,根的平衡因子是 0 / 1 / -1 都会停止,是 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走到空之后,插入新节点到parent的子节点
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
//连接节点的父节点
cur->_parent = parent;
//更新平衡因子(balance factor)
while (parent)
{
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
if (parent->_bf == 0)
{
//左右子树高度差为0,更新结束
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
//继续向上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//旋转处理
break;
}
else
{
assert(false);
}
}
return true;
}
旋转
原则
旋转的规则有两点:1、保持搜索树的规则。2、让旋转的AVL树从不平衡变平衡,其次是降低了旋转子树的高度。旋转总共分为四种:左单旋、右单旋、左右双旋和右左双旋。
右单旋
使用右单旋的场景为插入节点后平衡因子为 subL->_bf == -1 和 parent->_bf == -2 的情况。我将右单旋的使用场景分为了两种,下图1展示的是10为根的树,插入结点参与旋转。下图2展示的是插入节点不参与旋转。
核心旋转步骤:因为 5 < b子树的值 < 10,将b变为10的左子树,10变为5的右子树,5编程这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵树的高度恢复到了插入之前的高度,符合旋转原则。如果插入之前10是整棵树的一棵子树,那么旋转后不会影响上一层,只需要将旋转后的子树根节点连接到原父节点上即可,插入结束。
情况1:插入节点在后,插入结点参与旋转。
情况2:插入节点后,插入节点不参与旋转。
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
//父节点和子节点的右子树形成子节点的新右子树
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
// 判断父节点是否为子树
Node* pParent = parent->_parent;
if (pParent == nullptr)
{
_root = subL;
}
else
{
if (pParent->_left == parent)
pParent->_left = subL;
else
pParent->_right = subL;
}
subL->_parent = pParent;
//子节点的右子树为原父节点
subL->_right = parent;
parent->_parent = subL;
//更新平衡因子
subL->_bf = parent->_bf = 0;
}
左单旋
使用左单旋的场景为插入节点后平衡因子为 subR->_bf == 1 和 parent->_bf == 2 的情况。本质上与右单旋相同,只是旋转方向相反,下图为两情况的抽象版本。
旋转核心步骤,因为10 < b子树的值 < 15,将b变成10的右子树,10变成15的左子树,15变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的高度,符合旋转原则。如果插入之前10整棵树的⼀个局部子树,旋转后不会再影响上⼀层,只需要将旋转后的子树根节点连接到原父节点上即可,插入结束。
//左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
Node* pParent = parent->_parent;
if (pParent == nullptr)
{
_root = subR;
}
else
{
if (pParent->_left == parent)
{
pParent->_left = subR;
}
else
{
pParent->_right = subR;
}
}
subR->_parent = pParent;
subR->_left = parent;
parent->_parent = subR;
subR->_bf = parent->_bf = 0;
}
左右双旋
通过下图可以看到,左边高时,如果插入位置不是在a子树,而是插入在b子树,b子树高度从h变成h+1,引发旋转,右单旋无法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的纯粹的左边高,但是插入在b子树中,10为跟的子树不再是单纯的左边高,对于10是左边高,但是对于5是右边高,需要用两次旋转才能解决,以5为旋转点进行⼀个左单旋,以10为旋转点进行⼀个右单旋,这棵树这棵树就平衡了。
- 上图分别为左右双旋中h==0和h==1具体场景分析,下面我们将a/b/子树抽象为高度h的AVL子树进行分析,另外我们需要把b子树的细节进一步展开为8和左子树高度为h-1的e和f子树,因为我们要对b的父亲5为旋转点进行左单旋,左单旋需要动b树中的左子树。b子树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察8的平衡因子不同,这里我们要分三个场景讨论。
- 场景1:h>=1时,新增结点插入在e子树,e子树高度从h-1并为h并不断更新8->5->10平衡因子,引发旋转,其中8的平衡因子为-1,旋转后8和5平衡因子为0,10平衡因子为1。
- 场景2:h>=1时,新增结点插入在f子树,f子树高度从h-1变为h并不断更新8->5->10平衡因子,引发旋转,其中8的平衡因子为1,旋转后8和10平衡因子为0,5平衡因子为-1。
- 场景3:h== 0时,a/b/c都是空树,b自己就是一个新增结点,不断更新5->10平衡因子,引发旋
转,其中8的平衡因子为0,旋转后8和10和5平衡因子均为0。
//左右双旋
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else 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
assert(false);
}
右左双旋
- 跟左右双旋类似,下面我们将a/b/c子树抽象为高度h的AVL子树进行分析,另外我们需要把b子树细节进一步展开为12和左子树高度为h-1的e和f子树,因为我们要对b的父亲15为旋转点进行右单旋,右单旋需要动b树中的右子树。b子树中新增结点的位置不同,平衡因子更新的细节也不同,过观察12的平衡因子不同,这里我们要分三个场景讨论。
- 场景1: h >= 1时,新增结点插入在e子树,e 子树高度从h-1变为h并不断更新12->15->10平衡因子,引发旋转,其中12的平衡因子为-1,旋转后10和12平衡因子为0,15平衡因子为1。
- 场景2: h >= 1时,新增结点插入在f子树,f 子树高度从h-1变为h并不断更新12->15->10平衡因子引发旋转,其中12的平衡因子为1,旋转后15和12平衡因子为0,10平衡因子为-1。
- 场景3: h == 0时,a/b/c都是空树,b 自己就是一个新增结点,不断更新15->10平衡因子,引发旋转,其中12的平衡因子为0,旋转后10和12和15平衡因子均为0。
//右左双旋
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else 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
assert(false);
}
查找
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < key)
{
cur = cur->_right;
}
else if (cur->_kv.first > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
AVL树平衡检测
// 求树的高度
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;
}
bool _IsBalanceTree(Node* root)
{
//空树也是AVL树
if (root == nullptr)
{
return true;
}
//计算左右子树高度差节点的平衡因子
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
int diff = rightHeight - leftHeight;
//计算出的平衡因子和左右子树高度差节点的平衡因子不相等。
// 或者左右子树高度差节点的平衡因子绝对值超过1,则一定不是AVL树
if (abs(diff) >= 2)
{
cout << root->_kv.first << "高度差异常" << endl;
return false;
}
if (root->_bf != diff)
{
cout << root->_kv.first << "平衡因子异常" << endl;
}
return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
测试用例:
#include"AVLTree1.h"
void TestAVLTree1()
{
AVLTree<int, int> t;
// 常规的测试⽤例
//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
// 特殊的带有双旋场景的测试⽤例
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto& e : a)
{
t.Insert({ e, e });
}
t.Inorder();
//cout << t.IsBalanceTree() << endl;
}
int main()
{
TestAVLTree1();
return 0;
}
AVL树的删除
AVL树的删除本章节不做讲解,有兴趣的同学可参考:《殷人昆 数据结构:用面向对象方法与C++语言描述》中讲解。