目录
1.AVL树(高度平衡搜索二叉树)定义
1.1平衡因子(Balance Factor简写成bf)
1.2avl树的定义
2.AVL树插入的基本操作
2.1逻辑抽象图
2.2插入流程
2.3左单旋
2.4右单旋
2.5右左双旋
2.6左右双旋
3.AVL树的检验技巧
3.1、检验依据
3.2、检验方法
3.3、AVL树的性能
1.AVL树(高度平衡搜索二叉树)定义
平衡二叉树 全称叫做 平衡二叉搜索(排序)树
,简称 AVL树。英文:Balanced Binary Tree (BBT),注:二叉查找树(BST)
由 前苏联 的两位数学家:G.M.Adelson-Velskii
和 E.M.Landis
共同提出,首次出现在 1962
发布的论文 《An algorithm for the organization of information》 中。
AVL树在二叉搜索树的的基础上增加了两个特性:
- 它的左右子树都是
AVL
树 - 左右子树的高度之差(平衡因子)的绝对值不超过
1
1.1平衡因子(Balance Factor简写成bf)
定义:节点的右子树减去左子树的高度,在 AVL树中,所有节点的平衡因子都必须满足: -1<=bf<=1;
1.2avl树的定义
AVL
树在原 二叉搜索树 的基础上添加了 平衡因子 bf
以及用于快速向上调整的 父亲指针 parent
,所以 AVL
树是一个三叉链结构:
上图我们描述了一棵树的构建每个节点应该具备的属性,我们要想构建一颗avl树,我们就要以上图为基准,先创建节点,代码如下:
template<class K,class V>
struct AVLTreeNode
{AVLTreeNode(const pair<K, V> kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0){}AVLTreeNode* _left;AVLTreeNode* _right; AVLTreeNode* _parent;std::pair<K, V>_kv;int bf; //平衡因子};
然后就是组织的过程,我们只要创建根节点,然后增删改查就OK了,我们要注意平衡因子的约束,比如我们要按顺序插入1 2 3 4 5 6有了平衡因子的约束,插入题如下所示:
由上图可知,同样的结点,由于插入方式不同导致树的高度也有所不同。特别是在带插入结点个数很多且正序的情况下,会导致二叉树的高度是O(N),而AVL树就不会出现这种情况,树的高度始终是O(lgN).高度越小,对树的一些基本操作的时间复杂度就会越小。这也就是我们引入AVL树的原因。
2.AVL树插入的基本操作
插入节点的基本操作跟搜索二叉树差不多,但是需要收到平衡因子的约束,下面是插入过程平衡因子的注意事项;
- 新增在左,parent平衡因子减减;
- 新增在右,parent平衡因子加加;
- 更新后parent平衡因子 == 0,说明parent所在的子树高度不变,不会影响祖先,不用再继续沿着root的路径网上更新;
- 更新后parent平衡因子 == 1 or -1,说明parent所在的子树的高度变化,会影响祖先,需要继续沿着到root往上更新;
- 更新后parent平衡因子 == 2 or -2,说明parent所在的子树高度变化且不平衡,对parent所在的子树进行旋转,让他平衡;
- 更新到root节点;
2.1逻辑抽象图
AVL
树的 旋转操作 比较复杂,需要考虑多种形状、多种情况,为了方便理解,将 部分节点 视为一个整体(抽象化),主要看高度 h
进行旋转操作,可以得出下面这个抽象图
通过逻辑图可以方便我们对avl树插入的情况做清晰的分析。
2.2插入流程
- 判断根是否为空,如果为空,则进行第一次插入,成功后返回 true
- 找到合适的位置进行插入,如果待插入的值比当前节点值大,则往 右 路走,如果比当前节点值小,则往 左 路走
- 判断父节点与新节点的大小关系,根据情况判断链接至 左边 还是 右边
- 更新平衡因子,然后判断是否需要进行 旋转 调整高度
代码框架如下,具体旋转情况具体分析实现
//插入节点
bool Insert(const std::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;}elsereturn false;}//创建新节点,链接cur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}//根据平衡因子判断是否需要旋转while (parent){//更新平衡因子if (parent->_right == cur)parent->_bf++;elseparent->_bf--;//判断是否需要调整//……}return true;
}
2.3左单旋
左单旋的适用场景如下:在根的右子树中出现 平衡因子 为 1
的情况下,仍然往右侧插入节点,插入后会导致 右子树 中某个节点 平衡因子 值为 2
,此时就需要使用 左单旋 降低高度。
在Z的右枝插入节点会导致z的平衡因子++,R的平衡因子会增加成2,则不满足avl树的要求,我们就要对R节点左旋,具体代码如下:
//左单旋
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;//先将 subR 的左孩子移交给父亲parent->_right = subRL;if (subRL != nullptr)subRL->_parent = parent;Node* pparent = parent->_parent;//易错点:忘记更改原父亲的链接关系subR->_left = parent;parent->_parent = subR;//再将父亲移交给 subR,subR 成为新父亲if (parent == _root){//如果原父亲为根,那么此时需要更新 根subR->_parent = nullptr;_root = subR;}else{//单纯改变链接关系if (pparent->_right == parent)pparent->_right = subR;elsepparent->_left = subR;subR->_parent = pparent;}//更新平衡因子parent->_bf = subR->_bf = 0;
}
旋转其实就是改变链接过程,更改的链接示意图如下
助记:搜索二叉树的右数>左树 则 1.parent->right = cur->left 2.cur->left = parent
注意: subRL
可能是 nullptr
,在改变其链接关系时,需要判断一下,避免空指针解引用行为;parent
可能是 根节点,subR
在链接后,需要更新 根节点;左单旋后,parent
、subR
的平衡因子都可以更新为 0
,此时是很平衡的
2.4右单旋
右单旋的适用场景如下:在根的左子树中出现 平衡因子 为 1
的情况下,仍然往左侧插入节点,插入后会导致 左子树 中某个节点 平衡因子 值为 2
,此时就需要使用 右单旋 降低高度
右单旋 的场景与 左单旋 如出一辙,不过方向不同而已
右单选的代码,复制左单旋改变链接指向就行
//右单旋
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;//先将 subL 的右孩子移交给父亲parent->_left = subLR;if (subLR != nullptr)subLR->_parent = parent;Node* pparent = parent->_parent;subL->_right = parent;parent->_parent = subL;//再将父亲移交给 subL,subL 成为新父亲if (parent == _root){//如果原父亲为根,那么此时需要更新 根subL->_parent = nullptr;_root = subL;}else{//单纯改变链接关系if (pparent->_right == parent)pparent->_right = subL;elsepparent->_left = subL;subL->_parent = pparent;}//更新平衡因子parent->_bf = subL->_bf = 0;
}
助记:搜索二叉树的右数>左树 则 1.parent->left = cur->right 2.cur-> right = parent
注意: subLR
可能是 nullptr
,在改变其链接关系时,需要判断一下,避免空指针解引用行为;parent
可能是 根节点,subL
在链接后,需要更新 根节点;右单旋后,parent
、subLR
的平衡因子都可以更新为 0
,此时是很平衡的
2.5右左双旋
当值插入 右子树的右侧 时,可能引发 左单旋,当值插入 左子树的左侧 时,则可能引发 右单旋
如果插入的是 右子树的左侧 或 左子树的右侧 时,则可能引发 双旋
比如 插入右子树的左侧 时,单单凭借 左单旋 无法解决问题,需要 先进行 右单旋,再进行 左单旋 才能 降低高度,这一过程就成为 双旋(右左双旋)
//右左双旋
void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int BF = subRL->_bf;//先右单旋RotateR(subR);//再左单旋RotateL(parent);//根据不同的情况更新平衡因子if(BF == 0){parent->_bf = subR->_bf = 0;}else if (BF == 1){parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else if (BF == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else{//非法情况std::cerr << "此处的平衡因子出现异常!" << std::endl;assert(false); //直接断言报错}
}
右左双旋 的抽象图 旋转 流程如下(动图)
注:双旋 部分的动图省略了部分细节,着重展现 高度降低 的现象
右左双旋 逻辑:
确定 parent、subR、subRL
将 subRL 的右子树托付给 subR,左子树托付给 parent
subRL 向上提,整体高度下降
需要特别注意平衡因子的调整
双旋 的 平衡因子 调整需要分类讨论:
情况一:新节点插入至右子树左侧后,subRL 平衡因子变为 0,此时树变得更加平衡了,因此 parent、subR、subRL 三者的平衡因子都为 0
情况二:新节点插入至右子树的左侧后,subRL 平衡因子变为 -1,证明 新节点插入至 subRL 的左边,并且右边没有东西,旋转后,将新节点托付给 parent 后,parent 变得平衡了,但 subR 因没有分到节点,因此导致其左侧失衡,平衡因子变为 1,subRL 平衡,为 0(这其实就是动图展示的情况)
情况三:新节点插入至右子树的左侧后,subRL 平衡因子变为 1,证明 新节点插入至 subRL 的右边,并且左边没有东西,旋转后,parent 没有分到节点,subR 分到了,subRL 为平衡,因此 parent 的平衡因子为 -1,subR 和 subRL 的平衡因子都是 0
经过这样分析后,就能得到代码中的判断逻辑
注意: 先要右单旋,才左单旋;平衡因子的更新需要分类讨论
2.6左右双旋
当节点插入至 左子树的右侧 时,会触发 左右双旋,需要 先进行 左单旋,再进行 右单旋 才能降低高度
//左右双旋
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int BF = subLR->_bf;//先左单旋RotateL(subL);//再右单旋RotateR(parent);//根据不同的情况更新平衡因子if (BF == 0){parent->_bf = subL->_bf = 0;}else if (BF == 1){parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (BF == -1){parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else{//非法情况std::cerr << "此处的平衡因子出现异常!" << std::endl;assert(false); //直接断言报错}
}
左右双旋 的 旋转 流程如下图所示(动图)
左右双旋 逻辑:
确定 parent、subL、subLR
将 subLR 的右子树托付给 parent,左子树托付给 subL
subLR 向上提,整体高度下降
需要特别注意平衡因子的调整
调整逻辑与 右左双旋 差不多
情况一:新节点插入至左子树右侧后,subLR 平衡因子变为 0,此时树变得更加平衡了,因此 parent、subL、subLR 三者的平衡因子都为 0
情况二:新节点插入至左子树的右侧后,subLR 平衡因子变为 -1,证明 新节点插入至 subLR 的左边,并且右边没有东西,旋转后,将新节点托付给 subL 后,subL 变得平衡了,但 parent 因没有分到节点,因此导致其右侧失衡,平衡因子变为 1,subLR 平衡,为 0
情况三:新节点插入至左子树的右侧后,subLR 平衡因子变为 1,证明 新节点插入至 subLR 右边,并且左边没有东西,旋转后,subL 没有分到节点,parent 分到了,subLR 为平衡,因此 subL 的平衡因子为 -1,parent 和 subLR 的平衡因子都是 0(动图中演示的就是情况三)
总的来说,双旋 需要慎重考虑 平衡因子 的调整
小结:旋转情况:
- 插入至
右右
时,左单旋 - 插入至
左左
时,右单旋 - 插入至
右左
时,右左双旋 - 插入至
左右
时,左右双旋
3.AVL树的检验技巧
3.1、检验依据
如何检验自己的 AVL 树是否合法? 答案是通过平衡因子检查
平衡因子 反映的是 左右子树高度之差,计算出 左右子树高度之差 与当前节点的 平衡因子 进行比对,如果发现不同,则说明 AVL 树 非法或者如果当前节点的 平衡因子 取值范围不在 [-1, 1] 内
3.2、检验方法
统计 二叉树子树高度 很简单,只需要在 检验合法性函数 中调用即可
//验证是否为 AVL 树
bool IsAVLTree()
{return _IsAVLTree(_root);
}//获取高度
size_t getHeight()
{return _getHeight(_root);
}bool _IsAVLTree(Node* root)
{if (root == nullptr)return true;//计算左右子树的高度size_t leftTreeH = _getHeight(root->_left);size_t rightTreeH = _getHeight(root->_right);//计算差值int diff = rightTreeH - leftTreeH;if (diff != root->_bf || root->_bf < -1 || root->_bf > 1){std::cerr << "当前节点出现了问题: " << root->_kv.first<< " | " << root->_bf << std::endl;return false;}return _IsAVLTree(root->_left) && _IsAVLTree(root->_right);
}size_t _getHeight(Node* root)
{if (root == nullptr)return 0;size_t leftH = _getHeight(root->_left);size_t rightH = _getHeight(root->_right);return 1 + std::max(leftH, rightH);
}
通过一段简单的代码,随机插入 10000
个节点,判断 是否合法 及当 AVL
树的 高度
void AVLTreeTest2()
{srand((size_t)time(NULL));AVLTree<int, int> av;for (int i = 0; i < 10000; i++){int val = rand() % 10000 + i;av.Insert(val, val);}cout << "检查AVL树: " << av.IsAVLTree() << endl << "高度为:" << av.getHeight() << endl;
}
3.3、AVL树的性能
AVL 树是一棵 绝对平衡 的二叉树,对高度的控制极为苛刻,稍微有点退化的趋势,都要被旋转调整,这样做的好处是 严格控制了查询的时间,查询速度极快,约为 logN
但是过度苛刻也会带来一定的负面影响,比如涉及一些 结构修改 的操作时,性能非常低下,更差的是在 删除 时,因为从任意位置破坏了 二叉搜索树 及 AVL 树的属性,有可能会引发连锁旋转反应,导致一直 旋转 至 根 的位置(旋转比较浪费时间)
AVL 树性能很优秀,如果在存储大量不需要修改的静态数据时,用 AVL 树是极好的,但在大多数场景中,用不到这么极限的性能,此时就需要一种 和 AVL 树差不多,但又没有那么严格 的 平衡二叉搜索树 了