目录
一、概念
二、插入
1.KV模型的AVL树结点定义
2.插入
1.按照BST的规则先插入
2.更新平衡因子
3.旋转的4种情况
1.左单旋
2.右单旋
3.左右双旋
4.右左双旋
三、AVL树的判断
假设程序出了问题,怎么分析
一、概念
二叉搜索树所具有的问题:
将排好序的序列,依次插入到BST中,所得到的二叉搜索树是一个纵深,即退化为了单支树,其使用效率与链表接近。因此,引入平衡的概念,即AVL树的特点:
向二叉搜索树中插入新结点后,保证每个结点的左右树高度之差的绝对值不超过1(必要时需要对树中的结点进行调整)。
为什么叫AVL树?
平衡的解决办法是由俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis发明。
为什么高度差是 0 / 1 / -1的叫AVL树?
因为结点个数的不同,无法保证左右子树的高度差为0,故指定最大为1/-1,如果超过1/-1,就对它做平衡处理。
对于AVL树中的每一个结点:
它的左右子树都是AVL树
左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
二、插入
1.KV模型的AVL树结点定义
相比BinarySearchTreeNode,AVL树的结点引入平衡因子 = 右子树高度 - 左子树高度,并且多加了指向父亲的指针。
template <class Key,class Value> struct AVLTreeNode { typedef AVLTreeNode<Key,Value> Node; Node* _left; Node* _right; Node* _parent; int _bf;//balance factor pair<Key, Value> _kv; AVLTreeNode(const pair<Key,Value>& kv) :_left(nullptr) ,_right(nullptr) ,_parent(nullptr) ,_bf(0) ,_kv(kv) {} };
2.插入
插入分为2步:
1.按照BST的规则插入
2.更新平衡因子
更新平衡因子后,对平衡因子进行检查,如果有结点的平衡因子为2或者-2,则进行旋转,这一步也就是对二叉树做平衡处理。
1.按照BST的规则先插入
bool Insert(const pair<K, V>& kv) { }
//1.按照BST的插入规则先插入 if (_root == nullptr) { _root = new Node(kv); return true; } Node* cur = _root; Node* parent = nullptr; while (cur) { if (kv.first < cur->kv._first) { parent = cur; cur = cur->_left; } else if (kv.first > cur->kv.first) { parent = cur; cur = cur->_right; } else { //说明要插入的值已经存在了,提升插入失败 return false; } } cur = new Node(kv); if (kv.first > parent->kv.firsrt) { parent->_right = cur; } else { parent->_left = cur; } cur->_parent = parent;
2.更新平衡因子
更新平衡因子时要不断向上更新多个结点,这个过程的结束条件:
1.如果向上更新一直到根结点,即当前结点cur->_parent == nullptr,则停止更新,此时得到的树满足AVL树。
2.如果向上更新一直到某一个结点的_bf为0,则停止更新,说明该结点的子树高度没有发生变化。
更新平衡因子的结果:
1._bf == 0,插入结束。
2._bf == 1 / -1,则向上继续更新,cur = parent ,parent = parent->_parent。
3._bf == 2 / -2,旋转。
//2.更新平衡因子 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 = cur->_parent; parent = parent->_parent; } else if(parent->_bf == 2 || parent->_bf == -2) { //旋转 } else { assert(false); } }
3.旋转的4种情况
如果出现了违反AVL平衡的规则,要对其旋转,来得到平衡。
关于此处对旋转分析的画图,有这样一个问题,为什么某种情况就会引发对应的特定旋转?
旋转是因为某个结点的平衡因子变为了2/-2,需要说明的是:
1,从插入结点向上更新平衡因子时,一定是只有一个结点的平衡因子变为了2/-2,且立刻开始平衡处理。
2,平衡因子变为2/-2的必要条件是它的平衡因子为1/-1,因此下面的分析都是基于结点的平衡因子为1、-1的分析。
3,平衡因子由1变为2,可能是由平衡因子为1或者-1的结点引发
4,平衡因子由-1变为-2,可能是由平衡因子为1或者-1的结点引发
1.左单旋
具体到h=0的树中,实质就是:从平衡因子变为2的结点看,是一条右下方向的直线
,说明:违反规则的结点的平衡因子由1变为2,是由平衡因子为1的结点引发,这是左单旋的情况。
void RotateL(Node*& parent) { Node* subR = parent->_right; Node* subRL = subR->_left; parent->_right = subRL; if(subRL) subRL->_parent = parent; subR->_left = parent; Node* pparent = parent->_parent; parent->_parent = subR; if (parent == _root) { _root = subR; subR->_parent = nullptr; } else { if (parent == pparent->_left) { pparent->_left = subR; } else { pparent->_right = subR; } subR->_parent = pparent; } parent->_bf = 0; subR->_bf = 0; }
2.右单旋
具体到h = 0的树中,实质就是:从平衡因子变为-2的结点看,是一条左下方向的直线
,说明:违反规则的结点的平衡因子由-1变为-2,是由平衡因子为-1的结点引发,这是右单旋的情况。
void RotateR(Node*& parent) { Node* subL = parent->_left; Node* subLR = subL->_right; parent->_left = subLR; if (subLR) subLR->_parent = parent; subL->_right = parent; Node* pparent = parent->_parent; parent->_parent = subL; if (parent == _root) { _root = subL; subL->_parent = nullptr; } else { if (pparent->_left == parent) { pparent->_left = subL; } else { pparent->_right = subL; } subL->_parent = pparent; } parent->_bf = 0; subL->_bf = 0; }
3.左右双旋
对于这棵树
如果我们在a这棵树中插入了结点,那么引发到结点90处就是右单旋。如果在 b中插入,结点90的平衡因子会变为 -2,而结点30的平衡因子变为1,实质就是,从平衡因子为 -2的结点看,这是一条先左后右的的折线
,说明:违反规则的结点的平衡因子由-1变为-2,是由平衡因子为1的结点引发,这是左右双旋的情况。即先左单旋,再右单旋的平衡处理。
先把子树b拆分为根+b c两棵子树
对结点30左单旋:
再对结点90右单旋:
如果插入的结点在子树c上
对结点30左单旋:
![]()
对结点90右单旋:
经过对比,可以发现,当插入结点后,结点90和结点30的平衡因子均未发生改变,发生变化的只是结点60的平衡因子,因此,可以通过这个条件来确定最终结点的平衡因子。
但是,当h = 0,又是一种新的情况:
经过上述分析,发现插入结点的位置不同,树的结点的平衡因子也不同,之所以平衡因子会有三种结果,是因为折线的平衡处理要进行两次旋转。而仅仅利用已经实现的单旋代码,经过上述处理,得到的结点平衡因子均为0,并不满足要求,因此,对于平衡因子的处理,我们要单独分析,单独处理。
通过对比,可以利用结点60的平衡因子分别为1/-1/0对应三种情况。
void RotateLR(Node*& parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(subL);
RotateR(parent);
if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
assert(false);
}
}
4.右左双旋
和左右双旋一样的分析:
void RotateRL(Node*& parent) { Node* subR = parent->_right; Node* subRL = subR->_left; int bf = subRL->_bf; RotateR(subR); RotateL(parent); subRL->_bf = 0; if (bf == 1) { parent->_bf = -1; subR->_bf = 0; } else if (bf == -1) { parent->_bf = 0; subR->_bf = 1; } else if (bf == 0) { parent->_bf = 0; subR->_bf = 0; } else { assert(false); } }
三、AVL树的判断
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证 AVL 树,可以分两步:1. 验证其为二叉搜索树如果中序遍历可得到一个有序的序列,就说明为二叉搜索树2. 验证其为平衡树每个节点子树高度差的绝对值不超过1( 注意节点中如果没有平衡因子 ) ,节点的平衡因子是否计算正确。
bool _isBalance(Node* root) { if (root == nullptr) { return true; } int left_height = _Height(root->_left); int right_height = _Height(root->_right); if (abs(left_height - right_height) >= 2) { cout << "不平衡" << root->_kv.first <<" :bf" << root->_bf<< endl; return false; } if (right_height - left_height != root->_bf) { cout << "平衡因子异常" << root->_kv.first << " :bf" << root->_bf << endl; return false; } return _isBalance(root->_left) && _isBalance(root->_right); }
上述代码会有大量重复计算高度的过程,将上述前序改为后序:
bool _isBalance(Node* root,int& high) { if (root == nullptr) { high = 0; return true; } int left_height = 0; int right_height =0; if (!_isBalance(root->_left,left_height) || !_isBalance(root->_right,right_height)) { return false; } if (abs(left_height - right_height) >= 2) { cout << "不平衡" << root->_kv.first <<" :bf" << root->_bf<< endl; return false; } if (right_height - left_height != root->_bf) { cout << "平衡因子异常" << root->_kv.first << " :bf" << root->_bf << endl; return false; } high = left_height > right_height ? left_height + 1 : right_height + 1; return true; }
假设程序出了问题,怎么分析
为了模拟场景,现在将部分代码屏蔽或者故意写错:
![]()
测试代码为:
运行结果为:
说明在插入结点14后,更新平衡因子时,结点6发生异常。
如果要调试观察,一般利用断点打到当前步骤。但是断点的控制如果使用不当,很容易徒劳白费。因此,一般的测试形式是在写代码控制,比如像下面这样利用断点: