文章目录
1、了解AVL树
如果数据有序或接近有序,二叉搜索树将退化为单支数,查找元素相当于在顺序表中搜索元素,效率低下。因此有两位俄罗斯数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的办法:当向二叉搜索树插入新节点后,如果能保证每个结点的左右子树高度差的绝对值不超过1,即可降低树的高度,减少平均搜索长度。
AVL树也叫高度平衡搜索树。左右子树高度差叫平衡因子。平衡因子只是AVL树实现的一种方式,还有别的方式。在平衡因子方式中,如果平衡因子是-1,0,1,那么就是AVL树,不是就需要调整。
2、模拟实现
博客末尾有整体代码的链接
结构
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
pair<K, V> _kv;
int _bf;
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{
}
};
Insert
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->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;//已经插入好了,关系也都搞清楚了,cur就是新节点,parent就是它的父节点
return true;
}
private:
Node* _root = nullptr;
};
这是一个基本的AVL树的代码,bf就是平衡因子。在此基础上开始加功能。
如何写?假如这个新结点插入在左子树最下边一个结点的后面,它有三个可能,在最下结点的左右两边,或者和这个结点同一层。插入完成后,平衡因子要更新,更新完后就得查看bf是否还是符号要求,如果绝对值大于1,平衡结构收到影响,就需要旋转处理。
插入结点后这个结点只会影响它的部分祖先,也就是那一个子树里的某些结点。这时候就可以利用parent来访问祖先。
如果新结点在父节点右边,父节点的因子++;如果在左边,因子–。更新完后父结点的平衡因子如果等于1或者-1,说明高度变了,要更新爷爷结点的因子。
从cur和parent现在的位置出发,cur新增结点当然是0,假如在parent右边,parent的因子变为1,cur来到parent位置,parent来到上面的结点位置,假如原parent所在结点在父结点的右侧,那么因为在右侧所以要++,现在新parent的位置就会变成2,这就出来另一种情况了,更新完后新parent的因子变为2,或者可能出现-2,这时候parent所在的子树不平衡,就需要处理一遍,也就是旋转。
如果插入新结点后,parent的因子是0,就不需要往上调整了。说明这时候parent原本是1或者-1,插入之后就高度不变,就可以插入结束了,这时候parent左右子节点都有,所以高度不需要调整。
所以就是
parent的bf绝对值是1,那就需要往上更新
parent的bf是0,那就插入结束
parent的bf绝对值大于1,那就旋转
可能会出现这种情况,一路更新到根节点。
在上面的insert函数里,我们继续这样写
while (parent)
{
if (cur == parent->_right)
{
parent->_bf++;
}
else
{
parent->_bf--;
}
//比较因子
if (parent->_bf == 1 || parent->_bf == -1)
{
//继续更新
parent = parent->_parent;
cur = cur->_parent;
}
else if (parent->_bf == 0)
{
break;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//需要旋转处理
}
else
{
//为了防止出现更多绝对值大于1的情况
assert(false);
}
}
接下来就是旋转
3、旋转
1、左单旋(右边高,插入在右边)
abc都是高度为h的子树。比如h == 2,就可以有两个子树或一个左子树或一个右子树的3种情况。c一定是有两个子树的,而ab则是三种情况中任意一种。
h == 1
在最底层最右边,也就是c,无论插入在左还是右,c的因子都会变化,对于60这个结点来说,它的右子树这里插入新结点,所以它+1,而30也是右子树插入了一个新节点而变为2。
h == 0,abc位置没有结点,插入后往上更新因子,各因子的变化如同上一段。
h == 2,c有四个位置可插入,无论哪一个,都会让30结点的因子变为2。如果c结点不是有两个子树,就不会让30这个结点变为2。假如c有一棵子树,那么再插入一个节点后,c那里就需要旋转了,也就不会影响上面的节点。在这种情况下,ab总共有9种组合,c可以有4个变化,所以有36个组合。
h == 0和1,各有2个组合,总共40个组合。无论怎么样,都是左单旋。最后都要变成第一个图第三个模样。
b变成30的右边,30变成60的左边,60变成整棵树的根。
旋转的原则是保持搜索树,并且左右均衡,降低整棵树的高度
2、右单旋(左边高,插入在左边)
了解左单旋后,右单旋就会更容易理解。
b变成60的左边,60变为30的右边,30成为整棵树的根。
右单旋情况分析和左单旋一样。
调用的条件
//前面这几个判断,是在找因子是2的那个节点
//代入情景想一下,因子是1或-1就向上走,再次循环
//整棵树都是1、-1、0那就没问题,找到2,那也就进入了这个判断
//这时候问题parent就是问题节点,cur就是它的子节点,下面就开始了判断
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);
}
}
旋转代码(放在private里,单旋实现的详细情况放在代码实现中)
void RotateL(Node* parent)//左单旋
{
//画图来理清这些关系
Node* subR = parent->_right;
Node* subRL = subR->_left;
//前置设定好后,开始旋转。要考虑清除,每个结点有三个指针的,三叉链
//subRL可能为空
//旋转的不一定是整棵树
parent->_right = subRL;
if (subRL)//如果是空,也就不管了,因为也就是说根本没有这个节点,这里被初始化为空,所以就直接指向空了
subRL->_parent = parent;
Node* ppnode = parent->_parent;//先拿走父节点的父节点,也就是要改变的部分的根,因为下面要改变根
subR->_left = parent;
parent->_parent = subR;
//前面这些已经改了很多关系了
//总体来说,除了原parent的父节点还指向这个parent,其他关系都已经调整好了,所以只剩这个爷节点的关系了
if (ppnode == nullptr)//到这里判断是不是根节点,是根节点就直接让定义新的根就可以了
{
_root = subR;
_root->_parent = nullptr;
}
else//不是就调整爷节点的指针指向新根
{
if (ppnode->_left == parent)
{
ppnode->_left = subR;
}
else
ppnode->_right = subR;
subR->_parent == ppnode;
}
parent->_bf = subR->_bf = 0;
}
//左单旋详细说了关系调整,右单旋也是一样,画图就清楚了
void RotateR(Node* parent)//右单旋
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* ppnode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (ppnode == nullptr)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subL;
}
else
ppnode->_right = subL;
subL->_parent = ppnode;
}
subL->_bf = parent->_bf = 0;
}
3、双旋(先左后右)
h == 0,abcd都不存在,60结点也不存在,60这个结点就相当于新增。此时30因子是1,90因子是-2。
上图为h == 1,60的左右子树插入结点后都需要旋转。
h == 2。bc是1,都是1个结点,ad则是三种情况的一种,b或者c的任意一个孩子位置插入结点都会引发旋转,这样总共36种,加上上面的,总共40种。
这时候一个单旋不能解决问题,需要两个单旋。不论b还是c插入,30和60都需要进行左旋一次,左旋后会发现,二叉树一边高,60和30都在90的左子树上,所以需要从90开始右旋。
void RotateLR(Node* parent)
{
RotateL(parent->_left);
RotateR(parent);
}
双选控制因子比较麻烦。b位置插入的话,60原始因子是-1,最后30 60 90对应的因子分别是001;如果是c插入,60原始因子是1,就是-100;h == 0的时候,两种情况分别是1 0 -2,0 0 0。上图是b插入,下图是c插入。这里还是要画图理解。
void RotateLR(Node* parent)
{
//parent为90,subL是30,subLR是60
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 1)
{
parent->_bf = 0;
subLR->_bf = 0;
subL->_bf = -1;
}
else if (bf == -1)
{
parent->_bf = 1;
subLR->_bf = 0;
subL->_bf = 0;
}
else if(bf == 0)
{
parent->_bf = 0;
subLR->_bf = 0;
subL->_bf = 0;
}
else
{
assert(false);
}
}
4、双旋(先右后左)
h == 0,60就是新增结点。
h == 1,60下面没有子树,30有一个左结点,90有一个右结点,那么在60左右新增结点都会引发旋转
h == 2,那么a和d就有三种情况,60有两个子结点bc,bc可以新增4个结点。
有三种情况会引发双旋,c插入,c高度变化为h;b插入,b高度变化为h;60本身是新增点。要判断是哪个情况就判断60的因子,也就是1,-1,0
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 1)
{
parent->_bf = -1;
subRL->_bf = 0;
subR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subRL->_bf = 0;
subR->_bf = 1;
}
else if (bf == 0)
{
parent->_bf = 0;
subRL->_bf = 0;
subR->_bf = 0;
}
else
{
assert(false);
}
}
进入旋转前的判断条件
while (parent)
{
if (cur == parent->_right)
{
parent->_bf++;
}
else
{
parent->_bf--;
}
if (parent->_bf == 1 || parent->_bf == -1)
{
//继续更新
parent = parent->_parent;
cur = cur->_parent;
}
else if (parent->_bf == 0)
{
break;
}
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)
{
RotateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
}
else
{
assert(false);
}
break;
}
else
{
//为了防止出现更多绝对值大于1的情况
assert(false);
}
}
测试代码:
void InOrder()
{
_InOrder(_root);
cout << endl;
}
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_kv.first << " ";
_InOrder(root->_right);
}
void Test_AVLTree1()
{
int a[] = {
16, 3, 7, 11, 9, 26, 18, 14, 15 };
AVLTree<int, int> t1;
for (auto e : a)
{
t1.Insert(make_pair(e, e));
}
t1.InOrder();
}
4、检查平衡
这里检查高度即可,平衡因子因为是我们自己定的,用来判断是否是平衡二叉树有些不妥。
int _High(Node* root)
{
if (root == NULL) return 0;
int leftH = _High(root->_left);
int rightH = _High(root->_lright);
return leftH > rightH ? leftH + 1 : rightH + 1;
}
bool _IsBalance(Node* root)
{
if (root == NULL) return true;
int leftH = _High(root->_left);
int rightH = _High(root->_right);
//所有子树都要检查,所以除了根结点的左右子树,还有剩下结点的
return abs(leftH - rightH) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
bool IsBalance()
{
return _IsBalance(_root);
}
int Height()
{
return _Height(_root);
}
两个测试用例
int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
虽然树正常,但是平衡因子的数值可能不对劲。比如一个结点和它的右子树的因子都是0,这时候再次插入就会引发因子数值的错误。这样改
bool _IsBalance(Node* root)
{
if (root == NULL) return true;
int leftH = _High(root->_left);
int rightH = _High(root->_right);
if (rightH - leftH != root->_bf)
{
cout << root->_kv.first << "节点平衡因子异常" << endl;
return false;
}
//所有子树都要检查,所以除了根结点的左右子树,还有剩下结点的
return abs(leftH - rightH) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
void Test_AVLTree1()
{
//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
int a[] = {
4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
AVLTree<int, int> t1;
for (auto e : a)
{
t1.Insert(make_pair(e, e));
cout << e << "" << t1.IsBalance() << endl;//每插入一次就打印看看谁插入出了问题
}
t1.InOrder();
cout << t1.IsBalance() << endl;
}
把LR的函数中调整因子的代码屏蔽掉,第二个测试用例就会出现6节点异常。
5、测试性能(随机数)
void Test_AVLTree2()
{
srand(time(0));
const size_t N = 100000;
AVLTree<int, int> t;
for (size_t i = 0; i < N; ++i)
{
size_t x = rand();
t.Insert(make_pair(x, x));
}
cout << t.IsBalance() << endl;
}
AVL树的性能和满二叉树差不多,高度是logn,增删查改是logn,插入10亿个结点需要最多查找30次。
6、删除
整体和插入呈现相反的思路。比如删除右边子节点,父节点因子-1,删除后如果父节点是-1就不需要调整,如果是0,那么就需要调整。
下一篇是红黑树。
结束。