目录
一、AVL树结点
二、AVL树结构
三、插入数据(重点)
1、右单旋
2、左单旋
3、左右双旋
4、右左双旋
AVL树是一颗平衡二叉搜索树,它的本质就是一颗之前说过的二叉搜索树。但是二叉搜索树可能会出现极端情况,导致二叉搜索树变成一颗单边的树,这样使用二叉搜索树的效率就大大降低了,为了防止这种情况的出现,有了新的平衡二叉搜索树。它就是在普通二叉树的基础上多了一个要求——所有结点的左右子树的最大高度差不能超过 2。这样不仅能防止出现普通二叉搜索树中的极端情况,还能有效降低树的高度,减少搜索需要的时间消耗,同时构建时花费的时间也不是很多。下图就是一颗平衡二叉搜索树(平衡因子我们采用右-左的形式)。
一、AVL树结点
平衡二叉搜索树的结点和二叉搜索树的结点结构是极为类似的,为了方便维护这个二叉搜索树,我们给结点添加了两个新的成员变量_bf表示当前结点的平衡因子,如果平衡因子不符合要求,我们需要对结点进行翻转操作。同时为了方便翻转操作,我们添加了一个_parent指针指向父亲结点。
template<class T>
struct AVLTreeNode
{
AVLTreeNode<T>* _left;
AVLTreeNode<T>* _right;
AVLTreeNode<T>* _parent;
T _data;
int _bf;
AVLTreeNode(const T& data=T())
:_data(data)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
{
}
};
二、AVL树结构
和所有的树型结构一样,我们只需要通过一个根节点的指针就能找到这棵树,所以在AVL树的结构里我们也只需要一个指向根节点的指针这样的成员变量。
template<class T>
class AVLTree
{
typedef AVLTreeNode<T> Node;
private:
Node* _root = nullptr;
};
三、插入数据(重点)
插入数据的过程分为两步,第一步就是找到要插入的数据的位置,这一步和之前在二叉搜索树里讲的是一样的,如果当前位置的值比插入数据大,则左子树里继续寻找,若比当前位置小,则在右子树里继续寻找,直到找到应当插入的位置,把数据插入进去;如果在树里已经存在数据,则插入失败。
插入数据的第一步是完全重复的,但是我们要保证在插入数据以后,这个二叉树还能保持平衡,此时我们就要从插入结点的位置向上开始更新平衡因子。
假如我们在左子树中插入了数据,此时我们可能影响的结点最多只会到根结点,并且是沿着插入位置一直向上的,与根结点的右子树是完全无关的,所以我们只要沿着插入的位置寻找父亲结点更新平衡因子即可。同时我们可以通过判断插入位置在父亲结点的左孩子还是右孩子,即可判断平衡因子是加一还是减一(平衡因子这里采用右-左的形式,所以如果插入在右孩子父亲的平衡因子加1,插入在左孩子,父亲的平衡因子减一)。
如果更新了父亲的平衡因子以后,发现父亲的平衡因子变成了0,那么父亲的平衡因子一定是由-1>>0或者1>>0。这说明在插入新结点之前,父亲结点的左右子树处于一个一边高一边低的状态,同时我们的新结点插入到了低的那一边,因此结点所在子树的高度并没有发生改变,所以插入新的结点的时候只会影响到这个父亲结点的位置,再往上的结点的平衡因子不会发生改变,所以可以退出循环。
如果更新了父亲的平衡因子以后,发现父亲的平衡因子变成了1或者-1,那么父亲的平衡因子一定是由0>>1或者0>>-1,这说明在插入新结点之前父亲结点所在子树的左右子树的高度是一样,在插入新的结点以后,变成了一高一低的情况,父亲结点所在子树的高度发生了变化,因为父亲的父亲结点的平衡因子要发生改变,所以还要继续向上更新。
如果更新了父亲的平衡因子以后,发现父亲的平衡因子变成了2或者-2,那么此时平衡二叉搜索树的平衡就被打破了,我们在当前结点进行翻转操作来降低树的高度,达到让这个树重新恢复平衡的目的,这个旋转又分为四种情况——右单旋、左单旋、左右双旋、右左双旋,这个是AVL树中最重要的部分,也是最难的部分。
bool Insert(const T& data)
{
if (_root == nullptr)
{
_root = new Node(data);
return true;
}
//找插入的位置
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_data > data)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_data < data)
{
parent = cur;
cur = cur->_right;
}
else
assert(false);
}
cur = new Node(data);
if (parent->_data > data)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
//更新平衡因子
//根结点的父亲是nullptr 所以更新到根结点的时候就会停下来
while (parent)
{
//判断新插入的结点在父亲的左孩子还是右孩子
if (cur == parent->_left)
parent->_bf--;
else
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)
{
//旋转
break;
}
else
assert(false);
}
return true;
}
1、右单旋
这里我们拿这样的一颗树来举例,这里的10这个结点可以是整颗树的根结点,也可以是整颗树中一个子树的根结点,而a、b、c三棵子树则是高度为h(h>=0)的三棵树,这样抽象出来的一棵树就能包括右单旋的所有情况。
这时是在a的左子树或者右子树中插入了一个数据,使得a子树的高度变成了h+1,此时5的平衡因子由0变成了-1.继续向上更新平衡因子;10的平衡因子由-1变成了-2,此时以10为根结点的子树的平衡被破坏了,在这个结构下,我们要对以10为根结点的子树进行右单旋,使其高度由h+2恢复成h+1。
5的右子树b中的所有值是大于5并且小于10的,所以我们可以把b子树接到10的左子树上是不会破坏二叉搜索树的结构的,然后我们再把10接到5的右子树上,我们会发现以10为根高度为h+2的子树变成了以5为根高度为h+1的子树,同时所有结点也都符合二叉搜索树的规则;并且在插入新结点之前以10为根的子树的高度也是h+1,所以就不用继续向上更新平衡因子了。
在旋转的理论环节我们只需要改变子树的连接位置即可,但是在实际的代码实现中我们还要同步的改变_parent指针的指向,同时还要注意h的高度为0时的情况。
我们可以先用两个变量subL(结点5)和subLR(b子树的根结点)来标记会经常使用到的这两个子树的根结点。第一步就是把subLR接到parent(10结点)的左边,当h为0时,subLR就是空指针,就不用把subLR的_parent指针接回去,不然会造成访问空指针的错误;第二步要保存子树根结点父亲的信息,因为parent结点可能是整棵树的根结点,也有可能是一部分子树的根结点,我们在旋转完了以后,要把新的根结点接到原先的位置上;第三步就是把parent接到subL的右子树上;第四步就是把新的根结点subL接到原先的位置上,如果parent为整棵树的根结点,那么它是没有父亲结点的,此时要更新整个AVL树_root的指向,如果parent只是一部分子树的根,那就把subL接回原先的位置即可;最后一步就是更新平衡因子,从图中我们可以很清楚的看出来,在完成旋转以后a子树的平衡因子没有发生改变,但是subL和parent的左右子树的高度差都变成了0,所以我们在这里要把他们的平衡因子更新一下。
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
//h为0时就不用再把subLR的_parent接回去
if (subLR)
subLR->_parent = parent;
//保存子树根结点的父亲信息
Node* pParent = parent->_parent;
//把parent接到subL的右边
subL->_right = parent;
parent->_parent = subL;
//把subL接到原先parent的位置
subL->_parent = pParent;
if (pParent == nullptr)
_root = subL;
else
{
if (pParent->_left == parent)
pParent->_left = subL;
else
pParent->_right = subL;
}
//更新平衡因子
subL->_bf = parent->_bf = 0;
}
2、左单旋
左单旋的情况和右单旋是完全类似的,这里拿下图举例,就是在15的右子树中插入一个结点,导致15的右子树a由高度h变成了h+1,此时15的平衡因子为1,10的平衡因子为2,二叉搜索树的平衡被破坏,所以要在10的位置进行左单旋。
左单旋的步骤就是先让15的左子树b接到10的右子树上,再让10变成15的左子树,最后再把15接回原来的位置即可,这里要注意的事项和右单旋也是一样的,右h为0以及parent是整个树的根的可能性。
//左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//把subRL接到parent的右边
parent->_right = subRL;
//h为0时subRL为空树
if (subRL)
subRL->_parent = parent;
//记录parent的父亲结点的信息
Node* pParent = parent->_parent;
//把parent接到subR的左边
subR->_left = parent;
parent->_parent = subR;
//把subR接到原先的父亲下面
subR->_parent = pParent;
//判断原本的parent是否为整棵树的根结点
if (pParent == nullptr)
_root = subR;
else
{
if (parent == pParent->_left)
pParent->_left = subR;
else
pParent->_right = subR;
}
//更新平衡因子
parent->_bf = subR->_bf = 0;
}
3、左右双旋
左右双旋的初始模型和右单旋的模型相同,新增的结点不是在a而是在b上,此时就需要用到左右双旋。
左右双旋就和它的名字一样,只需要先进行一次左单旋,再进行一次右单旋就行,左右双旋的要点在于平衡因子的更新,要分三种情况分别讨论。
左右双旋的操作就是先对parent的左子树的根节点进行一次左单旋,再对parent为根结点的子树进行一次右单旋就可以实现重新平衡的目的,但是这样做平衡因子并没有完全更新好,所以我们要分三种情况讨论平衡因子的更新情况。
一:插入的结点在subLR的左子树,因为中间过程中需要更新平衡因子的只有parent、subL、subLR三个结点,所以这里我们可以通过观察最后的结果来更新平衡因子,当插入的位置在subLR的左子树时,subL的平衡因子为0,subLR的平衡因子为0,parent的平衡因子为1。
二:插入的位置在subLR的右子树,我们同样需要更新的还是 parent、subL、subLR三个结点,同样通过观察最后的结果来得出结论。此时subL的平衡因子为-1,subLR的平衡因子为0,parent的平衡因子为0。
三:插入到subLR位置以后就破坏了平衡,此时旋转的步骤也是一样,但是旋转以后所有的结点的平衡因子都是0。
这三种情况我们是通过判断插入的数据是在subLR的左子树还是右子树,又或者是插入的数据就是subLR来区分,其实我们可以通过subLR的平衡因子就可以直接区分出三种情况,插入数据以后,当subLR的平衡因子为0时,此时插入的结点的就是subLR;插入结点后subLR的平衡因子是1时,此时插入的位置在subLR的右子树;插入结点后subLR的平衡因子是-1是,此时插入的位置在subLR的左子树。
//左右双旋
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else
assert(false);
}
4、右左双旋
右左双旋 的旋转方式和左右双旋一样,这里我就直接给出三种情况的结果图。
当subRL的平衡因子为0时,parent、subR、subRL的平衡因子都是0,;当subRL的平衡为-1时,parent、subRL的平衡因子为0,subR的平衡因子为1;当subRL的平衡因子为1时,subRL、subR的平衡因子为0,parent的平衡因子为-1。
//右左双旋
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}
else
assert(false);
}
通过parent和cur的平衡因子就能判断出需要使用哪种旋转,完成的插入操作代码如下:
//插入结点
bool Insert(const T& data)
{
if (_root == nullptr)
{
_root = new Node(data);
return true;
}
//找插入的位置
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_data > data)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_data < data)
{
parent = cur;
cur = cur->_right;
}
else
assert(false);
}
cur = new Node(data);
if (parent->_data > data)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
//更新平衡因子
while (parent)
{
if (cur == parent->_left)
parent->_bf--;
else
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)
RotateR(parent);
else if (parent->_bf == 2 && cur->_bf == 1)
RotateL(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;
}