目录
1. AVL概念
2. AVL树节点的定义
3. AVL树的插入
4. AVL树的旋转
5. AVL树的验证
6. AVL树的删除
7. AVL树的性能
暴力搜索、二分搜索、二叉搜索树、二叉平衡搜索树(AVL、红黑树)、多叉平衡搜索树(B树)、哈希表
1. AVL概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差 (简称平衡因子) 的绝对值不超过1 (-1 / 0 / 1) ,不是绝对的0是因为做不到绝对的平衡(例如,非满二叉树)
平衡因子:右子树高度 - 左子树高度
满二叉树:2^h - 1 = N-> 约等于logNAVL树:2^h - X = N (X范围 [ 1, 2^(h - 1) - 1 ] )
2. AVL树节点的定义
直接写key—value版本,因为它与单key版本,只是多了一个对应value(pair就解决了),可以看二叉搜索树一文再理解两个版本的关系
加入平衡因子会更加方便
template<class K, class V>
struct AVLTreeNode
{int _bf; //balance factorpair<K, V> _kv;AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;AVLTreeNode(const pair<K, V>& kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}
};
3. AVL树的插入
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:
-
按照二叉搜索树的方式找到合适点插入新节点
-
调整节点的平衡因子
- 新增在左边,parent平衡因子--
- 新增在右边,parent平衡因子++
- 如果插入后,节点的平衡因子变成了0,那么说明这个节点的左右子树高度平衡了,高度没有增大或减小,所以不会对往上的祖先产生影响,就不需要往上更新祖先的平衡因子
- 如果插入后,节点的平衡因子变成了1 或 -1,那么说明这个节点的左右子树高度不平衡,高度增大了或减小了,会对其祖先的平衡因子产生影响,所以使用 parent 指针向上更新平衡因子(这就体现了 parent 指针的用武之地),直到某一个祖先的平衡因子变为 0 或根节点就停止!!!(因为如果一个节点的高度没变,即平衡因子为0,那么它就不影响parent节点)
- 如果插入后,节点的平衡因子变成了2 或 -2,说明parent所在的子树的高度变化且不平衡了!这时就要体现 AVL 平衡的特点,对 parent 所在子树进行旋转
- 不会出现3 或 -3以上情况,因为它们必然会经历2 或 -2的情况,那时我们早就已经处理完了
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->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;// ... 控制平衡// 更新平衡因子while (parent){if (cur == parent->_left){parent->_bf--;}else // if (cur == parent->_right){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){RotateRL(parent);}else if (parent->_bf == -2 && cur->_bf == 1){RotateLR(parent);}break;}else //正常代码不会执行到这里,所以抛异常{assert(false);}}return true;}
4. AVL树的旋转
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
新节点插入较高右子树的右侧 --- 右右:左单旋
cur 的 left 链接到 parent 的 right,再将 parent 链接到 cur 的 left
在操作时,不仅要更新_bf平衡因子,还要额外注意父节点指针,一不留心就会出现三叉链,而且还要注意,parent 是不是 root,如果不是根节点,那么还要提前记录 parent 的 parent,以便使cur 的 parent 指向修改
符合左单旋的场景无穷尽,下图直接概括了左单旋的抽象总结图:
但是无论h高度是多少,节点的操作是一样的
//左单旋void RotateL(Node* parent){++_rotateCount;Node* cur = parent->_right;Node* curleft = cur->_left;//开始链,注意父亲链parent->_right = curleft;if (curleft) //这里判断cur的right是不是为空,为空的话就不能使用指针链接{curleft->_parent = parent;}cur->_left = parent;Node* ppnode = parent->_parent;parent->_parent = cur;//判断parent的父亲的情况if (parent == _root){_root = cur;cur->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = cur;}else{ppnode->_right = cur;}//记得父亲链cur->_parent = ppnode;}//不要忘了平衡因子parent->_bf = cur->_bf = 0;}
新节点插入较高左子树的左侧---左左:右单旋
简单分析后即可总结出上图:
//右单旋void RotateR(Node* parent){++_rotateCount;Node* cur = parent->_left;Node* curright = cur->_right;//开始链,注意父亲链parent->_left = curright;if (curright) //这里判断cur的right是不是为空,为空的话就不能使用指针链接{curright->_parent = parent;}Node* ppnode = parent->_parent;cur->_right = parent;parent->_parent = cur;//判断parent的父亲的情况if (ppnode == nullptr){_root = cur;cur->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = cur;}else{ppnode->_right = cur;}//记得父亲链cur->_parent = ppnode;}//不要忘了平衡因子parent->_bf = cur->_bf = 0;}
新节点插入较高右子树的左侧--- 右左:先右单旋再左单旋
//右左双旋void RotateRL(Node* parent){Node* cur = parent->_right;Node* curleft = cur->_left;int bf = curleft->_bf;RotateR(parent->_right);RotateL(parent);//分情况更新平衡因子if (bf == 0){cur->_bf = 0;curleft->_bf = 0;parent->_bf = 0;}else if (bf == 1){cur->_bf = 0;curleft->_bf = 0;parent->_bf = -1;}else if (bf == -1){cur->_bf = 1;curleft->_bf = 0;parent->_bf = 0;}else{assert(false);}}
代码解析:在插入一个节点之后,向上更新平衡因子,直到遇见以上四种情况,开始旋转处理
新节点插入较高左子树的右侧 --- 左右:先左单旋再右单旋
//左右双旋void RotateLR(Node* parent){Node* cur = parent->_left;Node* curright = cur->_right;int bf = curright->_bf;RotateL(parent->_left);RotateR(parent);//分情况更新平衡因子if (bf == 0){parent->_bf = 0;cur->_bf = 0;curright->_bf = 0;}else if (bf == -1){parent->_bf = 1;cur->_bf = 0;curright->_bf = 0;}else if (bf == 1){parent->_bf = 0;cur->_bf = -1;curright->_bf = 0;}}
如果程序出错了,数据量很大调试就很难受了,这时候可以一段一段注释调试,对于不同场景可能不合适,所以还可以手写一个判断函数,让计算机协助你找到错误,打开监视窗口、打断点,要带着思路、有预测结果的去调试,中间可以使用条件断点(可以手写if,里面写一些语句,程序走到这里时停下)
总结:
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
- pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR,当pSubR的平衡因子为1时,执行左单旋;当pSubR的平衡因子为-1时,执行右左双旋
- pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL,当pSubL的平衡因子为-1是,执行右单旋;当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
5. AVL树的验证
//递归求高度,测试代码是否正确int Height(){return Height(_root);}int Height(Node* root){if (root == nullptr)return 0;int leftHeight = Height(root->_left);int rightHeight = Height(root->_right);return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;}//测试代码是否正确,是不是AVL结构bool IsBalance(){return IsBalance(_root);}bool IsBalance(Node* root){if (root == nullptr)return true;int leftHight = Height(root->_left);int rightHight = Height(root->_right);if (rightHight - leftHight != root->_bf){cout << "平衡因子异常:" << root->_kv.first << "->" << root->_bf << endl;return false;}return abs(rightHight - leftHight) < 2&& IsBalance(root->_left)&& IsBalance(root->_right);}
我们使用随机生成的数来测试
#include "AVLTree.h"
#include <vector>const int N = 1000000;int main()
{AVLTree<int, int> at;vector<int> v;v.reserve(N);srand(time(0));for (int i = 0; i < N; i++){v.push_back(rand());}for (auto e : v){at.Insert(make_pair(e, e));}cout << at.IsBalance() << endl;return 0;
}
6. AVL树的删除
按搜索树的规则查找节点进行删除,与插入时对节点的平衡因子更新相反,如果删除节点之后 parent 平衡因子为0,那么需要继续往上更新,如果是 1 或 -1,那么不需要往上更新
而且删除时,双旋情况更多,平衡因子的更新更复杂
7. AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 logN 但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合