代码汇总:
概念:
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
为什么是不超过1呢?:
因为对于4个节点这类的 不可能做到左右子树的高度绝对值相差为0 不超过1 也可以理解成一种退而求其次
什么是二叉搜索树:若它的左子树不空,则左子树上所有结点的值均小于它的 根结点 的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
性质:
它的左右子树都是AVL树
左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
![](https://i-blog.csdnimg.cn/blog_migrate/df538e4a5cc1a25837194f9bb19a69f2.png)
手撕AVL:
(补充)对pair结构的认识
概念:
pair是将2个数据组合成一组数据,如stl中的map就是将key和value放在一起来保存。 pair的实现是一个结构体,主要的两个成员变量是first second ,因为是使用struct不是class,所以可以直接使用pair的成员变量。
类模板:template<class T1,class T2> struct pair
参数:T1是第一个值的数据类型,T2是第二个值的数据类型。
功能:
pair将一对值(T1和T2)组合成一个值,
这一对值可以具有不同的数据类型(T1和T2),
两个值可以分别用pair的两个公有函数first和second访问。
构造函数:(make_pair在插入中很常用)
pair<T1, T2> p1;
//创建一个空的pair对象(使用默认构造),它的两个元素分别是T1和T2类型,采用值初始化。
pair<T1, T2> p1(v1, v2);
//创建一个pair,它的两个元素分别是T1和T2类型,其中first成员初始化为v1,second成员初始化为v2。
make_pair(v1, v2);
// 以v1和v2的值创建一个新的pair对象,其元素类型分别是v1和v2的类型。
p1 < p2;
// 两个pair对象间的小于运算,其定义遵循字典次序:如 p1.first < p2.first 或者 !(p2.first < p1.first) && (p1.second < p2.second) 则返回true。
p1 == p2;
// 如果两个对象的first和second依次相等,则这两个对象相等;该运算使用元素的==操作符。
p1.first; // 返回对象p1中名为first的公有数据成员
p1.second; // 返回对象p1中名为second的公有数据成员
创建和初始化:
必须提供俩个可同可不同的类型名字
pair<string, string> anon; // 创建一个空对象anon,两个元素类型都是string
pair<string, int> word_count; // 创建一个空对象 word_count, 两个元素类型分别是string和int类型
pair<string, vector<int> > line; // 创建一个空对象line,两个元素类型分别是string和vector类型
pair<string, string> author("James","Joy"); // 创建一个author对象,两个元素类型分别为string类型,并默认初始值为James和Joy。
pair<string, int> name_age("Tom", 18);
pair<string, int> name_age2(name_age); // 拷贝构造初始化
pair对象的操作:
可以直接用.first .second对pair的成员进行访问
AVL节点的初始化:(三叉链+平衡因子)
什么是平衡因子? balance factor
节点右子树高度-左子树高度
1.第一个struct 写出左指针 右指针 平衡因子 构造函数(不写的话 将来插入new会出问题)
2.第二个struct 对AVL的节点重命名成Node
3.在第二个struct的public中增添函数
#pragma once
#include<assert.h>
#include<time.h>
template<class K,class V>
struct AVLTreeNode
{
pair<K, V>_kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf;// balance factor
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
,_parent(nullptr)
,_bf(0)
};
template<class K,class V>
struct AVLTree
{
typedef AVLTreeNode<K, V>Node;
private:
Node* _root = nullptr;
};
AVL树的插入bool Insert(const pair<K, V>& kv):
思路:
1.当树为空的时候 直接new插入即可
//空树
if (root == nullptr)
{
_root = new Node(kv);
return true;
}
2.用俩个指针 parent指向nullptr cur指向_root
![](https://i-blog.csdnimg.cn/blog_migrate/c461b06170953ae128588276feef49e2.png)
parent指针存在的意义:令parent和cur产生迭代(parent是cur的跟屁虫)
当节点找到他应该插入的位置的时候 如果没有parent 一个cur只是一个单纯的形参变量 和这个结构产生不了联系
//准备迭代建立关系
Node* parent = nullptr;
Node* cur = _root;
3.while(cur) 寻找满足插入结点的位置 同时if语句中parent与cur不断迭代更新
//寻找在哪里插入
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);//嘎嘎插入
4.已经插入节点 判断parent的key键值大小 从而判断cur插入左侧还是右侧
二叉树搜索树:
![](https://i-blog.csdnimg.cn/blog_migrate/0ae31657566420aa9b366d7bd9714c2e.png)
AVL树:
![](https://i-blog.csdnimg.cn/blog_migrate/573af492ba2e6063bd2a278d8e05ae06.png)
//建立联系
if (parent->_lv.first < kv.first)
{
parent->_right = cur;
cur->_parent = parent;//如果是普通的二叉搜索树没有这句
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
5.更新平衡因子 最坏的情况就是一直向上更新到了parent 所以是while(parent)
由于插入了cur 新增在左 parent->_bf++; 新增在右 --;
是否继续更新依据:子树的高度是否变化
1、parent->_bf == 0说明之前parent->_bf是 1 或者 -1
说明之前parent一边高一边低,这次恰好插入填上矮的那边,parent所在子树高度不变,直接break
2、parent->_bf == 1 或 -1 说明之前是parent->_bf == 0,两边一样高,现在插入一边更高了,
parent所在子树高度变了,得继续往上更新(这段代码的cur和parent都在往上跑 所以叫往上更新)
3、parent->_bf == 2 或 -2,说明之前parent->_bf == 1 或者 -1,现在插入严重不平衡,违反规则
只好动手术---旋转
//更新平衡因子
while (parent)//最多更新到根
{
// 新增在右,parent->bf++;
// 新增在左,parent->bf--;
if (cur == parent->_left)
{
parent->_bf--;
}
else
{
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)
{
//旋转
}
else
{
assert(false);//如果这里报错 说明插入写的有问题 可以更快的判断错误位置
}
}
return true;
}
此时只差旋转手术刀没有解决 这也是一个不好突破的点
AVL树的四种旋转:
else if (parent->_bf == 2 || parent->_bf == -2)
{
//旋转
}
当走到这种地步的时候 只能通过旋转才可以保持结构的稳定
旋转条件:(也是Insert需要补充的代码)
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
当pSubR的平衡因子为1时,执行左单旋(RR)
当pSubR的平衡因子为-1时,执行右左双旋(RL)
2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
当pSubL的平衡因子为-1是,执行右单旋(LL)
当pSubL的平衡因子为1时,执行左右双旋(LR)
旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
else if (parent->_bf == 2 || parent->_bf == -2)
{
if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent);//RR 在较高右子树插入右节点
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);//LL 在较高的左子树插入左节点
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);//LR 在较高的左子树插入右节点
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);// RL 在较高的右子树插入左节点
}
else
{
assert(false);
}
}
旋转的要求:
1.左右高度不超过一
2.旋转过程中也得保持是搜索树、
3.更新调整孩子节点的平衡因子
4.让子树高度和插入前保持一致(break)
~所谓的左旋和右旋都是以子树为原点的:如b是a的子树,那么旋转就围绕b来进行。
~如果b是a的左子树,那么就围绕b将a向右旋转,看着就像是a直接掉下来了,掉成了b的右子树。
~如果b是a的右子树,那么就围绕b将a向左旋转,看着就像是a直接掉下来了,掉成了b的左子树。
![](https://i-blog.csdnimg.cn/blog_migrate/596372e6852e4e6388dfadba4661bbba.png)
新节点插入新节点插入较高右子树的右侧---右右:左单旋
60是30的子树 看起来就像是将60提起来 30被旋转到了左边一样
![](https://i-blog.csdnimg.cn/blog_migrate/3bdb735debe6e39cc663ebcfb668a6b5.png)
逻辑思路:
60的左边变成30的右边
30变成60的左边
原来30是根 现在60是根
代码书写思路:(走下去还有返回头来看看_parent节点有没有书写完毕)
注意:b存在与否 树局部与否
将30 60 和 b节点分别取名为 parent subR subRL 是为了防止将来的节点丢失
如果b不存在的话 是不需要subR的_parent建立联系的
为了避免局部子树旋转带来的问题 利用ppNode记录parent的_parent;
若不是局部子树 改变根 返回空
若是局部子树 书写subR的_parent=ppNode
注意:平衡因子
最后一定要刷新平衡因子 都变成了0
void RotateL(Node* parent)
{
Node* subR = parent->_right;//30的右孩子60记录
Node* subRL = subR->_left;//记录可能不存在的b
//开始旋转
parent->_right = subRL;//60的左调整到30的右
if (subRL)//如果b存在的话 要是不存在 空指针也没有parent
subR->_parent = parent;
Node* ppNode = parent->_parent;//担心是局部子树 记录下根节点的parent
subR->left = parent;//30到60的左边
parent->_parent = subR;//parent节点一定存在 所以不写if语句
//如果非局部子树 根节点的parent得置空
if (ppNode == nullptr)
{
_root = subR;//subR就是60
_root->_parent == nullptr;//必须得置空
}
//如果是局部子树
else
{
if (ppNode->left == parent)//不会改变 parent可是我一开始就传进来的参数
{
ppNode->_left = subR;
}
else
{
ppNode->_right = subR;
}
subR->_parent = ppNode;//相呼应 保持一致
}
parent->_bf = subR->_bf = 0;//只动了60 和 30的孩子 没有动abc
}
新节点插入较高左子树的左侧---左左:右单旋
![](https://i-blog.csdnimg.cn/blog_migrate/94a3175171951b9d92ca633ce6a27532.png)
60的左边变成30的右边
60变成30的右边
根变成了30
节点:parent subL subLR
![](https://i-blog.csdnimg.cn/blog_migrate/ef35f22a1b0ecd3d7f2731f8b125d488.png)
新节点插入较高左子树的右侧---左右:先左单旋再右单旋
![](https://i-blog.csdnimg.cn/blog_migrate/cf269a2069aed47cb547ec8e470d8495.jpeg)
将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新
逻辑分析:90 parent 30 subL 60 subLR
对于90左边高 对于30右边高
刚开始30为轴 左单旋 b变30右 30变60左 60变根 缕直之后
90为轴 c变90左 90变60右 60变根
注意:这里的平衡因子不可以顺利的都要变成0 而是得具体分析 得利用subLR的_bf来分析
![](https://i-blog.csdnimg.cn/blog_migrate/e99197479becfba05a61fc4e3cdcd4bc.png)
情况一: b插入
![](https://i-blog.csdnimg.cn/blog_migrate/82d8d3396bc5e4f61b9e088725fd81e4.png)
subL 0 sublR 0 parent 1
情况二:c插入
![](https://i-blog.csdnimg.cn/blog_migrate/37fee18415986716ea71e2f09bbdb126.png)
情况三:subLR 60节点自己就是新增 所以一股脑子全是0
这般操作小来 平衡因子的值就确定完毕了
代码分析:注意记录平衡因子就好
void RotateLR(Node* parent)
{
Node* subL = parent->_left;//30
Node* subLR = sub->_right;//60
int bf = subLR->_bf;//用60衡量平衡因子
//先对30进行左旋
RotateL(parent->_left);
//对根进行右旋
RotateR(parent);
//平衡因子的调整
if (bf == -1) // subLR左子树新增
{
subL->_bf = 0;
parent->_bf = 1;
subLR->_bf = 0;
}
else if (bf == 1) // subLR右子树新增
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == 0) // subLR自己就是新增
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
assert(false);//方便调整代码
}
}
新节点插入较高右子树的左侧---右左:先右单旋再左单旋
![](https://i-blog.csdnimg.cn/blog_migrate/2f75a1e85bc276254419f504bc8b7ada.jpeg)
逻辑分析:30 parent 90 subR 60 subRL
将双旋变成单旋后再选转,即:先对90进行右单旋,然后再对30进行左单旋,旋转完成后再考虑平衡因子的更新 右单旋 c变30左 90变60右 60变成根
b变30右 30变60左 60变根
平衡因子三种情况:(按照30 60 90 的顺序)
情况一:
-1 0 0
情况二:
0 0 1
情况三:
0 0 0
AVL性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这
样可以保证查询时高效的时间复杂度,即$log_2 (N)$。但是如果要对AVL树做一些结构修改的操
作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,
有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数
据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合