万字手撕AVL树 | 上百行的旋转你真的会了吗?【超用心超详细图文解释 | 一篇学会AVL】
说在前面
今天这篇博客,是博主今年以来最最用心的一篇博客。我们也很久没有更新数据结构系列了,几个月前博主用心深入的学习了这颗二叉平衡搜索树,博主被它的查找效率深深吸引。
AVL树出自1962年中的一篇论文《An_algorithm_for_the_organization_of_information》,它解决了普通二叉搜索树退化的问题,这个博主稍后会详细解释。AVL树放到现在,它的查找效率是很少数据结构能够比拟的。
博主为了这篇博客,做了很多准备,试了很多画图软件,就是为了让大家看得明白!希望大家不要吝啬一键三连啊!!
前言
那么这里博主先安利一下一些干货满满的专栏啦!
这里包含了博主很多的数据结构学习上的总结,每一篇都是超级用心编写的,有兴趣的伙伴们都支持一下吧!手撕数据结构
https://blog.csdn.net/yu_cblog/category_11490888.html?spm=1001.2014.3001.5482
这里是STL源码剖析专栏,这个专栏将会持续更新STL各种容器的模拟实现。算法专栏
https://blog.csdn.net/yu_cblog/category_11464817.html
STL源码剖析
https://blog.csdn.net/yu_cblog/category_11983210.html?spm=1001.2014.3001.5482
什么是AVL树?
tips:博主在最后会放一份整体代码,供大家参考!
首先,它是一颗二叉搜索树。
什么是二叉搜索树:
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
如图所示:这就是一颗二叉搜索树。
当我们需要在这颗搜索树里面查找节点4的时候,我们从3开始,比3大,往右走,比5小,往左走。这样我们就找到了4这个节点。
一颗二叉搜索树最高的查找复杂度是O(h),h为树的高度,这样其实我们也可以得到较好的查找效率了。但是,搜索树可能会退化。如图所示:
比如第一棵树,我们如果要查找最下面那个节点,我们要找n次。比如第二棵树,我们找最下面节点要找n/2次,假设我们要查10亿个数据,我们要找5亿次,其实还是O(n)级别。在这种情况下,查找就没有优势了。导致这种情况的主要原因就是,高度不平衡!我们设想一颗满二叉树,它就是平衡的,我们查找一个值,即高度次,复杂度是O(logn)。因此我们需要在插入的同时,不断变换这颗树,使它的高度平衡,这样我们的查找性能才能得到质的提升!一颗二叉平衡搜索树,在10亿个数据中查找一个值,最多查找31次,这种优化,是非常大的!
因此我们引出二叉平衡搜索树,常见的二叉平衡搜索树有AVL树和红黑树。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
- 平衡因子 = 右子树高度 - 左子树高度
AVL树的结构定义
AVL树通常使用三叉链进行构造,我们在处理指针的时候要记得_parent也要处理。
template<class K,class V>
struct AVLNode {
public:
AVLNode<K, V>* _left;
AVLNode<K, V>* _right;
AVLNode<K, V>* _parent;
pair<K, V>_kv;
int _bf; //balance factor
public:
AVLNode(const pair<K, V>& kv)
:_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0) {}
};
template<class K, class V>
struct AVL {
typedef AVLNode<K, V>Node;
private:
Node* _root = nullptr;
public:
//成员函数
//...
}
AVL树节点的插入(重点)
AVL树的节点插入可以分为三个步骤:
- 新节点的插入
- 平衡因子的更新
- 通过平衡因子确定平衡性是否被打破,若平衡性被打破,进行旋转
AVL树的旋转:
- 左单旋
- 右单旋
- 左右双旋
- 右左双旋
AVL树的旋转是本篇博客最最最最最重点的地方,博主将会详细解释这部分!
一、新节点的插入
新节点的插入步骤和普通二叉搜索树的插入步骤一样,找到插入的位置,直接插入即可!
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;
//先更新平衡因子
//...
//平衡因子更新后,判断是否需要旋转
return true;
}
二、更新平衡因子
当一个新节点被插入之后,该节点的祖先路径上的节点有可能会受到影响,我们要看情况进行更新。如图所示:插入节点后,只会影响祖先路径上节点的平衡因素。
我们只需要利用_parent指针,不断迭代向上就行了。
- 如果插入在「新节点父亲」的右边,父亲的平衡因子++
- 如果插入在「新节点父亲」的左边,父亲的平衡因子 - -
tips:博主在最后会放一份整体代码,供大家参考!
如果平衡因子更新后是1/-1,说明子树的高度被改变了,需要继续向上迭代。如果平衡因子更新后是0,说明新节点只是把子树不齐的地方补齐了(这个很好理解,如果大家不明白可以简单画个图),如果更新后平衡因子是2/-2,说明平衡被打破了,需要旋转
while (parent) {//只有根没有父亲
//最坏可能需要更新到根
if (cur == parent->_right) {
parent->_bf++;
}
else {
parent->_bf--;
}
if (parent->_bf == 0) {
//高度不变 -- 停止更新
break;
}
else if (parent->_bf == 1 || parent->_bf == -1) {
//继续更新
parent = parent->_parent;
cur = cur->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2) {
//说明parent所在的子树已经不平衡了 -- 需要旋转
}
else {
assert(false);//理论上不能走到这里
}
}
三、旋转
AVL树的旋转:
- 左单旋
- 右单旋
- 左右双旋
- 右左双旋
首先我们来看几个例子,大概了解一下旋转是一个什么样的操作,看下旋转式怎么让不平衡的树便平衡的:
这里只展示了旋转中的其中一种情况,左单旋,现在博主将分四种情况给大家详细讲解
左单旋:新节点插入较高右子树的右侧
大家注意,这里的触发条件是,较高的是右子树,而且插入的是在该右子树的右侧
其实就是把30拿下来,60替代它的位置即可
此时我们要注意,这里我们发现平衡因子异常的点,也就是30这个节点,它不一定是整棵树的根,它有可能只是一个子树的根,但是在旋转过程中,我们不需要关心它上面的结构是什么,我们旋转完成之后,重新链接上去就行了.
给重要的节点标上名字,怎么旋转的,我们就一目了然了,因为是三叉链条,我们直接操作指针即可!
我们把思路转化成代码:
首先,右右触发左单旋,即parent->_bf==2&&cur->_bf==1(在上图中cur是parent的右孩子),这个时候触发左单旋.
if (parent->_bf == 2 && cur->_bf == 1) {
//注意这里肯定是bf==1的情况 -- 才是单旋
//parent->_bf==2说明是左单旋
rotate_left(parent);//旋转就动了6个指针 -- O(1)
}
void rotate_left(Node* parent) {
//当然我们还要注意处理parent指针
//parent和subR不可能为空 -- ,但是subRL可能为空
//1.parent是整颗树根
//2.parent是子树的根
//最后更新一下平衡因子
//只有subR和parent的平衡因子受到了影响
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL) {
subRL->_parent = parent;
}
Node* ppNode = parent->_parent;//记录一下原先parent的parent
subR->_left = parent;
parent->_parent = subR;
if (_root == parent) {
_root = subR;
subR->_parent = nullptr;
}
else {
//如果ppNode==nullpt,是不会进来这里的
if (ppNode->_left == parent) {
ppNode->_left = subR;
}
else {
ppNode->_right = subR;
}
subR->_parent = ppNode;
}
//更新一下平衡因子
subR->_bf = parent->_bf = 0;//这个看图就行了
}
右单旋:新节点插入较高左子树的左侧
大家注意,这里的触发条件是,较高的是左子树,而且插入的是在该左子树的左侧
这里其实就是左单旋的一个镜像,博主把图画给大家,代码相信我们已经可以自己写出来了
由图片我们可以得知,触发条件时parent->_bf==-2&&cur->_bf==-1
代码如下:
else if (parent->_bf == -2 && cur->_bf == -1) {
//右单旋
rotate_right(parent);
}
//右单旋 -- 思路和左单旋是镜像 -- 很简单
void rotate_right(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) {
subLR->_parent = parent;
}
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (_root == parent) {
_root = subL;
subL->_parent = nullptr;
}
else {
if (ppNode->_left == parent) {
ppNode->_left = subL;
}
else {
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
//更新平衡因子
subL->_bf = parent->_bf = 0;
}
双旋:
看到这里,难道这两种单旋就可以解决所有问题了吗?答案是否定的,我们举个例子:
我们可以看到这种情况,无论怎么单旋,我们都不能使树便平衡.
由此我们引出双旋:
左右双旋:新节点插入较高左子树的右侧:先左旋后右旋
双旋的过程会比单旋的过程复杂一些
其中,左右双旋分别有三种插入的情况,如图所示:
其中情况3:h==0,此时60就是新插入的节点
下面是旋转的过程:
相信上面的图已经把旋转过程解释得非常清晰了,其实就是两次单旋的组合
现在我们要重点讨论旋转后的平衡因子的更新:
我们可以发现最后的平衡因子取决于初始状态下subLR的平衡因子
情况1: subLR->_bf==-1
- 旋转后parent,subL,subLR的平衡因子分别为1,0,0
情况2: subLR->_bf==1
- 旋转后parent,subL,subLR的平衡因子分别为0,-1,0
情况3: subLR->_bf==0
- 旋转后parent,subL,subLR的平衡因子分别为0,0,0
现在我们只需要对照着图片,对照着调整后的平衡因子,就可以很快的写出代码:
首先,通过图片我们可以知道,左右双旋的触发条件是:
parent->_bf == -2 && cur->_bf == 1
else if (parent->_bf == -2 && cur->_bf == 1) {
//左右双旋
rotate_left_right(parent);
}
void rotate_left_right(Node* parent) {
//要在单旋之前记录一下,因为单旋之后平衡因子会变
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;//记录一下subLR的平衡因子
rotate_left(parent->_left);//先最左边进行一个左旋
rotate_right(parent);//再对自己进行一个右旋转
//如何区分三种情况的平衡因子更新呢?
subLR->_bf = 0;//一定要画图!三种情况的subLR最终都是0
if (bf == 1) {
//情况1
parent->_bf = 0;
subL->_bf = -1;
}
else if (bf == -1) {
//情况2
parent->_bf = 1;
subL->_bf = 0;
}
else if (bf == 0) {
//情况3
parent->_bf = 0;
subL->_bf = 0;
}
else assert(false);
}
右左双旋:新节点插入较高右子树的左侧:先右旋后左旋
同样,右左双旋也有三种情况.
右左双旋其实就是左右双旋的一个镜像,搞明白了左右双旋,右左双旋直接画一下图,总结一下三种情况的平衡因子,直接写代码就行了.
触发条件:
parent->_bf == 2 && cur->_bf == -1
else if (parent->_bf == 2 && cur->_bf == -1) {
//右左双旋
rotate_right_left(parent);
}
void rotate_right_left(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
rotate_right(parent->_right);
rotate_left(parent);
subRL->_bf = 0;
if (bf == 1) {
subR->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1) {
subR->_bf = 1;
parent->_bf = 0;
}
else if (bf == 0) {
subR->_bf = 0;
parent->_bf = 0;
}
else assert(false);
}
四、AVL树的检验
写到这里,我们就可以尝试插入一些节点,检查AVL树是否平衡了
当然,我们可以通过调试,打断点去检查这棵树,但是这样很麻烦.
同时,我们是不能通过检查中序遍历是否有序去判断AVL树的合法性的.因为所有的搜索树中序遍历都是有序的.
我们要通过AVL树的性质去检查,检查每颗子树的左右子树高度差是否小于等于1:
当然现在写出这种代码对于我们来说其实很简单了,这里也提供一道里扣题的传送门,其实就是AVL树的检验,大家可以顺便完成它
面试题 04.04. 检查平衡性https://leetcode.cn/problems/check-balance-lcci/这里博主也一起提供中序遍历的代码:
class AVL {
//...
//...
//...
public:
void inorder() {
_inorder(this->_root);
}
bool is_balance() {
return _is_balance(this->_root);
}
private:
void _inorder(Node* root) {
if (root == nullptr) {
return;
}
_inorder(root->_left);
cout << (root->_kv).first << ":" << (root->_kv).second << endl;
_inorder(root->_right);
}
int _height(Node* root) {
if (root == nullptr)return 0;
int leftHT = _height(root->_left);
int rightHT = _height(root->_right);
return max(leftHT, rightHT) + 1;
}
bool _is_balance(Node* root) {
if (root == nullptr)return true;
int leftHT = _height(root->_left);//左子树高度
int rightHT = _height(root->_right);//右子树高度
int diff = rightHT - leftHT;
//把平衡因子也检查一下
if (diff != root->_bf) {
cout << root->_kv.first << "的平衡因子异常" << endl;
return false;
}
return abs(diff) < 2
&& _is_balance(root->_left)//判断一下左子树是否平衡
&& _is_balance(root->_right);//判断一下右子树是否平衡
}
}
五、删除等接口
讲到这里有伙伴可能会问,为什么讲AVL,不讲删除那些接口呢?
因为,校招,公司面试,以后工作中都基本不会考察到AVL树的删除接口,红黑树也是一样,我们只需要掌握插入接口就行了.
AVL树,红黑树我们都是做了解性学习,我们并不需要去手撕它的全部代码,这样时间成本很大,意义不大.我们学习AVL树,我们需要深入的去理解它的结构,学习一个插入接口,我们已经可以很好的做到这一点了.
这里博主大概讲一下删除的步骤:
一开始也是像搜索树一样找到要删除的节点,用替换法删除,删除后也是同样,检查平衡性.如果平衡性被破坏,进行旋转,这个过程其实就大概是插入的反方向操作
六、AVL.h整体代码
#pragma once
#include<map>
#include<set>
#include<algorithm>
#include<assert.h>
#include<time.h>
using namespace std;
template<class K,class V>
struct AVLNode {
public:
AVLNode<K, V>* _left;
AVLNode<K, V>* _right;
AVLNode<K, V>* _parent;
pair<K, V>_kv;
int _bf; //balance factor
public:
AVLNode(const pair<K, V>& kv)
:_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0) {}
};
//如何更新平衡因子
//如何旋转
template<class K, class V>
struct AVL {
typedef AVLNode<K, V>Node;
private:
Node* _root = nullptr;
private:
//左单旋
void rotate_left(Node* parent) {
//当然我们还要注意处理parent指针
//parent和subR不可能为空 -- ,但是subRL可能为空
//1.parent是整颗树根
//2.parent是子树的根
//最后更新一下平衡因子
//只有subR和parent的平衡因子受到了影响
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL) {
subRL->_parent = parent;
}
Node* ppNode = parent->_parent;//记录一下原先parent的parent
subR->_left = parent;
parent->_parent = subR;
if (_root == parent) {
_root = subR;
subR->_parent = nullptr;
}
else {
//如果ppNode==nullpt,是不会进来这里的
if (ppNode->_left == parent) {
ppNode->_left = subR;
}
else {
ppNode->_right = subR;
}
subR->_parent = ppNode;
}
//更新一下平衡因子
subR->_bf = parent->_bf = 0;//这个看图就行了
}
//右单旋
void rotate_right(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) {
subLR->_parent = parent;
}
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (_root == parent) {
_root = subL;
subL->_parent = nullptr;
}
else {
if (ppNode->_left == parent) {
ppNode->_left = subL;
}
else {
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
//更新平衡因子
subL->_bf = parent->_bf = 0;
}
//左右双旋
void rotate_left_right(Node* parent) {
//要在单旋之前记录一下,因为单旋之后平衡因子会变
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;//记录一下subLR的平衡因子
rotate_left(parent->_left);//先最左边进行一个左旋
rotate_right(parent);//再对自己进行一个右旋转
//如何区分三种情况的平衡因子更新呢?
subLR->_bf = 0;//一定要画图!三种情况的subLR最终都是0
if (bf == 1) {
//情况1
parent->_bf = 0;
subL->_bf = -1;
}
else if (bf == -1) {
//情况2
parent->_bf = 1;
subL->_bf = 0;
}
else if (bf == 0) {
//情况3
parent->_bf = 0;
subL->_bf = 0;
}
else assert(false);
}
//右左双旋
void rotate_right_left(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
rotate_right(parent->_right);
rotate_left(parent);
subRL->_bf = 0;
if (bf == 1) {
subR->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1) {
subR->_bf = 1;
parent->_bf = 0;
}
else if (bf == 0) {
subR->_bf = 0;
parent->_bf = 0;
}
else assert(false);
}
public:
//我们先不返回pair,到时候我们封装map的时候在搞
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->_right) {
parent->_bf++;
}
else {
parent->_bf--;
}
if (parent->_bf == 0) {
//高度不变 -- 停止更新
break;
}
else if (parent->_bf == 1 || parent->_bf == -1) {
//继续更新
parent = parent->_parent;
cur = cur->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2) {
//说明parent所在的子树已经不平衡了 -- 需要旋转
//左旋
if (parent->_bf == 2 && cur->_bf == 1) {
//注意这里肯定是bf==1的情况 -- 才是单旋
//parent->_bf==2说明是左单旋
rotate_left(parent);//旋转就动了6个指针 -- O(1)
}
else if (parent->_bf == -2 && cur->_bf == -1) {
rotate_right(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1) {
//左右双旋
rotate_left_right(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1) {
//右左双旋
rotate_right_left(parent);
}
else assert(false);
break;
}
else {
assert(false);//理论上不能走到这里
}
}
return true;
}
public:
void inorder() {
_inorder(this->_root);
}
bool is_balance() {
return _is_balance(this->_root);
}
private:
void _inorder(Node* root) {
if (root == nullptr) {
return;
}
_inorder(root->_left);
cout << (root->_kv).first << ":" << (root->_kv).second << endl;
_inorder(root->_right);
}
int _height(Node* root) {
if (root == nullptr)return 0;
int leftHT = _height(root->_left);
int rightHT = _height(root->_right);
return max(leftHT, rightHT) + 1;
}
bool _is_balance(Node* root) {
if (root == nullptr)return true;
int leftHT = _height(root->_left);//左子树高度
int rightHT = _height(root->_right);//右子树高度
int diff = rightHT - leftHT;
//把平衡因子也检查一下
if (diff != root->_bf) {
cout << root->_kv.first << "的平衡因子异常" << endl;
return false;
}
return abs(diff) < 2
&& _is_balance(root->_left)//判断一下左子树是否平衡
&& _is_balance(root->_right);//判断一下右子树是否平衡
}
};
void test1() {
int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
AVL<int, int>t1;
for (auto e : a) {
t1.insert(make_pair(e, e));
}
t1.inorder();
cout << "is_balance():" << t1.is_balance() << endl;
}
void test2(){
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
AVL<int, int>t1;
for (auto e : a) {
t1.insert(make_pair(e, e));
}
t1.inorder();
cout << "is_balance():" << t1.is_balance() << endl;
}
void test3(){
size_t N = 10000;
srand(time(nullptr));
AVL<int, int>t1;
for (size_t i = 0; i < N; ++i) {
int x = rand();
t1.insert(make_pair(x, i));
}
cout << "is_balance():" << t1.is_balance() << endl;
}
七、总结
看到这里,大家应该对AVL树的实现,重点是它的旋转有了比较深入的了解了。这篇博客博主花了很多心思在画图上,也投入了很多时间到画图上。下期给大家带来红黑树的内容。希望大家可以多多支持,一键三连,点赞关注收藏评论后在离开哦!
( 转载时请注明作者和出处。未经许可,请勿用于商业用途 )
更多文章请访问我的主页