虽然二叉搜索树可以大大提升查找的效率,但如果数据本身有序或接近有序,二叉搜索树将退化为单支树,此时借助它查找元素就相当于在顺序表中搜索元素,效率反而大大降低(详情可见【数据结构】二叉搜索树)。
为了解决二叉搜索树退化为单支树影响效率的问题,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1,即可降低树的高度,从而减少平均的搜索长度,提高效率。
基于这种方法改进而来的二叉搜索树,被命名为AVL树(由两位数学家的名字而来)。
AVL树是二叉平衡搜索树的一种,因其结构本质为“通过子树的高度差来控制平衡”,也被称为高度二叉平衡搜索树。
本篇博客将通过对AVL树主要性质的梳理和主要功能的模拟实现,帮助读者更加全面地理解AVL树。
目录
一、AVL的性质
控制一个节点的左右子树高度之差,是使AVL树平衡的一种重要手段。为了方便描述左右子树高度之差,我们将其称为平衡因子。在一棵平衡的AVL树中,任意节点的平衡因子值应为-1、0和1。
平衡因子是判断一棵树高度平衡的重要条件。如果一棵二叉搜索树满足了高度平衡的条件,那么它就是AVL树。
对于任意一棵AVL树,它都具有如下性质:
- 任意树节点的左右子树都是AVL树;
- 任意树节点的平衡因子取值范围为[-1,1]。
如果它有n个结点,其高度可保持在O(logN),搜索查找的时间复杂度为O(lognN)。
二、AVL树的模拟实现
1 - 树的构建
//此处树节点被定义为三叉链结构,每个节点中都有平衡因子
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的构造,对树节点做初始化
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
{}
};
//AVL树
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
//...
private:
Node* _root = nullptr;
};
2 - 插入及旋转
AVL树可以看作是引入了平衡因子的二叉搜索树。那么,在二叉搜索树的基础上,AVL树的插入过程可以分为两步:
- 按照二叉搜索树的方式插入新节点:利用插入的值创建一个新的树节点。树为空,就直接将新节点赋给根节点的指针;树不为空,就按二叉搜索树的性质查找到合适的插入位置,在合适位置插入新节点。
- 控制树的平衡:利用旋转。
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;
}
//小,就做合适位置的左孩子
else
{
parent->_left = cur;
}
/
//控制树的平衡
//每插入一个节点,重置其_parent,为调整平衡铺垫
cur->_parent = parent;
//然后,控制平衡,维持AVL树的形态
//插入时,
//树中新增的节点会影响其祖先(可能是双亲的双亲,也可能是曾曾曾双亲等)的平衡因子
//插入在祖先的左子树中,以祖先为根的树的高度发生变化,祖先的平衡因子需-1;
//插入在祖先的右子树中,同理,平衡因子需+1。
//插入后,
//若祖先节点的平衡因子因插入而变为0,则意味着矮的(空缺的)那个子树位置被填上了
//且以祖先为根的树的高度并没有变化,此时无需再沿返回到整棵树的根的路径向上更新其他祖先,调整到此结束
//若平衡因子变为1或-1,则说明以祖先为根的树的高度发生变化,此时需要继续往上调整祖先的祖先的平衡因子
//若平衡因子变为2或-2,则说明以祖先为根的树的高度发生变化,且该树不平衡了,
//此时先对以祖先为根的树进行旋转处理,再调整平衡因子,整体平衡后,调整就结束了
//若倒霉到必须一直更新到根节点,调整也结束了
//例如:
//假设cur为待插入位置的节点,parent为cur的双亲节点
//cur插入后,parent的平衡因子一定需要调整。
//在插入之前,parent的平衡因子分为三种情况:-1,0, 1,;插入后,又分以下两种情况:
// 1. 若cur插入到了parent的左侧,只需给parent的平衡因子-1即可
// 2. 若cur插入到了parent的右侧,只需给parent的平衡因子+1即可
//经过以上操作,此时parent的平衡因子可能有三种情况:0,+1/-1, +2/-2
// 1. 如果parent的平衡因子为0,说明插入之前parent的平衡因子为+1/-1,插入后被调整成0,满足AVL树的平衡条件,无需再调整;
// 2. 如果parent的平衡因子为+1/-1,说明插入前parent的平衡因子为0,插入后被更新成+1/-1,以parent为根的树的高度增加,需要继续向上(向祖先和曾祖先等)更新;
// 3. 如果parent的平衡因子为+2/-2,则以parent为根的树不满足平衡条件,需要对其进行旋转处理,再调整平衡因子
while (parent)
{
//插入时,调整parent的平衡因子
if (cur == parent->_left)
{
parent->_bf--;
}
else //if (cur == prant->_right)
{
parent->_bf++;
}
//根据parent的平衡因子作进一步调整
if (parent->_bf == 0) //为0则无需调整
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1) //为+1/-1,则需继续向上调整
{
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2) //为+2/-2,则需做旋转处理
//ps:此处继续用else if,是因为无法保证出现_bf为3或-3等bug情况发生,所以用其来防bug影响
{
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); //双亲左边高,孩子右边高 -> 左右双旋
}
break;
}
else//走到这步,一定是出bug了
{
assert(false);
}
}
return true;
}
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构, 使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
假如以parent为根的子树不平衡,即parent的平衡因子为2或者-2,分以下情况考虑:
1、parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为pSubR
- 当pSubR的平衡因子为1时,执行左单旋
- 当pSubR的平衡因子为-1时,执行右左双旋
2、parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为pSubL
- 当pSubL的平衡因子为-1时,执行右单旋
- 当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新
2.1.左单旋
新节点插入较高右子树的右侧(右右) => 左单旋。
void RotateL(Node* parent)
{
Node* cur = parent->_right; //cur是parent的右孩子
Node* curleft = cur->_left; //curleft是cur的左孩子
//将parent及其左子树整体旋转下来,并将parent与cur、curleft与parent、cur与ppnode一一正确链接
parent->_right = curleft;
if (curleft)
{
curleft->_parent = parent; //curleft可能为空,若为空则无需将curleft->_parent与parent链接
}
cur->_left = parent;
//需要旋转的树,可能是一个局部的子树
//cur可能需要跟parent的双亲节点ppnode链接
Node* ppnode = parent->_parent;
parent->_parent = cur;
if (parent == _root /*ppnode==nullptr*/) //旋转点在根,cur无需跟parent的双亲节点链接
{
_root = cur;
cur->_parent = nullptr;
}
else //旋转点不在根,cur需要跟parent的双亲节点链接
{
if (ppnode->_left == parent) //parent是ppnode的左孩子,就让cur代替其成为左孩子
{
ppnode->_left = cur;
}
else //parent是ppnode的右孩子,就让cur代替其成为右孩子
{
ppnode->_right = cur;
}
cur->_parent = ppnode; //将cur的双亲节点置为ppnode
}
//最终,更改平衡因子
parent->_bf = cur->_bf = 0;
}
//使用情景:无数多种。一般插入节点的时候,使树不平衡则需要旋转
//h(子树的高度)==0 => 空树,无需旋转
//h==1 => 有一层子树,插入可能破坏平衡,此时需旋转
//h==2 => 插入前树的可能形状有3*3*1=9种;插入位置的可能有四种(左/右孩子的左/右边);h==2合计的组合情况有:9*4=36种
//......
//以此类推,使用情景有无数多种
2.2.右单旋
类似左单旋。
新节点插入较高左子树的左侧(左左) => 右单旋。
void RotateR(Node* parent)
{
Node* cur = parent->_left; //cur是parent的左孩子
Node* curright = cur->_right; //curright是cur的右孩子
//将parent及其右子树整体旋转下来,并将parent与cur、curleft与parent、cur与ppnode一一正确链接
//链接curright与parent
parent->_left = curright;
if (curright)
{
curright->_parent = parent;
}
//链接cur与parent、cur与parent的双亲ppnode
Node* ppnode = parent->_parent;
cur->_right = parent;
if (ppnode == nullptr)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
//调整平衡因子
parent->_bf = cur->_bf = 0;
}
//使用情景:无数多种。一般插入节点的时候,使树不平衡则需要旋转
//h(子树的高度)==0 => 空树,无需旋转
//h==1 => 有一层子树,插入可能破坏平衡,此时需旋转
//h==2 => 插入前树的可能形状有3*3*1=9种;插入位置的可能有四种(左/右孩子的左/右边);h==2合计的组合情况有:9*4=36种
//......
//以此类推,使用情景有无数多种
2.3.右左双旋
新节点插入较高右子树的左侧(右左) => 先右单旋再左单旋。
//右左双旋
//第一次旋转为预处理,第二次旋转为真正的平衡调整
void RotateRL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
int bf = curleft->_bf;
//先右单旋,再左单旋
RotateR(parent->_right);
RotateL(parent);
//然后,还需调整平衡因子
//...
}
void RotateRL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
int bf = curleft->_bf;
//先右单旋,再左单旋
RotateR(parent->_right);
RotateL(parent);
//调整平衡因子
if (bf == 0) //上图中值为60的节点左右两边没有插入
{
cur->_bf = 0;
curleft->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1) //插入在60的右边
{
cur->_bf = 0;
curleft->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1) //插入在60的左边
{
cur->_bf = 1;
curleft->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
2.4.左右双旋
类似于右左双旋。
新节点插入较高左子树的右侧(左右) => 先左单旋再右单旋。
void RotateLR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
int bf = curright->_bf;
//先左单旋再右单旋
RotateL(parent->_left);
RotateLR(parent);
//调整平衡因子
if (bf == 0) //上图中值为60的节点左右两边没有插入
{
cur->_bf = 0;
curright->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1) //插入在60的左边
{
cur->_bf = 0;
curright->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1) //插入在60的右边
{
cur->_bf = -1;
curright->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
3 - 完整代码
#pragma once
#include<iostream>
using namespace std;
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;
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_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->_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;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
while (parent)
{
if (cur == parent->_left)
{
parent->_bf--;
}
else //if (cur == prant->_right)
{
parent->_bf++;
}
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)
{
RotateRL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
break;
}
else
{
assert(false);
}
}
return true;
}
void RotateL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
parent->_right = curleft;
if (curleft)
{
curleft->_parent = parent;
}
cur->_left = parent;
Node* ppnode = parent->_parent;
parent->_parent = cur;
if (parent == _root)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
parent->_bf = cur->_bf = 0;
}
void RotateR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
parent->_left = curright;
if (curright)
{
curright->_parent = parent;
}
Node* ppnode = parent->_parent;
cur->_right = parent;
if (ppnode == nullptr)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
parent->_bf = cur->_bf = 0;
}
void RotateRL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
int bf = curleft->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0)
{
cur->_bf = 0;
curleft->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
cur->_bf = 0;
curleft->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
cur->_bf = 1;
curleft->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
void RotateLR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
int bf = curright->_bf;
RotateL(parent->_left);
RotateLR(parent);
if (bf == 0)
{
cur->_bf = 0;
curright->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
cur->_bf = 0;
curright->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
cur->_bf = -1;
curright->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
int Height(Node* root)
{
if (root == nullptr)
return 0;
int leftHight = Height(root->_left);
int rightHeight = Height(root->_right);
return leftHight > rightHeight ? leftHight + 1 : rightHeight + 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 << "_bf error" << root->_kv.first<<"->"<<root->_bf << endl;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& IsBalance(root->_left)
&& IsBalance(root->_right);
}
/ / /
//删除(了解即可)
//step1.按搜索树的规则查找节点删除
//step2.更新平衡因子
//step3.出现异常需旋转
private:
Node* _root = nullptr;
};
补、一些迷思
1. AVL树的平衡为什么不是高度相等?
保证平衡因子为0(即左右子树高度相等),在节点少的时候容易做到,但节点多的时候很难做到相等。故退而求其次,将“平衡”定义为高度差的绝对值不超过1(即平衡因子为1或-1)。
2. 增删查改的时间复杂度:高度次 - O(logN)
解释:
满二叉树的节点个数:2^h-1 = N(最后一层全满不缺节点)
AVL树的节点个数:2^h-x = N(最后一层缺x-1个节点)
=> x的范围:[1,2^(h-1)-1]( [最后一层不缺节点,最后一层只剩一个节点] )2^h-1 = N => 2^(h-1) = (N+1)/2
2^h-x = N =>x最大时 2^h-N/2 = N =>2^h =N*3/2 =>约等于logN
3. AVL树的性能问题
AVL树是一棵绝对平衡的二叉搜索树,其每个节点的左右子树高度差绝对值都不超过1,这样可以保证查找时高效的时间复杂度(即O(logN))。
但是,当涉及一些结构上的修改操作,AVL树的性能却十分低下。
例如,在AVL树中插入数据时,要维护其绝对平衡,旋转调整不可避免,旋转的次数较多。更糟糕的是,在删除时, 有可能需要让旋转操作一直持续到根的位置才结束。
因此,如果需要一种查找高效且有序的数据结构,且数据的上限是静态的(即数据个数不会改变),则AVL树是一个优选;但如果数据经常涉及修改,则不推荐选用AVL树。
4.数据的查找方案一般有哪些?
- 暴力搜索(效率较低)
- 二分搜索(问题:原序列需有序,且序列涉及插入删除时非常麻烦,维护成本很高)
- 二叉搜索树(问题:极端场景下会退化为类似链表的结构)
- 二叉平衡搜索树(为了解决上面的问题,应运而生。但不能做到完全平衡)
- 多叉平衡搜索树(能做到完全平衡,属于B树系列)
- 哈希表