前面我们说了二叉搜索树在极端条件下时间复杂度为O(n),本篇我们将介绍一种对二叉搜索树进行改进的树——AVL树
一、AVL 树的概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
AVL树具有以下性质:
1.AVL树的左右子树都是AVL树
2.左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1) (右子树的高度减去左子树的高度)
AVL树其实就是高度平衡二叉搜索树,
注意:这里的平衡不是相等,而是高度差不超过。其实是因为无法做到高度相等,只能做到高度差不超过1。因为总有一些结点是无法做到满二叉树的
如下是一颗典型的AVL树
对于这种AVL树,它的增删查改效率都是很高的。都是logN级别的复杂度
试想一下:
如果是满二叉树,当它高度为h的时候,他的总结点个数为2^h-1==N。
如果是平衡二叉树的话,当它的高度为h的时候,它的节点个数为。这里的X属于[1,2(h-1)-1],所以它的效率为logN
二、AVL树的实现
1. AVL树的结点定义
如下所示,我们将AVL结点的值定义为一个pair对象,目的是使用key-value模型。bf是一个平衡因子。这里我们还需要使用三叉链结构
template<class K, class V>
struct AVLTreeNode
{pair<K,V> _kv;AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;int _bf;AVLTreeNode(const pair<K, V>& kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}};
2. AVL树的插入部分
关于AVL树的插入,我们根据它的特性,不难得知,首先要先将一个值给插入进去,再去考虑控制平衡。
bool Insert(const pair<K, V>& kv){Node* newnode = new Node(kv);if (_root == nullptr){_root = newnode;return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first > newnode->_kv.first){parent = cur;cur = cur->_left;}else if (cur->_kv.first < newnode->_kv.first){parent = cur;cur = cur->_right;}else{return false;}}if (parent->_kv.first > newnode->_kv.first){parent->_left = newnode;}else{parent->_right = newnode;}newnode->_parent = parent;//平衡return true;}
如上代码所示,与二叉搜索树的方法类似,我们先想办法插入一个值进去。上面我们已经成功插入了一个数据。这一点是不难的。
- 我们先申请一个结点
- 如果根节点为空,那么我们直接将这个结点交给根节点即可。
- 如果根节点不为空,那么由于是二叉搜索树,那么我们先设定两个结点指针,一个指向空称作parent,一个指向根节点称作cur。
- 然后我们让cur去与我们要插入结点进行比较,持续迭代,最终我们就可以找出cur为待插入结点的位置,parent为要插入结点的父亲
- 然后我们根据待插入的值与父节点进行比较,从而确定插入左子树还是右子树。这一步是非常有必要的。
在第五步中,我们极其容易犯一个错误,就是我们想当然的认为,cur此时就是要插入的孩子结点,所以我们会写出cur = newnode这样奇葩的代码,其实是万万不可的。我们可以画个内存图了解一下
- 插入号了结点以后,我们就可以开始将链接关系了。
3.AVL树的平衡因子的改变
将结点插入以后,我们需要做的就是控制平衡。因为我们AVL树的最终目的还是控制平衡。
我们先来看第一种情况
我们就以这颗树作为例子
它本身已经是一颗AVL树了,我们先考虑对右子树的插入
有以下几种情况:
通过这些图我们可以总结出如何更新平衡因子:
- 新增在左,parent的平衡因子减减
- 新增在右,parent的平衡因子加加
- 更新后的parent的平衡因子==0,说明parent所在的子树高度不变,不再影响祖先,不再沿着祖先的路径往上更新
- 更新后parent平衡因子==1/-1,说明parent所在子树的高度变化,影响了祖先,需要沿着祖先的路径往上更新
- 更新后parent的平衡因子==2/-2,说明parent所在子树出现了问题,已经失衡,需要对parent所在子树进行旋转,使其平衡
- 更新到根节点就可以结束了
最后两种情况出现平衡因子为不为0,-1或1的情况,此时需要进行旋转
我们先实现基础的更新平衡因子的函数
//更新平衡因子cur = newnode;while (parent){if (parent->_left == cur){parent->_bf--;}else if (parent->_right == cur){parent->_bf++;}else{assert(false);}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{assert(false);}}
4. AVL树的插入之左旋
当一颗树(这棵树有可能是子树,也可能是一颗完整的树)插入一个结点以后,它会变为如下的形状(红色是插入的结点)
一开始,我们的树还是处于平衡状态的,但是当我们插入了一个结点以后,失去了平衡了,所以我们需要对其进行旋转使其进行平衡。
我们不难得知,上面两种树,都是右边的偏高,我们想,可以让cur作为这颗树的根节点,然后parent作为cur的左子树的话,那么就可以使得高度降低1次了。刚好使得我们的AVL树再次平衡了。
如下所示,当我们进行这样的旋转之后,我们会发现cur与parent的平衡因子都变为了0。
当我们插入了这个结点以后,我们就需要控制平衡了,我们控制平衡的核心操作就是
parent->_right = cur->_left;
cur->_left = parent
这两步操作执行完成之后,parent的左右子树高度一定是相同的,所以parent的平衡因子为0,而且cur的左子树的高度也变为了h+1,右子树由于插入了一个值所以也是h+1,恰好平衡因子也为0了。所以这样一来,使得树的高度整体降了一个且树平衡了。
上面的操作固然是很重要很核心的两步,不过要注意,我们的是三叉链模型,所以我们还需要改变_parent的指向,否则关系会十分混乱。而且还需要去考虑parent所代表的子树是不是一颗完整的树还是说一个子树。如果是子树那么又是左子树还是右子树呢?这些都是需要去考虑的。不过在上面的过程中,我们会发现,需要改变的结点一共就三个:parent,cur,cur->left这三个结点的指针需要进行修改。其他的结点,他们原本的位置是哪里,现在的相对位置仍然不变。
上面这种旋转方式其实就是由于右子树高了,所以需要向左旋转,即将一个结点给左边。我们将其称之为左旋这样的话,就能保证在保证它是搜索树的条件下,还能降低这个子树的高度了
5.AVL树的左旋图
如上是我们在左旋的情况下的抽象图,为什么要用一个抽象图呢?这是因为我们前面对于左旋的分析并不能完整的表示出左旋的全部情况。h可以是任意值
在我们插入一个新的结点之前,a/b/c都是符合AVL规则的子树
当h为0的时候。
当h为1的时候
以此类推种类会随着h的增大而变多,所以我们用那个图来表示,接下来是代码实现环节:
void RotateL(Node* parent){Node* pparent = parent->_parent;Node* subR = parent->_right;Node* subRL = subR->_left;//修改parentparent->_right = subRL;parent->_parent = subR;//修改subRLif (subRL){subR->_parent=parent}//修改subRsubR->_left = parent;subR->_parent = pparent;//修改pparent与subRif (pparent == nullptr){_root = sub;}else {if (pparent->_left == parent){pparent->_left = cur;}else if (pparent->_right == parent){pparent->_right = subR;}}parent->_bf = sub->_bf = 0;}
};
6.AVL树的右旋图
讲完了左旋图,我们来讲讲右旋,右旋的抽象图和左旋很相似
与左旋类似,它的三颗子树高度必须满足高度一样,否则就不是AVL树了,或者说插入一个结点之后还是平衡的。
当h==0时:
当h==1时
以此类推种类会随着h的增大而变多,所以我们用那个图来表示,接下来是代码实现环节:
void RotateR(Node* parent){Node* sub = parent->_left;Node* subR = cur->_right;Node* pparent = parent->_parent;parent->_left = curR;parent->_parent = cur;if (subR){subR->_parent = parent;}sub->_right = parent;sub->_parent = pparent;if (pparent == nullptr){_root = sub;}else{if (pparent->_left == parent){pparent->_left = sub;}else if (pparent->_right == parent){pparent->_right = sub;}}parent->_bf = sub->_bf = 0;}
7. AVL树的双旋
我们在前面的抽象图中,主要讨论的是如果是右子树高,继续往右子树的右孩子插入的话,就会诱发左旋
但是如果我们像上面这种方式去左旋,我们会旋不明白,对吧,有没有人发现无论我们怎么单旋都是不行的!这时我们需要引入一个新的旋转思路,也就是双旋。
遇到这种情况我们不会?没事,我们可以把上面的这种图转化成我们熟悉的样子:
比如我们先对60这边进行右旋然后我们发现熟悉的左旋,于是再对b左旋
8.AVL树的右左旋
当h==0的时候,即60这个结点就是新插入的结点,他的旋转图如下
当h==1的时候,这个结点可以在60的左边或者右边插入,都是满足条件的。因为都属于右凸出折线插入。仍然是先以90为旋转点进行右旋,然后以30为旋转点进行左旋。可见树平衡
当h==2的时候, 每个子树又呈现出了不同的情况
然而a和d一定是x/y/z中的任意一种,中间这颗子树一定是z形状的。由于我们已经进行了细分,所以中间这棵树我们直接画出具象图
变化共3*3*4=36种,但是无论是如何的变化,我们都无需担心,因为只要我们先对90进行右旋,然后对30进行左旋,就一定可以使得这棵树变为平衡。
9. AVL树的右左双旋的本质
关于右左双旋,我们似乎可以直接复用前面的左旋和右旋的代码。但是这样做的话会出现问题的。因为只是左旋和右旋的话会导致平衡因子被改变为0,但是实际上,平衡因子不为0。也就是说平衡因子需要再次处理一下。
我们在分析一下右左双旋,当h==1的时候,我们当时可以插入在左边或者右边都是可以的
60的左边变成了30的右边,60的右边变成了90的左边,60变成了这棵树的根.
我们通过观察最终平衡因子的变化,似乎就区域于插入到了60的左边还是右边。即取决于插入后60的平衡因子。由于双旋的本质,所以最终插入结束以后,如果是插入到了60的左边,那么左边将被平衡为0,右边将被平衡为1。如果插入到了60的右边,左边被平衡为-1,右边被平衡为0。
如果60本身就是新插入的结点的话,那么最终三个结点都为0
如果插入到了60的左侧,那么最终的变化如下图所示
如果插入到了60的左侧,那么最终的变化如下图所示
10. AVL树的左右双旋
左右双旋与右左双旋是呈镜像关系的
void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 0){subR->_bf = 0;subRL->_bf = 0;parent->_bf = 0;}else if (bf == 1){subR->_bf = 0;subRL->_bf = 0;parent->_bf = -1;}else if (bf == -1){subR->_bf = 1;subRL->_bf = 0;parent->_bf = 0;}else{assert(false);}
}
void RotateLR(Node *parent)
{Node *subL = parent->_left;Node *subLR = subL->_right;int bf = subLR->_bf;RotateLeft(subL); RotateRight(parent); if (bf == 0){parent->_bf = subL->_bf = subLR->_bf = 0;}else if (bf == 1){subL->_bf = -1;parent->_bf = subLR->_bf = 0;}else if (bf == -1){parent->_bf = 1;subL->_bf = subLR->_bf = 0;}else{assert(false);}
}
11.AVL树的验证
1.验证有序
完成了平衡,那么我们这棵AVL树能正常使用嘛?让我们来验证一下吧!
在验证是否符合AVL树性质前,我们首先需要验证其是否是一棵二叉搜索树
在之前讲解二叉搜索树中提到过,如果中序遍历能够得到一个有序的序列,就说明是二叉搜索树
中序遍历代码如下:
void InOrder()
{_InOrder(_root);cout << endl;
}void _InOrder(Node *root)
{if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " "; // key/value模型,我们只打印key即可_InOrder(root->_right);
}
验证:
int main()
{AVLTree<int, int> t;int a[] = { 6, 3, 7, 11, 9, 26, 18, 14, 15 };for (auto e : a){t.Insert({ e, e });}t.InOrder();return 0;
}
运行结果:
2.验证平衡
我们可以利用AVL树的规则,写出如下的代码判断一棵树是否为AVL树
int _Height(Node* root){if (root == nullptr){return 0;}int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);return leftHeight > rightHeight ? 1 + leftHeight : 1 + rightHeight;}bool _IsBalance(Node* root){if (root == nullptr){return true;}int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);if ((rightHeight - leftHeight) != root->_bf){cout << "平衡因子异常,当前插入结点为 " << root->_kv.first << endl;return false;}return abs(rightHeight - leftHeight) < 2 && _IsBalance(root->_left) && _IsBalance(root->_right);}bool IsBalance(){return _IsBalance(_root);}
12. AVL树的删除
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与插入不同的是,删除节点后的平衡因子更新,如果更新后平衡因子是1或者-1,那么说明这棵树是不需要进行调整,如果平衡因子为0,说明原来是1或者-1,需要进行向上调整平衡因子。最差情况下一直要调整到根节点的位置 。他的更新策略与插入是相反的
13.AVL树的性能和代码
AVL树追求的是严格平衡,因此可以保证查找时高效的时间复杂度O(logN),但是如果我们需要频繁的对其进行旋转来维护平衡,一定程度上会影响效率,尤其是删除节点时的最差情况下我们可能需要一路旋转到根的位置。
相对于AVL树的严格平衡,红黑树则追求一种相对平衡,因此会略胜一筹,后面的文章中会对红黑树进行讲解。
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; //平衡因子AVLTreeNode(const pair<const K, V>& kv):_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv),_bf(0){}
};template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:bool insert(const pair<const K, V>& kv){if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){parent = cur;if (kv.first > cur->_kv.first)cur = cur->_right;else if (kv.first < cur->_kv.first)cur = cur->_left;elsereturn false;}cur = new Node(kv);if (kv.first > parent->_kv.first){parent->_right = cur;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}while (cur != _root){if (cur == parent->_left)parent->_bf--;elseparent->_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){if (cur->_bf == 1){RotateLeft(parent);}else if (cur->_bf == -1){RotateRL(parent);}}else{if (cur->_bf == 1){RotateLR(parent);}else if (cur->_bf == -1){RotateRight(parent);}}break;}else{assert(false);}}return true;}void RotateLeft(Node* parent) //新节点插入较高右子树的右侧:左单旋{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if(subRL)subRL->_parent = parent;Node* parentParent = parent->_parent;if (parent != _root){subR->_parent = parentParent;if (parent == parentParent->_left)parentParent->_left = subR;elseparentParent->_right = subR;}else{_root = subR;subR->_parent = nullptr;}subR->_left = parent;parent->_parent = subR;parent->_bf = subR->_bf = 0;}void RotateRight(Node* parent) //新节点插入较高左子树的左侧:右单旋{Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;Node* parentParent = parent->_parent;if (parent != _root){subL->_parent = parentParent;if (parent == parentParent->_left)parentParent->_left = subL;elseparentParent->_right = subL;}else{_root = subL;subL->_parent = nullptr;}subL->_right = parent;parent->_parent = subL;parent->_bf = subL->_bf = 0;}void RotateRL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateRight(subR);RotateLeft(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);}}void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateLeft(subL);RotateRight(parent);if (bf == 0){parent->_bf = subL->_bf = subLR->_bf = 0;}else if (bf == 1){subL->_bf = -1;parent->_bf = subLR->_bf = 0;}else if (bf == -1){parent->_bf = 1;subL->_bf = subLR->_bf = 0;}else{assert(false);}}void InOrder(){_InOrder(_root);cout << endl;}bool IsBalance(){return _IsBalance(_root);}int Height(){return _Height(_root);}size_t Size(){return _Size(_root);}Node* Find(const K& key){Node* cur = _root;while (cur){if (key > cur->_kv.first)cur = cur->_right;else if (key < cur->_kv.first)cur = cur->_left;elsereturn cur;}return nullptr;}
private:void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}bool _IsBalance(Node* root){if (root == nullptr)return true;int leftHeigit = _Height(root->_left);int rightHeight = _Height(root->_right);if (rightHeight - leftHeigit != root->_bf){cout << root->_kv.first << "平衡因子异常" << endl;return false;}return abs(rightHeight - leftHeigit) <= 1 && _IsBalance(root->_left)&& _IsBalance(root->_right);}int _Height(Node* root){if (root == nullptr)return 0;int higher = max(_Height(root->_left), _Height(root->_right));return higher + 1;}size_t _Size(Node* root){if (root == nullptr)return 0;return _Size(root->_left) + _Size(root->_right) + 1;}
private:Node* _root = nullptr;
};
如有错误欢迎指正