前言
我们上一期介绍了二叉搜索树并做了实现,本期我们来继续学习另一个更优的树即AVL树!
本期内容介绍
什么是AVL树?
AVL树的实现
AVL树的性能分析
在正式的介绍AVL树之前,我们先来回忆一下二叉搜索树的特点:左子树的值一定小于根节点的值,右子树的值一定大于根节点的值;基于他的这个特点,可以缩短查找的区间即可以提升查找的效率!但是他在有些情况下效率并不是很好。例如:当数据是有序或接近有序时,查找得需要O(N)的时间复杂度即退化成单链就和和链表一样了,效率不太好!为了解决这个问题,有人就提出了AVL树!
什么是AVL树?
为了解决二叉搜索树的弊端,在1962年来自俄罗斯的两位数学家G.M.Adelson-Velskii和E.M.Landis研究出了一种解决上述问题的方法:当向二叉树种插入新节点时,如果保每个节点的左右子树的高度的绝对值之差不超过1(如果超过了1需要内部调整)既可以降低树的高度,从而减少平均查找长度!符合这样的二叉搜索树就叫做平衡二叉搜索树即AVL树。也就是:
一颗AVL树要么是空树要么是符合下面性质的二叉搜索树:
该二叉搜索树的左右子树都是AVL树;
它的左右子树高度差(简称平衡因子)的绝对值不超过1(-1/0/1);
平衡因子可以是左减右,也可以右减左;这里采用后者!
所以,下面这棵二叉搜索树就是一个AVL树:
AVL树的实现
还是和以前一样先搭个架子出来,多次用到节点的开辟等操作,所以我们创建一个专门的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<K,V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_bf(0)//默认一个新节点是叶子节点,左右子树都为空所以高度差(平衡因子)为0
{}
};
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
private:
Node* _root = nullptr;//这里只有一个成员可以给一个缺省值就不用写构造函数了
};
AVL树的插入
AVL树的本质还是二叉搜索树,所以插入的时候还是遵循二叉搜索树的特点的!但是多了一个平衡因子,所以他的插入是分为两步的:
1、按照二叉搜索树的规则插入
2、调节平衡因子
前者很好理解,这里主要解释一下后者:我们采用的平衡因子的方式是右子树的高度 - 左子树高度,所以当新插入节点后,要更新其父先节点的平衡因子,如果插入的节点是在parent的左,平衡因子--; 如果是右平衡parent的因子++;
1、如果更新后父节点的平衡因子是0,即原先的parent左或右是有一个孩子的,现在是插入到原先没有孩子的那边了即平衡了,此时直接结束调节平衡因子;
2、如果更新parent的平衡因子后不是0,而是-1/1则需要继续向上更新!(直到cur更新到根节点,停止)
3、如果更新后父节点的平衡因子的绝对值超过了1就要旋转。(旋转后面单独介绍)
OK,举个例子:
这里咱们先暂时不管旋转是怎么旋转的,我们先把上一般的给搞出来:
bool Insert(const pair<K, V>& kv)
{
Node* parent = nullptr;//记录插入位置的父节点
Node* cur = _root;//当前插入节点
//第一次插入
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
//不是第一次插入,寻找插入位置
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);//找到插入位置
//判断是parent的左子树还是右子树
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;//将当前节点的父节点设置是为parent
//调节平衡因子 ---> bf = right - left
while (parent)
{
//更新当前节点的父节点的平衡因子
if (cur == parent->_left)//在parent的左
{
parent->_bf--;//bf--
}
else
{
parent->_bf++;//在parent的右, bf++
}
if (parent->_bf == 0)//如果更新完,cur的parent的bf,发现是0。说明是 -1/1 --> 0 即parent原先左或右是有一个孩子的
{
break;//更新结束
}
else if (parent->_bf == 1 || parent->_bf == -1)//如果是parent的bf是1/-1
{
cur = parent;
parent = parent->_parent;//需要继续更新其祖先节点
}
else
{
//此时parent的bf是-2/2需要旋转
break;//旋转结束跳出平衡因子的调节
}
}
return true;//插入成功
}
OK,先来验证一下,目前的逻辑对不对?AVL树的本质还是搜索树,所以他的中序是有序的,所以可以通过走中序验证目前的对不对:
中序遍历
实现思路:左->中->右
注意:由于AVL树的根节点是私有的,类的外面是访问呢不到的,所有以下三种思路:
1、将中序设置为友元(强烈不推荐)
2、提供get和set函数
3、通过子函数(推荐)
void _InOrder(const Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
这是子函数,因为只有AVL类里面专用所以可以将它设置为私有的!
OK,就以上面介绍过的为例:
我们乱序插入到当前的AVL树中,如果中序是有序的说明我们的当前逻辑是对的!
OK,现在parent的bf==1/-1以及等于0的情况解决了,剩下的就是parent的bf是-2/2的情况了,此时就需要旋转了!我们下面来专门谈一谈旋转!
AVL树的旋转
旋转是由于parent的bf到了-2/2,此时的结构不符合AVL的平衡了;而parent为-2/2是由其孩子造成的,而孩子有两种情况即左边和右边即cur是-1/1;所以此时就有四种情况:
1、parent == -2 && cur == -1即父亲的左边高、孩子(左子树)的左边高 --> 右单旋2、parent == 2 && cur == 1即父亲的右边高、孩子(右子树)的右边高 --> 左单旋
3、parent == -2 && cur == 1即父亲的左边高、孩子(左子树)的右边高 --> 左右双旋
4、parent == 2 && cur == -1即父亲的右边高、孩子(右子树)的左边高 --> 右左双旋
右单旋
parent的左边高,并且他的左孩子也高!
void RotateR(const Node* parent)
{
Node* subL = parent->_left;//parent的左子树
parent->_left = subL->_right;//将subL的又给parent的左
if (subL->_right)//如果subL的右不为空
subL->_right = parent;//此时subL右的父节点就是parent
Node* ppNode = parent->_parent;//记录parent的父节点方便后面subL不是root时候的链接
subL->_right = parent;//将parent连接到subL的右边
parent->_parent = subL;//将parent的父节点设置为subL
if (parent == _root)//当前的parent是根节点
{
_root = subL;//新的根节点就是subL
ppNode = nullptr;//根节点的父亲为空
}
else//当前的parent不是根节点
{
if (ppNode->_left == parent)//如果parent的是ppNode的左
{
ppNode->_left = subL;//将subL连接到ppNode的左边
}
else//parent的是ppNode的右
{
ppNode->_right = subL;//将subL连接到ppNode的右边
}
subL->_parent = ppNode;//subL的父节点指向ppNode
}
subL->_bf = parent->_bf = 0;//右单旋后subL和parent的bf都是0
}
左单旋
parent的右边高,并且他的右孩子也高!
void RotateL(const Node* parent)
{
Node* subR = parent->_right;//parent的右子树
parent->_right = subR->_left;//将subR的左子树给parent的右
if (subR->_left)//如果subR的左不为空
subR->_left->_parent = parent;//subR的左的父亲就是parent
Node* ppNode = parent->_parent;//记录parent的父节点方便后面subL不是root时候的链接
subR->_left = parent;//将parent连接到subR的左
parent->_parent = subR;//parent的父节点就是subR
if (parent == _root)//parent是根节点
{
_root = subR;//此时subR就是新的根
_root->_parent = nullptr;
}
else//parent不是根节点
{
if (ppNode->_left == parent)//parent是ppNode的左
{
ppNode->_left = subR;//将subR连接到ppNode的左
}
else//parent是ppNode的右
{
ppNode->_right = subR;//将subR连接到ppNode的右
}
subR->_parent = ppNode;//subR的父节点指向ppNode
}
subR->_bf = parent->_bf = 0;//左单旋后subR和parent的bf都是0
}
左右双旋
parent的左孩子高,并且他左孩子的右边高!
void RotateLR(const Node* parent)
{
Node* subL = parent->_left;//左子树
Node* subLR = subL->_right;//左子树的右子树
int bf = subLR->_bf;
RotateL(parent->_left);//先对左子树左旋
RotateR(parent);//在对整个树进行右旋
if (bf == 0)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else
{
assert(false);//正常不可能到这,这里是防止一开始就不是AVL
}
}
右左双旋
parent的右孩子高,他的右孩子的左边高!
void RotateRL(const 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 = 1;
}
else
{
assert(false);//正常不可能到这,这里是防止一开始就不是AVL
}
}
ok,这就是所有的情况,我们现在加上旋转来看看,为了验证是否是平衡的,我们可以写一个判断是否平衡的函数:
判断平衡
实现思路:某一个节点的左右子树的差的绝对值不可以超多1
由于AVLTree类外面访问不到根,所以我们还是写成子函数的形式:
bool _IsBalance(const Node* root)
{
if (root == nullptr)//空树也平衡
return true;
int left = _Hight(root->_left);//求左子树的高度
int right = _Hight(root->_right);//求右子树的高度
if (abs(left - right) >= 2)//如果左右子树的高度差的绝对值差超过1则就是不平衡
return false;
if (right - left != root->_bf)//不平衡,打印出他的节点的key值
{
cout << root->_kv.first << endl;
return false;
}
return _IsBalance(root->_left) && _IsBalance(root->_right);//左右子树都得平衡
}
int _Hight(Node* root)
{
if (root == nullptr)//空树的个数是0
return 0;
return max(_Hight(root->_left), _Hight(root->_right)) + 1;//不是空返回左右子树的较大值 + 本身
}
ok ,验证一下:
OK,没有问题!我们再来把其他的完善一下:
AVL树的查找
实现思路:和二叉搜索树的一样,比根的去右边找,比根小去左边找!
Node* Find(const K& k)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < k)//插入节点的键值比当前比较的节点的键值大
{
cur = cur->_right;//去当前节点的右子树查找
}
else if (cur->_kv.first > k)//插入节点的键值比当前比较的节点的键值小
{
cur = cur->_left;//去当前节点的左子树查找
}
else
{
return cur;//找到了
}
}
return nullptr;//没找到
}
获取高度
实现思路:左子树 + 右子树 + 1(本身)
int _Hight(Node* root)
{
if (root == nullptr)//空树的个数是0
return 0;
return max(_Hight(root->_left), _Hight(root->_right)) + 1;//不是空返回左右子树的较大值 + 本身
}
获取节点个数
实现思路:左子树的节点 + 右子树的节点 + 根
int _Size(const Node* root)
{
if (root == nullptr)//空树
return 0;
return _Size(root->_left) + _Size(root->_right) + 1;//左子树+右子树+本身
}
AVL树的性能分析
AVL树是一颗绝对平衡的二叉搜索树,其要求每个节点的左右子树差都不超过1,这样可以保证查询高效的同时复杂度不会达到和二叉搜索树的极端情况的O(N)而是在logN;但是如果对AVL树做一些结构的修改,例如:插入太多次旋转也就多了,更差的是在删除时有可能旋转到根;因此如果需要一种查询且有序的数据结构,再者数据为静态的(不会改变)AVL树是一种不错的选择,但是如果经常修改其行能就不太好了,那要是既要改变还要效率高该如何弄呢?那就是下期介绍的红黑树了!!!
全部源码
#pragma once
#include <assert.h>
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<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)//默认一个新节点是叶子节点,左右子树都为空所以高度差(平衡因子)为0
{}
};
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
bool Insert(const pair<K, V>& kv)
{
Node* parent = nullptr;//记录插入位置的父节点
Node* cur = _root;//当前插入节点
//第一次插入
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
//不是第一次插入,寻找插入位置
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);//找到插入位置
//判断是parent的左子树还是右子树
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;//将当前节点的父节点设置是为parent
//调节平衡因子 ---> bf = right - left
while (parent)
{
//更新当前节点的父节点的平衡因子
if (cur == parent->_left)//在parent的左
{
parent->_bf--;//bf--
}
else
{
parent->_bf++;//在parent的右, bf++
}
if (parent->_bf == 0)//如果更新完,cur的parent的bf,发现是0。说明是 -1/1 --> 0 即parent原先左或右是有一个孩子的
{
break;//更新结束
}
else if (parent->_bf == 1 || parent->_bf == -1)//如果是parent的bf是1/-1 0 ---> -1/1
{
cur = parent;
parent = parent->_parent;//需要继续更新其祖先节点
}
else//此时parent的bf是-2/2需要旋转 -1/1 --> 2/-2
{
if (parent->_bf == -2 && cur->_bf == -1)//父亲和孩子的左边都高,右单旋
{
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == 1)//父亲和孩子的右边都高,左单旋
{
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)//父亲的左边高,孩子的右边高,先左单旋,在右单旋
{
RotateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)//父亲的右边高,孩子的左边高,先右单旋,在左单旋
{
RotateRL(parent);
}
else
{
assert(false);//正常情况不可能走到这里,这里是防止一开始就不是AVL树的情况
}
break;//旋转结束跳出平衡因子的调节
}
}
return true;//插入成功
}
Node* Find(const K& k)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < k)//插入节点的键值比当前比较的节点的键值大
{
cur = cur->_right;//去当前节点的右子树查找
}
else if (cur->_kv.first > k)//插入节点的键值比当前比较的节点的键值小
{
cur = cur->_left;//去当前节点的左子树查找
}
else
{
return cur;//找到了
}
}
return nullptr;//没找到
}
void InOrder()
{
return _InOrder(_root);
}
bool IsBalance()
{
return _IsBalance(_root);
}
int Hight()
{
return _Hight(_root);
}
int Size()
{
return _Size(_root);
}
private:
void _InOrder(const Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;//parent的左子树
parent->_left = subL->_right;//将subL的又给parent的左
if (subL->_right)//如果subL的右不为空
subL->_right->_parent = parent;//此时subL右的父节点就是parent
Node* ppNode = parent->_parent;//记录parent的父节点方便后面subL不是root时候的链接
subL->_right = parent;//将parent连接到subL的右边
parent->_parent = subL;//将parent的父节点设置为subL
if (parent == _root)//当前的parent是根节点
{
_root = subL;//新的根节点就是subL
_root->_parent = nullptr;//根节点的父亲为空
}
else//当前的parent不是根节点
{
if (ppNode->_left == parent)//如果parent的是ppNode的左
{
ppNode->_left = subL;//将subL连接到ppNode的左边
}
else//parent的是ppNode的右
{
ppNode->_right = subL;//将subL连接到ppNode的右边
}
subL->_parent = ppNode;//subL的父节点指向ppNode
}
subL->_bf = parent->_bf = 0;//右单旋后subL和parent的bf都是0
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;//parent的右子树
parent->_right = subR->_left;//将subR的左子树给parent的右
if (subR->_left)//如果subR的左不为空
subR->_left->_parent = parent;//subR的左的父亲就是parent
Node* ppNode = parent->_parent;//记录parent的父节点方便后面subL不是root时候的链接
subR->_left = parent;//将parent连接到subR的左
parent->_parent = subR;//parent的父节点就是subR
if (parent == _root)//parent是根节点
{
_root = subR;//此时subR就是新的根
_root->_parent = nullptr;
}
else//parent不是根节点
{
if (ppNode->_left == parent)//parent是ppNode的左
{
ppNode->_left = subR;//将subR连接到ppNode的左
}
else//parent是ppNode的右
{
ppNode->_right = subR;//将subR连接到ppNode的右
}
subR->_parent = ppNode;//subR的父节点指向ppNode
}
subR->_bf = parent->_bf = 0;//左单旋后subR和parent的bf都是0
}
void RotateLR(Node* parent)
{
Node* subL = parent->_left;//左子树
Node* subLR = subL->_right;//左子树的右子树
int bf = subLR->_bf;
RotateL(parent->_left);//先对左子树左旋
RotateR(parent);//在对整个树进行右旋
//重新更新平衡因子
if (bf == 0)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else
{
assert(false);//正常不可能到这,这里是防止一开始就不是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);//正常不可能到这,这里是防止一开始就不是AVL
}
}
bool _IsBalance(const Node* root)
{
if (root == nullptr)//空树也平衡
return true;
int left = _Hight(root->_left);//求左子树的高度
int right = _Hight(root->_right);//求右子树的高度
if (abs(left - right) >= 2)//如果左右子树的高度差的绝对值差超过1则就是不平衡
return false;
if (right - left != root->_bf)//不平衡,打印出他的节点的key值
{
cout << root->_kv.first << endl;
return false;
}
return _IsBalance(root->_left) && _IsBalance(root->_right);//左右子树都得平衡
}
int _Hight(Node* root)
{
if (root == nullptr)//空树的个数是0
return 0;
return max(_Hight(root->_left), _Hight(root->_right)) + 1;//不是空返回左右子树的较大值 + 本身
}
int _Size(const Node* root)
{
if (root == nullptr)//空树
return 0;
return _Size(root->_left) + _Size(root->_right) + 1;//左子树+右子树+本身
}
private:
Node* _root = nullptr;//这里只有一个成员可以给一个缺省值就不用写构造函数了
};
ok,本期分享就到这里,好兄弟。我们下期再见!
结束语:不要因为被人的三言两语就打破你原本的深思熟虑!