什么是AVL树?
AVL树又称为高度平衡的二叉搜索树。
一棵AVL树如果不是空树,则符合以下性质:
1.左子树与右子树的高度之差的绝对值不超过1
2.左右子树均是AVL树
易知,二叉搜索树可以将查询效率提高至O(logh),h为二叉搜索树的高度。但当数据的插入是严格有序时,二叉搜索的效率会恶化至O(n),即线性复杂度。如下,有一组数据:
不可否认,这还真是一棵二叉搜索树,但这棵树好嘛?不好!为什么不好?当我们想要查找数据时,查询路径和遍历查数组有什么区别?没有区别!都是线性复杂度。
所以,两个俄罗斯数学家提出了AVL树,其目的在于提高二叉搜索树的效率,减少树的平均搜索长度。
那么AVL树又是如何提高效率的?AVL树是这么做的:每插入一个新节点时,就会调整树的结构,使得二叉搜索树保持平衡,从而尽可能降低树的高度,减少树的平均搜索长度。同一组数据,AVL树长这样:
是二叉搜索树吗?是!
本文AVL树平衡因子bf=右树高度-左树高度,咱们就看看上面这棵树每个节点的平衡因子⑧。
是AVL树吗?是!无论哪个节点的平衡因子bf(左右子树高度之差)的绝对值不大于1。
而这棵树的搜索长度最长3!比单纯的二叉搜索树长度少了3!提没提高搜索效率?嗯,的确提高了。
那么AVL树凭什么把同样顺序的数据构造成为这样的二叉搜索树呢?那就要详细说说AVL树的插入了!
AVL树的节点插入
前面说了:AVL树每插入一个新节点,都会调整树的结构,使这棵树保持平衡(即每个节点平衡因子bf的绝对值不大于1)
我们先来直观感受一下刚才那个AVL树怎么创建出来的:
小伙伴们应该注意到了里面有一个左旋的操作⑧。不错!左旋是使上面这颗树保持平衡的核心操作!但既然有左旋,当然有右旋了!能旋一次,就能旋两次!还有左右双旋(先左旋再右旋),右左双旋(先右旋再左旋)。打住打住!没了!没有其他旋的种类了。
接下来讲讲四个旋转!
左旋
我们再来抽象一下左旋操作,及看一下哪种情况需要左旋。
最后,贴上我当时的一个疑问,权当乐子,如果你也有恰巧有这样的疑惑,看了这番解释懂了,不胜荣幸!
左旋代码
简要说明:
变量 | 意义 |
---|---|
Node* | AVL树节点指针 |
t | 形参列表里的t即失衡节点 |
bf | 节点平衡因子 |
本文所有旋转函数只是完成从失衡节点开始往下的旋转 |
void RorateL(Node*& t) {//传入的t是失衡节点
Node* subL = t;//失衡节点最后会变左树
t = subL->right;//子节点将变父节点
subL->right = t->left;//子节点的左树成为父节点的右树
t->left = subL;//父节点左旋
subL->bf = t->bf = 0;
}
【Note】:形参列表传的引用,之所以使用引用,是因为使用引用可以实现真正的修改原指针指向。真实目的是该函数结束后,传入的t
是真正的根节点!可以看下图理解一下:
不一定失衡节点就是整棵AVL树的根节点!这个函数结束后,AVL树长这样,还需要进行一次连接!
右旋
其实右旋就是左旋的镜像,话不多说直接看图,鉴于左旋解释的比较清楚了,这里就不向介绍左旋那样搞了。
来抽象一右旋操作,及看一下哪种情况需要右旋。
右旋代码
void RorateL(Node*& t) {//传入的t是失衡节点
Node* subR = t;
t = subR->left;//子节点将变父节点
subR->left= t->right;//子节点的右树成为父节点的左树
t->right = subR;//父节点右旋
subR->bf = t->bf = 0;
}
左右双旋
双旋总是会考虑三个节点,这三个节点分别是失衡节点,失衡节点的某一子节点,失衡节点子节点的某一子节点。
来抽象左右旋操作,及看一下哪种情况需要左右双旋。
哎?!!忘了在图中说适合左右双旋的情况了!这里就简单说说叭,其实无论你在哪插入新节点,我们都会发现失衡节点subR
的平衡因子永远是 -2,而它的子节点sub
L平衡因子为1大于0,也就是subR
与subL
,平衡因子前者为负,后者为正,即达成左右双旋的条件==!不要跟我说前者为负,-1会被调整!-1的绝对值根本不大于1,没有失衡,不会发生结构调整!哈哈,开玩笑了,就是提醒一下牢记平衡因子绝对值大于1才会失衡,才会调整,这适合于任何情况
左右双旋代码
void RorateLR(Node*& t) {
Node* subL, * subR;
subR = t;
subL = subR->left;
t = subL->right;
subL->right = t->left;
t->left = subL;
if (t->bf <= 0)
subL->bf = 0;
else
subL->bf = -1;
subR->left = t->right;
t->right = subR;
if (t->bf >= 0)
subR->bf = 0;
else
subR->bf = 1;
t->bf = 0;
}
右左双旋
同理,右左双旋是左右双旋的镜像。顶不住了,简单说一下:
继续来抽象右左旋操作,及看一下哪种情况需要右左双旋。
右左双旋代码
void RorateLR(Node*& t) {
Node* subL, * subR;
subR = t;
subL = subR->left;
t = subL->right;
subL->right = t->left;
t->left = subL;
if (t->bf <= 0)
subL->bf = 0;
else
subL->bf = -1;
subR->left = t->right;
t->right = subR;
if (t->bf >= 0)
subR->bf = 0;
else
subR->bf = 1;
t->bf = 0;
}
AVL的插入函数
上面已经文字+图片把4个旋转解释差不多了,也把什么时候用哪种旋转说清楚了。还遗留几个问题:
1.上面都讲旋转,都是已经插入新节点的前提下在旋转调整,那怎么插入?
答:AVL是BST,所以插入与BST插入一样,但需要使用栈保存插入路线!,否则插入后无法向上回溯调整平衡因子,无法判断是否有节点因此失衡。
2.旋转完成后,就结束了?
答:并没有结束,传入旋转函数的根节点,在旋转完成后成为另一个节点,因此需要进行重新链接,具体情况可以看左旋部分的最后一张图。
3.旋转完成后,也进行了链接,还需要向上回溯继续修改平衡因子吗?
答:不需要,当旋转函数执行后,说明失衡的节点已经平衡,更上层的节点不会失衡,因此不需要继续向上回溯了。
整体代码
#include <iostream>
#include <vector>
#include <stack>
using namespace std;
template<class Ty>
class AVLTree;
template<class Ty>
class AVLNode {
friend class AVLTree<Ty>;
public:
AVLNode() :val(Ty()), bf(0),left(nullptr),right(nullptr){}
AVLNode(Ty _val = Ty(),int _bf = 0, AVLNode<Ty>* l = nullptr, AVLNode<Ty>* r = nullptr) :
val(_val), bf(_bf),left(l), right(r) {}
~AVLNode() {
left = nullptr;
right = nullptr;
bf = 0;
}
private:
AVLNode<Ty>* left;
AVLNode<Ty>* right;
Ty val;
int bf;
};
template<class Ty>
class AVLTree {
typedef AVLNode<Ty> Node;
public:
AVLTree() :root(nullptr) {}
AVLTree(const vector<Ty>& v) :root(nullptr) {
for (auto& e : v)
Insert(e);
}
public:
bool Insert(const Ty& data) { return Insert(root, data); }
protected:
bool Insert(Node*& t, const Ty& data) {
//先按BST插入节点,寻找插入位置过程中存储轨迹
Node* parent = nullptr;
Node* p = t;
stack<Node*> st;
while (p){
if (p->val == data)//重复数据不可插入
return false;
parent = p;
st.push(parent);
if (data > p->val)
p = p->right;
else
p = p->left;
}
p = new Node(data);
if (!parent) {//空树
t = p;
return true;
}
if (p->val > parent->val)//左小右大插入
parent->right = p;
else
parent->left = p;
//调整树,使BST->AVL
while (!st.empty()) {
parent = st.top();
st.pop();
if (p == parent->left)//左树升高
--parent->bf;
else
++parent->bf;
if (!parent->bf)//父节点平衡
break;
else if (parent->bf == 1 || parent->bf == -1)
p = parent;//向上回溯
else {//本节点不平衡,需要调整
if (parent->bf < 0) {
if (p->bf < 0)//右旋(/)
RorateR(parent);
else//左右双旋(<)
RorateLR(parent);
}
else {
if (p->bf > 0)//左旋(\)
RorateL(parent);
else//右左双旋(>)
RorateRL(parent);
}
break;//已平衡,跳出
}
}
//因旋转导致的原不平衡的节点被两次指向,需要调整
if (st.empty())
t = parent;
else {
Node* q = st.top();
if (parent->val < q->val)
q->left = parent;
else
q->right = parent;
}
return true;
}
private:
void RorateL(Node*& t) {
Node* subL = t;
t = subL->right;//子节点将变父节点
subL->right = t->left;//子节点的左树成为父节点的右树
t->left = subL;//父节点左旋
subL->bf = t->bf = 0;
}
void RorateR(Node*& t) {
Node* subR = t;
t = subR->left;//子节点将变父节点
subR->left= t->right;//子节点的右树成为父节点的左树
t->right = subR;//父节点右旋
subR->bf = t->bf = 0;
}
void RorateLR(Node*& t) {
Node* subL, * subR;
subR = t;
subL = subR->left;
t = subL->right;
subL->right = t->left;
t->left = subL;
if (t->bf <= 0)
subL->bf = 0;
else
subL->bf = -1;
subR->left = t->right;
t->right = subR;
if (t->bf >= 0)
subR->bf = 0;
else
subR->bf = 1;
t->bf = 0;
}
void RorateRL(Node*& t) {
Node* subL, * subR;
subL = t;
subR = subL->right;
t = subR->left;
subR->left = t->right;
t->right = subR;
if (t->bf >= 0)
subR->bf = 0;
else
subR->bf = 1;
subL->right = t->left;
t->left = subL;
if (t->bf <= 0)
subL->bf = 0;
else
subL->bf = -1;
t->bf = 0;
}
private:
Node* root;
};
结语
写这篇文章时,各种情况的平衡因子我是搞不明白旋转后变成啥样的,但这么一画一写,就基本弄清楚了,也算是有所收获吧。
AVL的删除更显复杂一些,目前肝不出来。不过!等我链接!
不破删除誓不还!
删除来了来了!
AVL树的删除