前面对map/multimap/set/multiset
进行了简单的介绍,在其文档介绍中发现,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N)
,因此map、set
等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
1. AVL树的概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii
和E.M.Landis
在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL
树或者是空树,或者是具有以下性质的二叉搜索树:
1.它的左右子树都是AVL
树
2.左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL
树。如果它有n个结点,其高度可保持在O(log2 n)
,搜索时间复杂度O(log2 n)
。
2. AVL树节点的定义
// 节点的类模板
template<class K, class V>
struct AVLTreeNode
{
// 键对值的对象 _kv
pair<K, V> _kv;
// 左子树的根节点
AVLTreeNode<K, V>* _left;
// 右子树的根节点
AVLTreeNode<K, V>* _right;
// 父节点
AVLTreeNode<K, V>* _parent;
int _bf; // balance factor:平衡因子
// 节点的构造函数
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
3.insert
bool Insert(const pair<K, V>& kv)
{
// 如果根节点_root,那么就创建新节点,并其赋值给_root
// 返回真,表示插入成功
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
// 将父亲节点初始化为空
Node* parent = nullptr;
// 将当前节点cur初始化为根节点
Node* cur = _root;
// 第一步:找到kv键值对要插入的位置
while (cur)
{
// 1.cur节点对应的key值小于将要插入的key值
if (cur->_kv.first < kv.first)
{
// 迭代
parent = cur;
// 向右子树走
cur = cur->_right;
}
// 2.cur节点对应的key值大于将要插入的key值
else if (cur->_kv.first > kv.first)
{
// 迭代
parent = cur;
// 向左走
cur = cur->_left;
}
else
{
// 3.如果相等,根据二叉搜索树的性质(不允许出现重复的key值)
return false;
}
}
// 第二步:创建新节点,使用要插入的kv来初始化这个新节点
// 1.parent是指向cur的,根据二叉搜索树的性质
cur = new Node(kv);
// 2.如果parent节点的key值小于要插入的key值,那么cur连接到parent的右子树
if (parent->_kv.first < kv.first)
{
// 父节点指向cur
parent->_right = cur;
// cur指向父节点
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
// 第三步:更新平衡因子
// 当parent为空,也就更新到了整棵树的根节点
while (parent)
{
// 3.1 因为新添加了一个节点,所以要更新其对应parent的平衡因子
// 平衡因子 = 右子树的高度 - 左子树的高度
// 新增在右,parent->bf++;
// 新增在左,parent->bf--;
if (cur == parent->_left)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
// 3.2 是否继续更新依据:子树的高度是否变化
// 如下图所示
// 情况1:更新后:parent->_bf == 0
// 说明插入节点之前 parent->_bf 是 1 或者 -1
// 说明之前parent一边高一边低,这次插入填上矮的那边,parent所在子树高度不变,不需要继续往上更新
// 情况2、
// 更新后:parent->_bf == 1 或 -1
// 说明插入节点之前是 parent->_bf == 0,两边一样高,现在插入一边更高了,parent所在子树高度变了,继续往上更新
// 情况3、
// 更新后:parent->_bf == 2 或 -2,
// 说明之前parent->_bf == 1 或者 -1,现在插入节点严重不平衡,违反规则,就地处理--旋转
// 旋转:
// 1、让这颗子树左右高度差不超过1
// 2、旋转过程中继续保持他是搜索树
// 3、更新调整孩子节点的平衡因子
// 4、让这颗子树的高度跟插入前保持一致
// 3.3 迭代更新
// 情况1:不需要继续往上更新,因此直接跳出循环就可以
if (parent->_bf == 0)
{
break;
}
// 情况2:继续往上更新(情况2向上更新可能会变为情况3,也可能迭代到parent为空,跳出循环)
else if (parent->_bf == 1 || parent->_bf == -1)
{
// 向cur的父亲节点进行迭代
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 情况3:此时必须进行旋转了旋转
// 3.1 当前节点cur的父节点的平衡因子为2,当前节点的平衡因子为1
if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
// 3.2
RotateR(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
}
else
{
assert(false);
}
break;
}
else
{
// 如果不满足上面的三种情况,那么就会直接报错
assert(false);
}
}
return true;
}
3.1 旋转
如果在一棵原本是平衡的AVL
树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL
树的旋转分为四种:
左单旋
思路:
-
val为30的这个节点的右子树的根节点的val值必然大于30(根据二叉搜索树的性质),因此将val值为60的节点的左子树根节点链接到30的右子树
-
将30的根节点链接到60的左子树
-
这样就完成了左旋
void RotateL(Node* parent)
{
// 第一步:
// subR、subRL、parent的关系如上图所示
// subR 是根节点的右子树的根节点
// subRL 是根节点subR的左子树的根节点
// parent 是整棵树的根节点
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 1.将subR的左子树根节点连接到parent节点的右子树(完成单向连接)
parent->_right = subRL;
// 2.如果subRL不为空,则它的父指针需要指向parent整棵树的根节点(完成双向连接)
if (subRL)
subRL->_parent = parent;
// 第二步:
// 提前保存整颗二叉树的根节点parent的父节点指针,保存为ppNode
Node* ppNode = parent->_parent;
// 将parent为根节点的树连接到subR的左子树(单向连接)
subR->_left = parent;
// 让parent的父节点的指针指向subR(完成双向连接)
parent->_parent = subR;
// 第三步:连接新树的根节点subR到ppNode
if (ppNode == nullptr)
{
// 如果ppNode为空,说明parent之前的parent->parent为空
// 此时经过旋转subR就是新树的根节点,所以根节点的父指针需要指向空
_root = subR;
_root->_parent = nullptr;
}
else
{
// 说明parent之前不是根节点,那么就需要将其与原本的parent的上一层节点链接起来
// 此时ppNode的左子树,或者右子树还指向parent
if (ppNode->_left == parent)
{
ppNode->_left = subR;
}
else
{
ppNode->_right = subR;
}
// subR的父节点也需要指向ppNode
subR->_parent = ppNode;
}
// 第四步:更新平衡因子
// 经过旋转之后,parent和subR的左右子树是平衡的,因此平衡因子为0
parent->_bf = subR->_bf = 0;
}
右单旋
// 思路与左单旋一致
void RotateR(Node* parent)
{
// 第一步:
// parent是整棵树的根节点
// subL是parent左子树的根节点
Node* subL = parent->_left;
// subLR 是subL节点右子树的根节点
Node* subLR = subL->_right;
// 第二步:
// 1.parent的left指针指向subLR(单向连接)
parent->_left = subLR;
// 2.subLR的父指针指向parent(完成双向连接)
if (subLR)
{
subLR->_parent = parent;
}
// 第二步:
// 1.保存parent的父指针为ppNode
Node* ppNode = parent->_parent;
// 2.subL是新树的根节点,subL的右指针,指向parent(单向连接)
subL->_right = parent;
// 3.parent的父指针指向subL(完成双向连接)
parent->_parent = subL;
// 第三步:将新树的根节点subL与ppNode完成双向连接
//if (_root == parent)
if (ppNode == nullptr)
{
// 如果ppNode为空,那么subL就是根节点,没有上层节点可以连接
_root = subL;
// 所以_root的父节点为空
_root->_parent = nullptr;
}
else
{
// 如果ppNode->_left指向parent节点,那么ppNode左子树的根节点为subL(单向连接)
if (ppNode->_left == parent)
{
ppNode->_left = subL;
}
else
{
ppNode->_right = subL;
}
// subL的父指针指向ppNode(完成双向连接)
subL->_parent = ppNode;
}
// 第四步:更新平衡因子
subL->_bf = parent->_bf = 0;
}
先左单旋,再右单旋
void RotateLR(Node* parent)
{
// parent->_left 指向的就是左单旋的根节点
Node* subL = parent->_left;
// subL->_right 就是新增的节点的子树的根节点
Node* subLR = subL->_right;
// 最终判断 subL subLR parent的平衡因子,
// 需要根据新增节点后的subLR->_bf(平衡因子)来判断,因此先保存,旋转后它(平衡因子)将会改变
int bf = subLR->_bf;
// 以parent->_left指向的节点为根节点进行左旋
RotateL(parent->_left);
// 以parent节点为根节点进行右旋
RotateR(parent);
if (bf == -1)
{
// 情况1(更新平衡因子,如上图所示,如果是b新增节点,那么旋转后为情况1)
// subLR左子树新增,经过左右双旋后的平衡因子
subL->_bf = 0;
parent->_bf = 1;
subLR->_bf = 0;
}
else if (bf == 1)
{
// 情况2(更新平衡因子,如上图所示,如果是c新增节点,那么旋转后为情况2)
// subLR右子树新增,经过左右双旋后的平衡因子
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == 0)
{
// 情况3(更新平衡因子,如上图所示,如果h为0,val值为60的节点是新增节点,那么旋转后为情况3)
// subLR节点本身就是新增节点,,经过左右双旋后的平衡因子
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
assert(false);
}
}
先右单旋,再左单旋
// 思路与先左单旋,再右单旋一致
void RotateRL(Node* parent)
{
// parent->_right 指向的就是右单旋的根节点
Node* subR = parent->_right;
// 新增节点所在子树的根节点为subR->_left
Node* subRL = subR->_left;
// 最终判断 subL subLR parent的平衡因子,
// 需要根据新增节点后的subLR->_bf(平衡因子)来判断,因此先保存,旋转后它(平衡因子)将会改变
int bf = subRL->_bf;
// parent->_right 指向的就是右单旋的根节点
RotateR(parent->_right);
// parent是左单旋的根节点
RotateL(parent);
if (bf == 1)
{
// 情况1:b树新增节点时
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
// 情况2:c树新增节点时
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 0)
{
// 情况3:h的高度为0时
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
4.中序遍历
void Inorder()
{
_Inorder(_root);
}
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_Inorder(root->_right);
}
5.AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
- 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
- 验证其为平衡树
- 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
- 节点的平衡因子是否计算正确
平衡的验证
bool IsBalance()
{
return IsBalance(_root);
}
int Height(Node* root)
{
if (root == nullptr)
return 0;
// 左子树的高度
int lh = Height(root->_left);
// 右子树的高度
int rh = Height(root->_right);
// 如果左子树的高度大于右子树的高度,那么左子树的高度+1,并返回,
// 反之右子树的高度+1,并返回
// 如果左右子树高度相等,那么返回0
return lh > rh ? lh + 1 : rh + 1;
}
// 验证平衡,也就是验证左右子树的高度差的绝对值要小于2
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;
return false;
}
// 需要判断整颗树,和子树的(左右子树的)高度差的绝对值都是小于2的,因此要递归到每一个子树
return abs(rightHeight - leftHeight) < 2
&& IsBalance(root->_left)
&& IsBalance(root->_right);
}
6.类模板
#pragma once
#include <assert.h>
#include <time.h>
// AVL树节点的类模板
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf; // balance factor
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
// AVL树的类模板
template<class K, class V>
struct AVLTree
{
// 将AVL树节点的类模板类型定义为Node
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->_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;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
// 1、更新平衡因子
while (parent) // parent为空,也就更新到根
{
// 新增在右,parent->bf++;
// 新增在左,parent->bf--;
if (cur == parent->_left)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
// 是否继续更新依据:子树的高度是否变化
// 1、parent->_bf == 0说明之前parent->_bf是 1 或者 -1
// 说明之前parent一边高一边低,这次插入填上矮的那边,parent所在子树高度不变,不需要继续往上更新
// 2、parent->_bf == 1 或 -1 说明之前是parent->_bf == 0,两边一样高,现在插入一边更高了,
// parent所在子树高度变了,继续往上更新
// 3、parent->_bf == 2 或 -2,说明之前parent->_bf == 1 或者 -1,现在插入严重不平衡,违反规则
// 就地处理--旋转
// 旋转:
// 1、让这颗子树左右高度不超过1
// 2、旋转过程中继续保持他是搜索树
// 3、更新调整孩子节点的平衡因子
// 4、让这颗子树的高度跟插入前保持一致
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)
{
RotateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
}
else
{
assert(false);
}
break;
}
else
{
assert(false);
}
}
return true;
}
// 左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
Node* ppNode = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (ppNode == nullptr)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = subR;
}
else
{
ppNode->_right = subR;
}
subR->_parent = ppNode;
}
parent->_bf = subR->_bf = 0;
}
// 右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
//if (_root == parent)
if (ppNode == nullptr)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = subL;
}
else
{
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
subL->_bf = parent->_bf = 0;
}
// 先左单旋,再右单旋
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == -1) // subLR左子树新增
{
subL->_bf = 0;
parent->_bf = 1;
subLR->_bf = 0;
}
else if (bf == 1) // subLR右子树新增
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == 0) // subLR自己就是新增
{
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;
RotateR(parent->_right);
RotateL(parent);
if (bf == 1)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 0)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
// 中序遍历
void Inorder()
{
_Inorder(_root);
}
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_Inorder(root->_right);
}
int Height(Node* root)
{
if (root == nullptr)
return 0;
int lh = Height(root->_left);
int rh = Height(root->_right);
return lh > rh ? lh + 1 : rh + 1;
}
// 判断是否平衡
bool IsBalance()
{
return IsBalance(_root);
}
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;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& IsBalance(root->_left)
&& IsBalance(root->_right);
}
private:
Node* _root = nullptr;
};
7.验证用例
- 常规场景1
{16, 3, 7, 11, 9, 26, 18, 14, 15}
- 特殊场景2
{4, 2, 6, 1, 3, 5, 15, 7, 16, 14}
void TestAVLTree1()
{
srand(time(0));
const size_t N = 100000;
AVLTree<int, int> t;
for (size_t i = 0; i < N; ++i)
{
size_t x = rand();
t.Insert(make_pair(x, x));
//cout << t.IsBalance() << endl;
}
//t.Inorder();
cout << t.IsBalance() << endl;
}
8.AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log_2 (N)
。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。