文章目录
- 1、了解AVL树
- 2、模拟实现
- 3、旋转
- 1、左单旋
- 2、右单旋
- 3、双旋(先左后右)
- 4、双旋(先右后左)
- 4、检查平衡
- 5、测试性能(随机数)
- 6、删除
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;
return true;
}
private:
Node* _root = nullptr;
};
这是搜索树的代码,在此基础上继续写,完成一个AVL树的版本。
如何写?加入这个新结点插入左子树最下边一个结点的后面了,它当然有三个可能,在最下结点的左右两边,或者和这个结点同一层。插入完成后,平衡因子要更新,更新完后就得查看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的bf绝对值是1,那就需要往上更新
parent的bf是0,那就插入结束
parent的bf绝对值大于1,那就旋转
可能会出现这种情况,一路更新到根节点。
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。因为只有一个子树的话,插入后高度不变,所以不会更新。在这种情况下,ab总共有9种组合,c可以有4个变化,所以有36个组合。
h == 0和1,各有2个组合,总共40个组合。无论怎么样,都是左单旋。最后都要变成第一个图第三个模样。
b变成30的右边,30变成60的左边,60变成整棵树的根。
旋转的原则是保持搜索树,并且左右均衡,降低整棵树的高度
2、右单旋
了解左单旋后,右单旋就会更容易理解。
b变成60的左边,60变为30的右边,30成为整棵树的根。
右单旋情况分析和左单旋一样。
代码
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;
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;
}
调用的条件
if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
}
3、双旋(先左后右)
h == 0,abcd都不存在,60结点也不存在,60这个结点就相当于新增。此时30因子是1,90因子是-2。
上图为h == 1,60的左右子树插入结点后都需要旋转。
h == 2,画图比较麻烦。bc是1,都是1个结点,ad则是三种情况的一种,b或者c的任意一个孩子位置插入结点都会引发旋转,这样总共36种,加上上面的,总共40种。
这时候一个单旋不能解决问题,需要两个单旋。如果b位置插入结点,30和60左旋,60和90右旋;
void RotateLR(Node* parent)
{
RotateL(parent->_left);
RotateR(parent);
}
双选控制因子比较麻烦。b位置插入的话,60原始因子是-1,最后30 60 90对应的因子分别是001;如果是c插入,60原始因子是1,就是-100;h == 0的时候,两种情况分别是10-2,000。
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);
RotateL(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 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 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,那么就需要调整。
下一篇是红黑树。
链接: https://gitee.com/kongqizyd/start-some-c-codes-for-learning.c/blob/master/AVL%E6%A0%91/AVLTree.h
结束。