二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
AVL就是二叉搜索树,只不过多了左右子树的高度差的绝对值不超过1这个概念,所以我们的结构就要引入平衡因子这个成员变量,由于我们需要沿着父亲不断地更新平衡因子,所以AVL树实现成三叉链比较好。
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;
//刚插入的节点没有孩子节点,所以平衡因子给0就好
AVLTreeNode(const pair<K,V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
插入
AVL树的插入和搜索二叉树一样,大的往右边走,小的往左边坐,只不过在插入了以后,这里是三叉链,我们要连同它的父亲一起更新一下。
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
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);
cur->_parent = parent;
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
到这里,搜索二叉树就已经算是插入完成了,但是我们是AVL树,所以我们还需要修改平衡因子,来判断AVL树在插入后是否失去平衡,需要调整。那怎么更新平衡因子呢?
平衡因子 _bf = 右子树的高度 - 左子树的高度
我们会发现插入一个节点后只会影响它的父亲节点和它的祖先节点,如果是右子树我们就让他的父亲的_bf++,如果是左孩子节点我们就让他的父亲的_bf–,直到有个平衡因子的绝对值大于1了,就说明这个节点需要调整。我们会发现这个过程是个循环的的过程,如果父亲的平衡因子更新后是0,那就说明父亲插入前后的高度差没有变化,不需要调整,如果父亲更新后的平衡因子是1或者-1就说明需要接着往上更新,最坏的情况是更新到父亲,如果是2或者-2就说明这个AVL树失去平衡,需要旋转调整。
至此我们就可以先把插入的大框架搭建起来了。
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
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);
cur->_parent = parent;
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
while (parent)
{
if (parent->_left == cur)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
if (parent->_bf == 0)
{
break;
}
else
{
if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
}
else
{
//需要调整
//....
}
}
}
return true;
}
那么接下来就剩下调整了,只要完成了调整我们AVL的插入就完成了。
调整一共有4种情况。只有当父亲的平衡因子到2或者-2的时候才需要调整。这时孩子可以能是1也可能是-1,所以两两结合就是4种情况。
这时我们需要一个右单旋,让它恢复平衡。
从图中我们可以看到,右单旋的触发条件是,父亲的平衡因子是-2,subL也就是cur的平衡因子是-1,这时就会触发右单旋,右单旋就是把subL变成根,然后把parent变成subL的右孩子,而subL的右孩子成为parent的左孩子,就可以完成右单旋,但是在写代码是我们要注意更新他们的父亲,并且在更新subLR的父亲是要注意它可能会不存在,所以我们需要就一个条件判断。
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* parentParent = parent->_parent;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
parent->_parent = subL;
if (_root != parent)
{
if (parentParent->_left == parent)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
else
{
_root = subL;
_root->_parent = nullptr;
}
parent->_bf = subL->_bf = 0;
}
左单旋后:
我们可以看到左单旋的触发条件是parent的平衡因子是2,并且subR也就是cur的平衡因子是1,此时就会触发左单旋,左单旋就是让parent左subR的左孩子,subR的左孩子做parent的右孩子,然后让subR重新做这棵树的根,同样和右单旋一样,我们也要同时更新它们的父亲,并且subRL是有可能为空的,所以在更新它的父亲的时候要防止对空指针的解引用。
//左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* parentParent = parent->_parent;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
parent->_parent = subR;
if (_root!=parent)
{
if (parentParent->_left == parent)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
else
{
_root = subR;
_root->_parent = nullptr;
}
parent->_bf = 0;
subR->_bf = 0;
}
此时光进行一个单旋是解决不了问题的,必须要先把它变成一遍高,在使用单旋。
进行左单旋后变成单边高,此时在进行一个右单旋。
此时这棵树就平衡了。
我们可以看出左右双旋的触发条件是parent的平衡因子是-2,但是subL即cur的平衡因子是1,即出现了不是单边高的情况,但是这个代码写起来还是比较简单的,因为我们可以复用前面的单旋代码,先对subL进行一个左单旋,在对parent进行一个右单旋,就可以了,但是复杂是是平衡因子的更新。
一共就分为这三种情况,我们可以通过subLR的平衡因子来区分,所以我们需要提前记录一下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 = subL->_bf = subLR->_bf = 0;
}
else
{
if (bf == -1)
{
parent->_bf = 1;
subL->_bf = subLR->_bf = 0;
}
else
{
if (bf == 1)
{
subL->_bf = -1;
parent->_bf = subLR->_bf = 0;
}
else
{
assert(false);
}
}
}
}
情况4(右左双旋)
此时就可以达到我们需要的目的了。
我们可以观察到右左双旋触发条件是parent的平衡因子是2,并且subR即cur的平衡因子是-1,此时就要进行右左双选,我们同样可以复用单旋的代码,但是复杂的还是平衡因子的更新。我们来看一下右左双旋的情况:
右左双选的平衡因子更新也就这三种情况。我们也是需要通过subRL之前的平衡因子来进行判断是那种情况。
//右左双旋
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 = subR->_bf = subRL->_bf = 0;
}
else
{
if (bf == 1)
{
parent->_bf = -1;
subR->_bf = subRL->_bf = 0;
}
else
{
if (bf == -1)
{
subR->_bf = 1;
parent->_bf = subRL->_bf = 0;
}
else
{
assert(false);
}
}
}
}
那么到这里我们就可以把插入的逻辑彻底完善了。
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
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);
cur->_parent = parent;
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
while (parent)
{
if (parent->_left == cur)
{
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)
{
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
{
assert(false);
}
}
}
}
return true;
}
我们构造了一个AVL树,怎么去验证它的正确性呢?
验证AVL树最好的方法就是看左右的高度差的绝对值是否大于1,并且与此同时,我们还可以顺带检查一下右子树的高度减左子树的高度是否等于平衡因子,所以我们还需要一个求树该的函数,这个也很好写,只要计算左右子树的高度,然后返回左右子树高的那个+1就可以了。
//判断AVL的正确性
bool isbanlance()
{
return _isbanlance(_root);
}
bool _isbanlance(Node* root)
{
if (root == nullptr)
{
return true;
}
int left = _Heigh(root->_left);
int right = _Heigh(root->_right);
if (right - left != root->_bf)
{
//cout << _root->_kv.first << "异常"<
return false;
}
return abs(left - right) < 2
&& _isbanlance(root->_left)
&& _isbanlance(root->_right);
}
//计算树高
int Height()
{
return _Heigh(_root);
}
int _Heigh(Node* root)
{
if (root == nullptr)
{
return 0;
}
int left = _Heigh(root->_left);
int right = _Heigh(root->_right);
return left > right ? left + 1 : right + 1;
}
总结:
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。