文章目录
前言
二叉搜索树的插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的效率。
但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度为O(N),因此 map、set 等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡二叉搜索树来实现。
最优情况下,有 n 个结点的二叉搜索树为完全二叉树,查找效率为:O(log2N)
最差情况下,有 n 个结点的二叉搜索树退化为单支树,查找效率为:O(N)
一、AVL树
1.1 AVL树的概念
平衡二叉搜索树(Self-balancing binary search tree),又称AVL树
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(超过1需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树,要么是空树,要么是具有以下性质的二叉搜索树:
-
每个节点的左右子树高度之差(简称平衡因子 Balance Factor)的绝对值不超过 1 (-1/0/1)
平衡因子 = 右子树的高度 - 左子树的高度:用来判断是否需要进行平衡操作
-
每一个子树都是平衡二叉搜索树
![20220315094033727](https://i-blog.csdnimg.cn/blog_migrate/2b2e97f49dc7b6b1303a97dad332c854.png)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。
有n个结点的AVL树,高度可保持在log2n,其搜索时间复杂度O(log2n)。
思考:为什么左右子树高度差不规定成0呢?
因为在2、4等节点数的情况下,不可能做到左右高度相等
1.2 AVL树节点的定义
AVL树节点是一个三叉链结构,除了指向左右孩子的指针,还有一个指向其父亲的指针,数据域是键值对,即pair对象,还引入了平衡因子,用来判断是否需要进行平衡操作。
// AVL树节点的定义(KV模型)
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv; // 键值对
int _bf; // 平衡因子(balance factor) = 右子树高度 - 左子树高度
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent; // 双亲指针
// 构造函数
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
,_bf(0)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
{
}
};
// AVL树的定义(KV模型)
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
private:
Node* _root;
public:
// 成员函数
}
1.3 AVL树 - 插入节点
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为3步:
- 插入新节点
- 更新树的平衡因子
- 根据更新后树的平衡因子的情况,来控制树的平衡(旋转操作)
1.3.1 插入新节点
和二叉搜索树插入方式一样,先查找,再插入。
// 插入节点
bool AVLTree::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) // 插入节点键值k大于当前节点
{
parent = cur;
cur = cur->_right;
}
else if(kv.first < cur->_kv.first) // 插入节点键值k小于当前节点
{
parent = cur;
cur = cur->_left;
}
else // 插入节点键值k等于当前节点
{
return false;
}
}
// while循环结束,说明找到适合插入节点的空位置了
// 插入新节点
cur = new Node(kv); // 申请新节点
// 判断当前节点是父亲的左孩子还是右孩子
if (cur->_kv.first > parent->_kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
//........................