上回说到,二叉搜索树的时间复杂度并不稳定,于是进化出了另外一种树,AVL树,也叫平衡树。
目录
1、概念
由于搜索二叉树对一个已经有序的数据进行插入的话,则会变成一个上一篇博文所画的树,变成一个类似单链表的树。它的查找效率就和单链表的时间复杂度是一样的,O(N)。于是有了一种新的二叉搜索树。
这种树引入了平衡因子的概念。每个结点都有一个平衡因子,这个平衡因子就是该结点右子树与左子树的高度之差。也就是说:
平衡因子 = 右子树高度 - 左子树高度。
这种树在插入的时候,会进行调整,保证每个结点的平衡因子的绝对值不超过1。也就是说平衡因子取值只有-1、0、1。
于是对于这种每个结点的平衡因子的绝对值都是1/0/-1的树就是AVL树。
这个树是由俄罗斯数学家G.M.Adelson-Velskii和E.M.Landis提出的。于是取首字母便是AVL。
2、定义
在上图中每个结点的平衡因子的绝对值都不超过1,故是一棵AVL树。在代码中,如下定义:
template <class T>
class BTnode
{
BTnode(const T& val = T())
:val_(val)
,left_(nullptr)
,right_(nullptr)
,parent_(nullptr)
,bf_(0)
{}
T val_;
BTnode<T>* left_;
BTnode<T>* right_;
BTnode<T>* parent_;//该节点的父结点
int bf_;//该节点的平衡因子
};
3、插入
AVL树在二叉搜索树的基础上增加了一个平衡因子,在定义中增加了一个指向父结点的指针。AVL树的插入其实就是在二叉搜索树的基础上增加了一些东西——对平衡因子的调整。
由于AVL树的每个结点的平衡因子的绝对值都是不超过1的,因此在插入结点后会有一个调整的过程。因此:
AVL树的插入 = 二叉搜索树的插图 + 调整
调整就是对平衡因子的调整。但是在调整过程中还有对树的结构进行变化,这个变化叫做旋转。一共有以下旋转:
- 左单旋
- 右单旋
- 左右双旋
- 右左双旋
为了便于理解,将一颗树抽象为:
因此对于旋转就可以如下区分:
3.1 左单旋
对于左单旋的情况就是,在插入结点后,平衡因子变成了parent->bf_ == 2,cur->bf_ == 1,可以看出是parent的结点的右孩子的右子树的高度增加了,因此可以简称为该节点右边的右边高。平衡因子的情况如上图所示,于是旋转之后的平衡因子就都变成了0,达成平衡。
左单旋就是,把父结点拉下来,自己顶上去。把自己的左子树放在父结点的右孩子上,父结点成为自己右孩子。代码如下:
void RotateLeft(pNode parent)
{
pNode subR = parent->right_;
pNode subRL = subR->left_;
subR->left_ = parent;
parent->right_ = subRL;
if(subRL)//如果左子树存在
subRL->parent_ = parent;
if (parent->parent_ == nullptr)//如果是根节点
{
subR->parent_ = nullptr;
root_ = subR;
}
else
{
subR->parent_ = parent->parent_;
if (parent->parent_->left_ == parent)
parent->parent_->left_ = subR;
else
parent->parent_->right_ = subR;
}
parent->parent_ = subR;
parent->bf_ = subR->bf_ = 0;
}
3.2右单旋
对于右单旋的情况可以对比左单旋,在插入结点后,平衡因子变成了parent->bf_ == -2,cur->bf_ == -1,就是是parent的结点的左孩子的左子树的高度增加了,因此可以简称为该节点左边的左边高。平衡因子的情况如上图所示,于是旋转之后的平衡因子就都变成了0,达成平衡。
与左单旋类似,父结点被拉下来,把cur的右孩子给父结点作为左孩子,父结点成为自己的右孩子。
void RotateRight(pNode parent)
{
pNode subL = parent->left_;
pNode subLR = subL->right_;
subL->right_ = parent;
parent->left_ = subLR;
if(subLR)
subLR->parent_ = parent;
if (parent->parent_ == nullptr)
{
subL->parent_ = nullptr;
root_ = subL;
}
else
{
subL->parent_ = parent->parent_;
if (parent->parent_->left_ == parent)
parent->parent_->left_ = subL;
else
parent->parent_->right_ = subL;
}
parent->parent_ = subL;
parent->bf_ = subL->bf_ = 0;
}
3.3左右双旋
左右双旋其实就是先左单旋再右单旋。至于为什么会有这样的情况,是由于上面两者都是插在同一边,要么是左边的左边,要么是右边的右边。其实也可能是左边的右边,也就是插在parent的左孩子的右结点子树上,当然可能是左子树也可能是右子树。
if(cur->bf_ == 1 && parent->bf_ == -2)//左右双旋
{
pNode subL = parent->left_;
pNode subLR = subL->right_;
int bf = subLR->bf_;
RotateLeft(subL);
RotateRight(parent);
if(bf == -1)//结点插入到左子树,左边高
{
parent->bf_ = 1;
subL->bf_ = 0;
}
else if(bf == 1)//结点插入到右子树,右边高
{
parent->bf_ = 0;
subL->bf_ = -1;
}
}
3.4右左双旋
与左右双旋相反,则是右左双旋。
if(cur->bf_ == -1 && parent->bf_ == 2)//右左双旋
{
pNode subR = parent->right_;
pNode subRL = subR->left_;
int bf = subRL->bf_;
RotateRight(subR);
RotateLeft(parent);
if(bf == 1)//这里就是结点在右子树
{
parent->bf_ = -1;
subR->bf_ = 0;
}
else if(bf == -1)//结点在左子树
{
parent->bf_ = 0;
subR->bf_ = 1;
}
}
彩蛋
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度是O(lg(N)) 。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如: 插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。 因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树, 但一个结构经常修改,就不太适合。
于是又进化了,红黑树诞生。