github 地址:https://github.com/duchenlong/Cpp/tree/master/avl_tree/avl_tree
什么是AVL树
对于为什么会有AVL树这个概念,名字来源于他的发明者G. M. Adelson-Velsky和E. M. Landis
两位大佬。
AVL树是最先发明的平衡二叉查找树,在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树
。
平衡二叉树的搜索效率确实也挺高的,但是由于他的根节点是由第一次插入的元素的决定的,一旦根节点确定,那么以后这个节点就是整棵树的根了,是不会变化的,这也就出现了一些意外情况:
- 当我们按照升序进行插入的时候,整个树就会变成一个只有右子树的结构
- 另一种就是我们以降序进行插入的时候,又会出现只有左子树的情况
对于这样的情况,当我们进行查找的时候,其实总的效率还是O(n)
的,但是由于树中的节点都是不一定连续的地址空间,然后用指针在逻辑上连接起来的,所以当时间复杂度为O(n)
的时候,可能还不如直接在数组中遍历找数据。
而AVL树的出现,很好的解决了这样的情况。既然树中节点的地址都是不连续的空间,那么就可以通过移动指针变量,来对树的节点进行旋转,从而让整棵树趋于平衡的状态,让查找的效率真正达到O(log n)
AVL树的概念
AVL 树相比于平衡二叉树,除了多了一个旋转的操作,还在成员变量中多了一个平衡因子的概念。
平衡因子
当树的一个节点是根节点的时候,他的平衡因子 = 右子树的最大深度 - 左子树的最大深度
而一颗AVL树,他一定需要具备以下的几个特征:
- 他的左右子树都是AVL树
- 左右子树的高度之差的绝对值不超过1
(1,0,-1)
,也就是每一个根节点的平衡因子都是(1,0,-1)
中的一个。
那么,当一颗树是AVL树的时候,就可以保证他是高度平衡的。
如果它有n个结点,其高度可保持在O(log n)
,搜索时间复杂度也为O(log n)
AVL树节点的定义
在AVL树的节点的结构中,为了调节平衡因子,所有就需要利用孩子节点找到父亲节点,所以说为了方便,就增加了一个父亲节点的指针,用来指向他的父亲节点。
而树中节点所保存的数据类型是一个pair
的结构体,他是C++中的一个只有两个元素的结构体模板,可以通过first,second
两个指针来访问两个结构体的元素。
template<class Key,class Val>
struct tree_node
{
typedef tree_node<Key, Val> node;
typedef pair<Key, Val> data_type;
node* _parent; //双亲节点
node* _left; //左孩子节点
node* _right; //右孩子节点
data_type _data;//节点的值
int _bf; //平衡因子
tree_node(const data_type& data)
:_data(data), _bf(0)
{
_parent = _left = _right = nullptr;
}
};
对于整个AVL树的结构,可以这样来
template<class key, class val>
class avl_tree
{
public:
typedef key key_type;
typedef val val_type;
typedef tree_node<key_type,val_type> node;
avl_tree()
:root(nullptr)
{}
private:
node* root;//根节点
};
AVL数的插入
首先,我们需要明确一点,就是AVL树在插入之前,他一定是一颗平衡的二叉树。只是在插入这个新的节点之后,才可能会变得不平衡,所以说平衡因子的取值只会是(-2,-1,0,1,2)
。所以说,只有出现(-2,2)
中的任何一个数,就说明以该节点为根节点的子树不平衡,需要调整。
对于前两步而言,程序部分都是和平衡二叉树一样的,只是多了一个更新父亲节点的操作
//如果树中没有节点
if (root == nullptr)
{
root = new node(data);
return true;
}
node* parent = nullptr;
node* cur = root;
//找到要插入的位置
while (cur)
{
if (cur->_data.first == data.first) return false;
parent = cur;
cur = (cur->_data.first < data.first) ? cur->_right : cur->_left;
}
cur = new node(data);
//插入 cur 到指定位置
if (parent->_data.first < data.first)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
接下来就是判断是否需要旋转,也就是插入节点后,这棵树是否平衡?
这个时候,我们维护了两个指针变量,一个是当前新插入的节点(cur),另一个是该节点的父亲节点(parent)。所以说,我们可以循环的取更新平衡因子,直到父亲节点(parent)是空的时候,就说明更新完毕了
//更新平衡因子
while (parent)
{
}
更新平衡因子 并 判断是否旋转
如何更新平衡因子?
对于插入一个节点,非三种情况:
- 新插入的节点就是AVL树 的跟节点,这个时候只需要更新跟节点就可以了
- 新插入的节点在右子树中,根节点的平衡因子 + 1
- 新插入的节点在左子树中,根节点的平衡因子 - 1
//新插入的节点在右子树
if (cur == parent->_right)
{
parent->_bf++;
}
else if (cur == parent->_left)//新插入的节点在左子树
{
parent->_bf--;
}
在更新平衡因子的时候,我们选择从下向上更新,而不是在找插入节点位置时候同时更新平衡因子。
我们需要考虑这样一种情况,对于某一个根节点,我们插入新的节点后,并没有使得这个根节点的左右子树的高度发生变化。那么我们是不需要更新根节点(以及根节点再向上的父亲节点)的平衡因子的
所以说,对于更新完毕的平衡因子,我们做以下的处理
- 当更新后,父亲节点的平衡因子为
0
。说明对于父亲节点向上的节点来说,他们的高度没有发生变化,更新完毕了 - 父亲节点的平衡因子为
(1,-1)
。说明这个父亲节点的高度发生了变化,会引起上面节点的高度发生变化,但是没有失去平衡,所以需要向上迭代 - 父亲节点的平衡因子为
(-2,2)
。说明当前的父亲节点已经不平衡了,我们需要进行旋转操作。
if (parent->_bf == 0)
{
break;
}
else if (abs(parent->_bf) == 1)
{
cur = parent;
parent = parent->_parent;
}
else if (abs(parent->_bf) == 2)
{
//此时parent的子树已经不平衡,需要调整
}
旋转
对于一颗AVL树,当需要进行旋转的时候,他大体上一定是下面这四种类型:
- 当父亲节点的平衡因子为
-2
的时候,说明左子树高度比较高
第二种情况经过化简,其实就是这个模式
- 当父亲节点的平衡因子为
2
的时候,说明右子树的高度比较高
同样的,第三种情况化简后可以是这样
那么对于第一种和第四种情况,他们都是只有一条单边,那么我们只需要旋转一次就可以了,此为单旋
单旋
对於单旋而言,又根据旋转方向的不同,分为左单旋和右单旋两种
- 左单旋,即为向左进行旋转
程序的实现
//右,右,左旋
void Right_Right(node* parent)
{
//保留父亲的父亲节点
node* pParent = parent->_parent;
//父亲的右节点
node* pRight = parent->_right;
//右节点的左节点
node* pRLeft = pRight->_left;
//先把 pRLeft 和 parent 节点连接起来
parent->_right = pRLeft;
//更新 pRLeft 的父亲节点为 parent
if (pRLeft) pRLeft->_parent = parent;
//开始左旋
pRight->_left = parent;
parent->_parent = pRight;
//pRight 此时作为新的父亲节点,需要判断是否是新的根节点
//并连接 pRight 和 pParent
if (pParent == nullptr)
root = pRight;
else
(pParent->_left == parent) ? pParent->_left = pRight : pParent->_right = pRight;
pRight->_parent = pParent;
pRight->_bf = parent->_bf = 0;
}
- 右单旋,即为向右进行旋转。
程序实现
//左,左,右旋
void Left_Left(node* parent)
{
// 保留父亲的父亲节点
node* pParent = parent->_parent;
//父亲的左节点
node* pLeft = parent->_left;
//左节点的右节点
node* pLRight = pLeft->_right;
//先把 pLRight 和 parent 节点连接起来
parent->_left = pLRight;
//更新 pLRight 的父亲节点为 parent
if (pLRight) pLRight->_parent = parent;
//开始右旋
pLeft->_right = parent;
parent->_parent = pLeft;
//pLeft 此时作为新的父亲节点,需要判断是否是新的根节点
//并连接 pLeft 和 pParent
if (pParent == nullptr)
root = pLeft;
else
(parent == pParent->_left) ? pParent->_left = pLeft : pParent->_right = pLeft;
pLeft->_parent = pParent;
pLeft->_bf = parent->_bf = 0;
}
最后,因为如果只进行单旋,那么旋转后的图形的根节点的平衡因子一定是0,并且原本不平衡的那个节点由于变成了一个叶子节点,所以说平衡因子也为0。
而最底下的那个节点,因为在单旋的过程中,高度没有发生变化,所以说他的平衡因子不变。
双旋
对于双旋的情况,我们重新用程序实现也是挺麻烦的,肯定还不简洁。所以可以尝试着把双旋变成单旋来试试。
- 我们上面所示的第二种情况,当失衡节点的平衡因子是-2时
程序实现
//左,右,双旋
void Left_Right(node* parent)
{
node* pLeft = parent->_left;
node* pLRight = pLeft->_right;
int pLRbf = pLRight->_bf;
//右,右,单旋
Right_Right(pLeft);
//左,左,单旋
Left_Left(parent);
//更新平衡因子
}
对于双旋而言,我们需要再次更新平衡因子,因为我们第一次单旋的分支并不一定是一个模板的分支,他可能只是单旋分支的前两部分
其实这里又有三种平衡因子变化的情况,就是当根节点的左节点的右节点,的平衡因子是 (0,-1,1)这三种情况的时候,对于双旋后的平衡因子都有不同的结果
- 当该节点的平衡因子为0时:
2. 当该点的平衡因子为-1时:
- 当该点的平衡因子为1时
所以说,在修改平衡因子的时候,可以判断一下这个节点的情况,在进行赋值就可以了
if (pLRbf == 1)
{
parent->_bf = 0;
pLeft->_bf = -1;
pLRight->_bf = 0;
}
else if (pLRbf == -1)
{
parent->_bf = 1;
pLeft->_bf = 0;
pLRight->_bf = 0;
}
else if (pLRbf == 0)
{
parent->_bf = 0;
pLeft->_bf = 0;
pLRight->_bf = 0;
}
这里需要注意的是,我们在修改平衡因子的过程是在旋转完毕后的,那么旋转后这个点的平衡因子是会发生变化的,所以说我们需要在进行旋转之前就保留这个点的平衡因子的值,然后用这个变量在最后去修改。(亲测一下午)
为什么说这个点的取值一定是(0,-1,1)?
因为我们这是一颗AVL树,他在插入一个元素之前一定是一颗平衡的树,我们进行双旋的时候,这个点一定是一个平衡的节点,所以他的取值是(0,-1,1)
- 当失衡节点的平衡因子是2的时候,也就是第三种情况
那么,他的旋转过程我们可以再进行一次封装,并且他调节平衡因子的过程和上面是差不多的,也是那三种情况。
void Right_Left(node* parent)
{
node* pRight = parent->_right;
node* pRLeft = pRight->_left;
int pRLbf = pRLeft->_bf;
//左,左,单旋
Left_Left(pRight);
//右,右,单旋
Right_Right(parent);
if (pRLbf == 1)
{
parent->_bf = -1;
pRight->_bf = 0;
pRLeft->_bf = 0;
}
else if (pRLbf == -1)
{
parent->_bf = 0;
pRight->_bf = 1;
pRLeft->_bf = 0;
}
else if (pRLbf == 0)
{
parent->_bf = 0;
pRight->_bf = 0;
pRLeft->_bf = 0;
}
}
AVL树的验证
对于验证,我们只需要对数的结构进行遍历(前中后序都可以),然后依次判断每个节点的左右孩子节点的深度可以了。
另外,和插入操作相同,我们在遍历的时候,需要传入一个参数,也就是当前节点的指针。而这个树的节点是被封装好的私有类型,所以我们需要在类内部调用另一个接口。
public:
//判断是否是平衡二叉树
bool IsBF()
{
return _IsBF(root);
}
private:
//判断是否是平衡
bool _IsBF(node* root)
{
if (root == nullptr) return true;
int leH = _Height(root->_left);//左子树高度
int riH = _Height(root->_right);//右子树高度
if (riH - leH != root->_bf)
{
cout << root->_data.first << " 平衡因子异常" << endl;
return false;
}
return _IsBF(root->_left) && _IsBF(root->_right);
}
//由根节点确定子树高度
int _Height(node* root)
{
if (root == nullptr)
return 0;
return 1 + max(_Height(root->_left), _Height(root->_right));
}
最后在测试的时候,我使用了大量的随机数来进行测试,程序一切正常