欢迎来到博主的专栏:数据结构
博主ID:代码小豪
平衡二叉搜索树
平衡二叉搜索树是二叉搜索树的衍生,其主要目的是解决搜索二叉树的退化问题。当搜索二叉树的插入的数据有序或接近有序时,二叉搜索树会退化成链表。
此时搜索二叉树无论是插入数据还是查找数据的时间复杂度会退化成O(N),失去了搜索二叉树的优势。为了解决这个问题,平衡二叉搜索树应运而生。
平衡二叉搜索树的重点在于平衡,所谓平衡,即二叉树的每个节点之间的左右子树高度差接近为0,红黑树,AVL树都是平衡二叉搜索树,只是采取的策略不同。
AVL树
AVL树采取的平衡策略为,每个节点的左右子树高度差不超过1。即在AVL树中,对于每个节点,其左右子树的高度都应符合以下情况
由于AVL树能够保持平衡,使得二叉树不会退化变成链表,因此AVL的搜索和插入的效率总是保持O(N)。
平衡因子
想要实现平衡二叉树,首先要能够检查根节点左右树之间的高度差(因为插入和删除都有可能导致根节点的左右子树高度差发生变化,破坏树的平衡)。AVL树采取的策略是记录每个节点的平衡因子。
平衡因子的计算公式为
平衡因子=右子树高度-左子树高度
当然你可以选择左子树高度-右子树高度的值作为平衡因子,在一些逻辑判断方面进行修改即可。
旋转
随着AVL树不断插入或删除,有很大的可能会破坏AVL树的平衡(即某个节点的平衡因子大于等于2),为了维持平衡,需要将AVL树重新变得平衡,这个操作称为旋转,旋转需要对父子节点之间的联系进行修改,因此每个节点还需要指向其父节点。关于旋转的具体操作,博主将在后续文章当中说明。
AVL树的数据结构
AVL树的节点需要记录五个数据,分别是key值和value值的组合对,指向父节点的指针parent,一个平衡因子,以及两个分别指向左右子树的指针。
template<class key,class value>
struct avltreenode
{
avltreenode(const pair<key, value>& kv)
:_kv(kv)
, _parent(nullptr)
, _left(nullptr)
, _right(nullptr)
, bf(0);
{}
pair<key, value> _kv;
avltreenode<key, value>* _parent;//指向父节点
avltreenode<key, value>* _left;//指向左子节点
avltreenode<key, value>* _right;//指向右子节点
int bf;//平衡因子
};
对节点的构造采取以下攻略:传入key和value的组合对,由于当前节点还没有左右子树,因此平衡因子为0,各指针置为空。
更新平衡因子
AVL树的查找与插入和删除的策略与搜索二叉树无异,当数据更新时(插入或删除节点),需要对平衡因子进行更新(因为插入或删除一定会改变某个节点的平衡因子),如果平衡因子异常了,就使用旋转保持平衡。
ok,在解决旋转的问题之前,还是先看看如何更新平衡因子吧。
首先我们来考虑一个问题,当插入一个节点时,有哪些节点需要更新平衡因子?
例1:
观察上图可以发现,插入节点时,父节点一定会更新平衡因子,有时候祖先节点也需要更新平衡因子。关于更新平衡因子的规则如下:
(1)节点插入在父节点的左边,父节点的平衡因子-1。
(2)节点插入在父节点的右边,父节点的平衡因子+1
例2
观察上图可以发现,插入节点时,有时候祖先节点(父节点的父节点…)也需要更新平衡因子。那么什么时候更新祖先节点呢。
是否更新祖先节点的条件在于其子节点的左右子树是否发生高度差的变化,如图1,插入新节点后,父节点的高度并没有变长,因此祖先节点不更新,如图2,插入新节点后,父节点的高度变长了(右子树变高了,因此高度变长),所以祖先节点也要更新。
判断方式为
(1)父节点平衡因子更新后,若平衡因子值为0,不需要更新祖先节点(因为高度没变,变的是高度差)。
(2)父节点平衡因子更新后,若平衡因子值为-1/1,此时更新祖先节点(左树/右树变高了)。
父节点插入新节点时,会发生3种情况。
(1)父节点原平衡因子为1/-1,更新平衡因子后变为0,不需要向上更新
(2)父节点元平衡因子为0,更新平衡因子后变为1/-1,此时需要向上更新
(3)父节点平衡因子为1/-1,更新平衡因子后变为2/-2,此时平衡因子异常,需要旋转节点保持平衡
父节点更新平衡因子后不会高于2,高于2则说明程序出bug了,需要排查。
void keep_balance(Node* parent, Node* cur)
{
while (parent!= nullptr)
{
if (parent->_left == cur)
{
parent->_bf--;
}
else if (parent->_right == cur)
{
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)
{
//平衡因子失常,进行旋转
else
{
cout << "平衡因子出现错误";
assert(0);
}
}
}
旋转
AVL树平衡因子失常的情况可以分为以下四种,先来看情况(1)
情况(1),节点的右子树失常。
此时使用左单旋可以解决失常问题,左单旋的操作如下:
(1)将right的左子树,变成node的右子树
(2)node成为right的左子树
(3)node的parent变成right的parent
根据搜索二叉树的性质,right的原左子树中的数据皆大于node,小于right,因此经过左单旋后,right的左子树都是小于right,而右子树皆大于right。因此左单旋不会破坏搜索二叉树的性质。
通过观察上图可知,如果node的平衡因子为2,right为1,就使用使用左单旋,当左单旋操作结束时,node和right的平衡因子都更新
if (parent->_bf == 2 && parent->_right->_bf == 1)
{
Node* right = parent->_right;
remoteL(parent);//左单旋
right->_bf = parent->_bf = 0;//左单旋后平衡因子更新
}
左自旋的实现如下:
void rotateL(Node* parent)
{
Node* parentparent = parent->_parent;
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL != nullptr)
{
subRL->_parent = parent;
}
subR->_left = parent;
parent->_parent = subR;
if (parentparent == nullptr)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
subR->_parent = parentparent;
if (parentparent->_left == parent)
parentparent->_left = subR;
else
parentparent->_right = subR;
}
}
情况(2)节点的左子树变长引发的失常。
右单旋的操作如下:
(1)让left的右子树,变成node的左子树
(2)node,变成left的右子树
(3)node的父节点,变成left的父节点
else if (parent->_bf == -2 && parent->_left == -1)
{
Node* left = parent->_left;
rempteR(parent);
left->_bf = parent->_bf = 0;
}
右单旋的实现代码如下:
void rotateR(Node* parent)
{
Node* parentparent = parent->_parent;
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR != nullptr)
subLR->_parent = parent;
subL->_right = parent;
parent->_parent = subL;
if (parentparent == nullptr)
{
_root = subL;
}
else
{
if (parentparent->_left == parent)
parentparent->_left = subL;
else
parentparent->_right = subL;
}
subL->_parent = parentparent;
}
仅靠左右单旋并不能解决所有的问题。比如:
可以发现此时右单旋不能解决失衡问题。这种情况可以抽象成情况(3)
情况(3):左子节点的右子树变长导致的平衡失常问题。
或者:
这两种情况都可以使用左右双旋解决问题。
左右双旋的逻辑如下:
(1)将left进行左旋
(2)将node进行右旋
这两种情况下,左右双旋的结果并不相同。这是因为,平衡因子会受到R节点的影响,如果R节点的平衡因子为-1,那么右子树的平衡因子会变化,如果R节点的平衡因子为1则结果也会不同。
else if (parent->_bf == -2 && parent->_left == 1)
{
Node* left = parent->_left;
Node* R = left->_right;
int bf = R->_bf;
remoteLR();//左右双旋
if (bf == 1)
{
parent->_bf = 1;
left->_bf = 0;
R->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
R->_bf = 0;
left->_bf = -1;
}
}
左右双旋的代码也很简单。
void rotateLR(Node* parent)
{
Node* subL = parent->_left;
rotateL(subL);
rotateR(parent);
}
情况(4),右子树的左子树变长引发的平衡因子失衡。
当R为-1的情况博主就不再推导了,直接给出方法。
情况4的解决方法为右左双旋
(1)right节点进行右旋
(2)node节点进行左旋
(3)注意更新平衡因子
else if (parent->_bf == 2 && parent->_right == -1)
{
Node* right = parent->_right;
Node* L = right->_left;
int bf = L->_bf;
remoteRL(parent);
if (bf == 1)
{
L->_bf = 0;
right->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
L->_bf = 0;
right->_bf = 1;
parent->_bf = 0;
}
}
void rotateRL(Node* parent)
{
Node* subR = parent->_right;
rotateR(subR);
rotateL(parent);
}