【C++】AVL树实现
目录
前言
一、AVL树的概念
二、AVL树的实现
1.基本框架
2.AVL树的插入
三、旋转
1.右单旋
2.左单旋
3.左右双旋
4.右左双旋
四、AVL树的查找
五、AVL树的平衡检测
六、AVL树的删除
总结
前言
本文主要讲解AVL树的插入,AVL树是在二叉搜索树的基础上,更加实用的一种数据结构,它拥有自平衡的特性,来保证它的搜索效率,本文就来探究它是如何实现自平衡的。
温馨提醒:本文所有代码中父节点我拼写成了perent,正确拼写应该是parent,不好意思,望周知(✪ω✪)。
一、AVL树的概念
- AVL 树是最先发明的自平衡二叉查找树,AVL是一颗空树,或者具备下列性质的二叉搜索树:它的左右子树都是AVL树,且左右子树的高度差的绝对值不超过1。AVL树是⼀颗高度平衡搜索二叉树, 通过控制高度差去控制平衡。
- AVL树得名于它的发明者G.M.Adelson-Velsky和E.M.Landis是两个前苏联的科学家,他们在1962 年的论文《Analgorithmfortheorganizationofinformation》中发表了它。
- AVL树实现这里我们引入一个平衡因子(balancefactor)的概念,每个结点都有一个平衡因子,任何结点的平衡因子等于右子树的高度减去左子树的高度,也就是说任何结点的平衡因子等于0/1/-1, AVL树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡, 就像一个风向标一样。
- 思考一下为什么AVL树是高度平衡搜索二叉树,要求高度差不超过1,而不是高度差是0呢?0不是更好的平衡吗?画画图分析我们发现,不是不想这样设计,而是有些情况是做不到高度差是0的。比如一棵树是2个结点,4个结点等情况下,高度差最好就是1,无法做到高度差是0。如下图一棵AVL树,它的平衡因子绝对值不超过1.
- 反之,如果平衡因子绝对值大于1,就不是AVL树,如下图:
- AVL树整体结点数量和分布和完全二叉树类似,高度可以控制在logN,那么增删查改的效率也可以控制在O(logN),相比二叉搜索树有了本质的提升。效率计算如下图:
温馨提醒:本章节需要有二叉搜索树的基础,主页有哦,AVL树就是在二叉搜索树上增加了自平衡机制,调整子树高度差,保证查找效率。
二、AVL树的实现
1.基本框架
#pragma once
#include <assert.h>template<class K, class V>
struct AVLTreeNode
{pair<K, V> _kv;//k:v结构AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _perent;int _bf;//平衡因子AVLTreeNode(const pair<K, V>& kv)//构造:_kv(kv), _left(nullptr), _right(nullptr), _perent(nullptr), _bf(0){}
};template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;//重命名
public:private:Node* _root = nullptr;//根节点
};
- AVL树的基本框架与搜索二叉树有点不同,主要是节点定义的区别:
- 节点的数据结构采用 key:value 结构,也就是使用pair保存数据。
- 节点采用三叉链结构,即有左孩子节点指针、右孩子节点指针、父节点指针。
- 增加成员变量 _bf,也就是平衡因子,用于检测每一个节点是否符合AVL树的特征(左右子树高度差不超过1)。
- 然后,AVL树的模版参数K,V,分别控制数据key:value的数据类型。类只有一个变量_root,也就是根节点。
温馨提醒:不要忘记了搜索二叉树的特性:对于不支持相等值的搜索二叉树,每个节点的左子树的数据全小于根节点,右子树的数据全大于根节点。
2.AVL树的插入
AVL树插入一个值的大概过程:
- 插入一个值按二叉搜索树规则进行插入。
- 新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点 -> 根结点路径上的平衡因子,实际中最坏情况下要更新到根节点,有些情况更新到中间就可以停止了,具体情况我们下面再详细分析。
- 更新平衡因子过程中没有出现问题,则插入结束。
- 更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转后调平衡的同时,本质降低了子树的高度,不会再影响上⼀层,所以插入结束。
我们先不管更新平衡因子和旋转,先把前面的代码写出来:
//插入bool Insert(const pair<K, V>& kv){//树为空if (_root == nullptr){_root = new Node(kv);return true;}//树不为空Node* cur = _root;Node* perent = nullptr;while (cur){if (kv.first > cur->_kv.first){perent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first){perent = cur;cur = cur->_left;}else{return false;}}//插入cur = new Node(kv);if (kv.first > perent->_kv.first){perent->_right = cur;}else{perent->_left = cur;}cur->_perent = perent;//更新平衡因子//...return true;}
- 这些就基本和搜索二叉树一致,这里就不赘述了。
- 需要注意的是插入新节点后记得更新新节点的父节点指针,毕竟是三叉链。
1.平衡因子更新:
更新原则:
- 平衡因子=右子树⾼度-左子树高度
- 只有子树高度变化才会影响当前结点平衡因子。
- 插入结点,会增加高度,所以新增结点在parent的右子树,parent的平衡因子++,新增结点在parent的左子树,parent平衡因子--。
- parent所在子树的高度是否变化决定了是否会继续往上更新。
我们先看一些实际情况:来了解什么时候可以停止向上更新平衡因子
(1)
- 如上图,新增节点13,在节点14的左边,所以14的平衡因子--,14的平衡因子变化就可能会引起其父节点平衡因子变化。因为14节点的平衡因子是由0变化到-1,所以它高度一定是加1,14的父节点是节点10,14在10的右边,所以14的平衡因子要++,此时对于节点10来说,平衡因子绝对值大于1了,需要旋转处理。(先不管如何旋转)
- 我们继续观察节点14,它的平衡因子原来是0,现在变成-1,说明它原来的子树高度差为0,插入一个节点后导致它高度差变化,所以,在插入节点操作中如果平衡因子变为-1,说明它一定是由0变化过来的,并且需要继续向上更新父节点的平衡因子。同理我们也能得出,如果插入操作导致平衡因子变为-1,说明它一定是从0变过来的,而且也需要继续向上更新平衡因子。
- 得出一条结论:更新后parent的平衡因子等于1或-1,更新前更新中parent的平衡因子变化为 0->1 或者 0->-1,说明更新前 parent 子树两边⼀样高,新增的插入结点后,parent所在的子树一边高一边低,parent所在的子树符合平衡要求,但是高度增加了1,会影响parent的父亲结点的平衡因子,所以要继续向上更新。
- 我们继续观察节点10,它的平衡因子变化是因为右子树新增一个节点,导致它的平衡因子从1变为2,此时它已经不满足AVL树了,需要进行旋转操作,我们先不管怎么旋转,我们先了解旋转的结果,旋转的目标有两个:1、把 parent子树旋转平衡。2、降低parent子树的高度,恢复到插入结点以前的高度。这样一来,相当于节点10的平衡因子没有发生变化,所以此时不需要向上更新平衡因子。同理当平衡因子从-1变为-2,我们也不需要向上更新父节点的平衡因子。
- 得出第二条结论:更新后parent的平衡因子等于2或-2,更新前更新中parent的平衡因子变化为1->2或者-1->-2,说明更新前parent子树一边高一边低,新增的插入结点在高的那边,parent所在的子树高的那边更高了,破坏了平衡,parent所在的子树不符合平衡要求,需要旋转处理,旋转的目标有两个:1、把 parent子树旋转平衡。2、降低parent子树的高度,恢复到插入结点以前的高度。所以旋转后也不需要继续往上更新,插入结束。
(2)
- 如上图,这次插入节点-1,位于父节点1的左子树,所以父节点1的平衡因子--,此时因为节点1的平衡因子发生变化,所以需要继续向上更新,节点1的父节点3的平衡因子此时从1变为0,这说明新增节点导致节点3的左右子树高度平衡了,这时我们还需要继续向上更新平衡因子吗?答案是不用了,因为平衡因子变为0说明它一定是从-1或者1变过来的,变化前高度差为1,变化后高度差为0,这对于其父节点来说高度没变化,所以不需要继续向上更新平衡因子了。
- 得出结论:更新后parent的平衡因子等于0,更新中parent的平衡因子变化为-1->0或者1->0,说明更新前 parent 子树一边高一边低,新增的结点插入在低的那边,插入后parent所在的子树高度不变,不会影响parent的父亲结点的平衡因子,更新结束。
(3)
- 插入节点7,节点6的平衡因子加1,节点3的平衡因子加1,根节点8的平衡因子也要加1。所以这就是最后一种情况也是最坏的情况,需要我们一路向上更新的根节点。
- 总结:如果平衡因子变为1或者-1,继续向上更新,如果一直到根节点平衡因子还是1或者-1,则停止更新。
2.总结:平衡因子更新停止条件
- 更新后parent的平衡因子等于0,更新中parent的平衡因子变化为-1->0或者1->0,说明更新前 parent 子树一边高一边低,新增的结点插入在低的那边,插入后parent所在的子树高度不变,不会影响parent的父亲结点的平衡因子,更新结束。
- 更新后parent的平衡因子等于1或-1,更新前更新中parent的平衡因子变化为 0->1 或者 0->-1,说明更新前 parent 子树两边⼀样高,新增的插入结点后,parent所在的子树一边高一边低,parent所在的子树符合平衡要求,但是高度增加了1,会影响parent的父亲结点的平衡因子,所以要继续向上更新。
- 更新后parent的平衡因子等于2或-2,更新前更新中parent的平衡因子变化为1->2或者-1->-2,说明更新前parent子树一边高一边低,新增的插入结点在高的那边,parent所在的子树高的那边更高了,破坏了平衡,parent所在的子树不符合平衡要求,需要旋转处理,旋转的目标有两个:1、把 parent子树旋转平衡。2、降低parent子树的高度,恢复到插入结点以前的高度。所以旋转后也不需要继续往上更新,插入结束。
- 如果平衡因子变为1或者-1,继续向上更新,如果一直到根节点平衡因子还是1或者-1,则停止更新。(最坏情况)
更新平衡因子代码:
//插入
bool Insert(const pair<K, V>& kv)
{//树为空if (_root == nullptr){_root = new Node(kv);return true;}//树不为空Node* cur = _root;Node* perent = nullptr;while (cur){if (kv.first > cur->_kv.first){perent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first){perent = cur;cur = cur->_left;}else{return false;}}//插入cur = new Node(kv);if (kv.first > perent->_kv.first){perent->_right = cur;}else{perent->_left = cur;}cur->_perent = perent;//更新平衡因子while (perent){if (cur == perent->_left)//插入节点位于左边{--perent->_bf;//平衡因子--}else//插入节点位于右边{++perent->_bf;//平衡因子++}if (perent->_bf == 0)//平衡因子等于0{break;//直接结束}else if (perent->_bf == -1 || perent->_bf == 1)//平衡因子需要继续向上更新{cur = perent;//更新cur为父节点perent = perent->_perent;//更新父节点为父父节点}else if (perent->_bf == -2 || perent->_bf == 2)//不满足AVL树,需要旋转处理{//旋转//...break;//旋转完直接结束}else//平衡因子出现其它值,说明存在bug{assert(false);//说明出现bug,报错处理}}return true;
}
- 以上就是插入节点时关于平衡因子更新的代码,应该不难理解。
- 接下来就是关于旋转怎么操作的问题了。
三、旋转
旋转的原则:
- 保持搜索树的规则
- 让旋转的树从不满足平衡变平衡,其次降低旋转树的高度,恢复到以前的高度(注意不是平衡因子)
旋转总共分为四种:左单旋/右单旋/左右双旋/右左双旋。
1.右单旋
- 以上就是一个完整的右单旋过程,看不懂没关系,我们一步一步来。
- 上图展示的是10为根的树,有a/b/c抽象为三棵高度为h的子树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是一个整棵树中局部的子树的根。这里a/b/c是高度为h的子树, 是一种概括抽象表示,他代表了所有右单旋的场景,实际右单旋形态有很多种。
- 这里首先的问题就是为啥 a,b,c 要抽象成三个子树,而不是具体的节点,我们通过下面几个案例就会明白了。
(1)h=0
- 当 a,b,c 为h=0的三颗子树时,那么就只有两个节点,此时在 a 位置插入节点1,这时,根节点10的平衡因子变为-2,然后进行旋转操作,平衡了高度差。
- 所以当h=0时,右单旋只有一种情况。
(2)h=1
- h=1时,a,b,c就是一个节点的子树,此时也只有一种情况。
- 我们先不管如何旋转,先观察插入位置,插入位置都是最左边最高处。
(3)h=2
- h=2,h=2的子树有3种情况,如上图中的 X,Y,Z。
- 这里需要注意,子树a只能是X这一种情况,子树b,c可以是这三种任意一种,为什么呢?因为是在子树a插入新节点,然后需要保证影响到根节点10以此触发旋转操作,如果a树为Y,Z一种,那么新插入节点后a要么平衡要么不平衡,a平衡不会影响到根节点的平衡因子,a不平衡则自身会触发旋转操作,最终也不会影响根节点的平衡因子。所以a只能是X。
- 通过计算,h=2时,一共有36种情况,而这些情况都可以经过右单旋恢复平衡。
(4)h=3
- h=3时,高度为3的子树就有非常多的情况了,可以分为上图中X和y-C,y-C表示最后一层四个叶子结点保留任意1个/任意2个/任意3个的子树。
- 同h=2,这里a子树也只能是X这种情况,计算下来,一共有5400种情况,以至于画图时不得不使用抽象的子树a,b,c来表示。
- 所以你应该明白了为什么a,b,c要使用抽象的子树表示吧,就是因为有无数种情况,无数种情况都可以抽象成a,b,c来表示。
理解了a,b,c的含义,我们接下来观察旋转如何实现:
- 如图,在a子树中插入一个新结点,导致a子树的高度从h变成h+1,不断向上更新平衡因子,导致10的平衡因子从-1变成-2,10为根的树左右高度差超过1,违反平衡规则。10为根的树左边太高了,需要往右边旋转,控制两棵树的平衡。
- 旋转核心步骤,因为5 < b子树的值 < 10,将b变成10的左子树,10变成5的右子树,5变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵树的高度恢复到了插入之前的h+2,符合旋转原则。如果插入之前根节点为10的整棵树是一个局部子树,旋转后不会再影响上一层,插入结束了。
- 简单点说,为什么可以这样旋转,因为b和10节点都大于节点5,所以b和10节点都能作为5的右子树。因为b一开始是10的左边,所以让b直接成为10的左子树。最后整棵树的高度恢复了到了h+1,这样就平衡了高度差。
- 我们通过观察上面h=0,1,2,3的案例,就能发现,只要出现根节点的左孩子平衡因子变为-1,导致根节点平衡因子变为-2,就可以使用右单旋进行调整了。
(1)右单旋的代码实现:
//右单旋
void RotateR(Node* perent)
{Node* subL = perent->_left;//左孩子Node* subLR = subL->_right;//左孩子的右孩子//旋转过去perent->_left = subLR;subL->_right = perent;Node* ppNode = perent->_perent;//记录父父节点//更新父节点指针if (subLR)subLR->_perent = perent;perent->_perent = subL;if (perent == _root)//判断根节点{_root = subL;subL->_perent = nullptr;}else{if (ppNode->_left == perent){ppNode->_left = subL;}else{ppNode->_right = subL;}subL->_perent = ppNode;}//更新平衡因子subL->_bf = perent->_bf = 0;
}
代码解释:
- 因为右单旋受影响的节点有根节点、根节点的左孩子、左孩子的右子树。所以如上图,我们将根节点命名parent,左孩子命名subL,左孩子右子树命名subLR
- 首先第一步就是将b(subLR)旋转过去作为根节点(parent)的左子树,然后将parent点作为左孩子(subL)的右孩子。
- 然后因为是三叉链,所以我们还需要更新subLR和parent的父节点指针,这里需要注意的是subLR可能为0,如h=0时,所以我们需要if判断一下。
- 其次就是根节点的更新,因为可能是局部子树也可能是整棵树,所以在更新父节点指针前,先保存parent的父节点指针ppNode。我们先判断parent是不是根节点,是:我们就更新subL为新根节点然后令subL的父节点指针为空,因为根节点没有父节点。否:说明是局部子树,所以我们先判断parent是ppNode的哪个孩子节点,然后调整ppNode的该孩子节点指向subL,最后令subL的父节点指针指向ppNode。这样就完成了根节点的更新。
- 最后就是更新平衡因子了。我们根据上面和上上面的图就能得知,平衡因子受影响的只有parent和subL,a,b,c子树是作为整体不受影响,并且,parent和subL的平衡因子经过旋转后就一定为0,这一点通过图就能直观的得出来。
完成右单旋代码后,我们就可以更新一下插入函数了:
//插入
bool Insert(const pair<K, V>& kv)
{//树为空if (_root == nullptr){_root = new Node(kv);return true;}//树不为空Node* cur = _root;Node* perent = nullptr;while (cur){if (kv.first > cur->_kv.first){perent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first){perent = cur;cur = cur->_left;}else{return false;}}//插入cur = new Node(kv);if (kv.first > perent->_kv.first){perent->_right = cur;}else{perent->_left = cur;}cur->_perent = perent;//更新平衡因子while (perent){if (cur == perent->_left)//插入节点位于左边{--perent->_bf;//平衡因子--}else//插入节点位于右边{++perent->_bf;//平衡因子++}if (perent->_bf == 0)//平衡因子等于0{break;//直接结束}else if (perent->_bf == -1 || perent->_bf == 1)//平衡因子需要继续向上更新{cur = perent;//更新cur为父节点perent = perent->_perent;//更新父节点为父父节点}else if (perent->_bf == -2 || perent->_bf == 2)//不满足AVL树,需要旋转处理{//旋转if (perent->_bf == -2 && cur->_bf == -1)//左边高{RotateR(perent);//右单旋}//剩余情况//...break;//旋转完直接结束}else{assert(false);//说明出现bug,报错处理}}return true;
}
- 右旋转,顾名思义就是朝右边旋转。
- 条件:纯粹的左边高,观察上面的图,也就是根节点(parent)的左孩子节点(cur)的左子树高(a),导致根节点平衡因子超了。
- 注意:根节点的左孩子节点的左子树,也就是a,a的形状是不确定的,所以只关注cur的平衡因子。
- 所以右旋转条件写成代码就是:perent->_bf == -2 && cur->_bf == -1
2.左单旋
理解了右单旋,左单旋就好说了。
- 如上图展示的是10为根的树,有a/b/c抽象为三棵高度为h的子树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是一个整棵树中局部的子树的根。这里a/b/c是高度为h的子树, 是一种概括抽象表示,他代表了所有右单旋的场景,实际右单旋形态有很多种,具体跟上面左旋类似。
- 在a子树中插入一个新结点,导致a子树的高度从h变成h+1,不断向上更新平衡因子,导致10的平衡因子从1变成2,10为根的树左右高度差超过1,违反平衡规则。10为根的树右边太高了,需要往左边旋转,控制两棵树的平衡。
- 旋转核心步骤,因为10 < b子树的值 <15,将b变成10的右子树,10变成15的左子树,15变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的h+2,符合旋转原则。如果插入之前10整棵树是一个局部子树,旋转后不会再影响上一层,插入结束了。最后树的高度依旧恢复到了h+1
- 总体上说就是右单旋的反方向版,受影响的3个位置:根节点(parent)、右孩子节点(subR)、右孩子的左子树(subRL)。
(1)左单旋代码实现
//左单旋
void RotateL(Node* perent)
{Node* subR = perent->_right;Node* subRL = subR->_left;//旋转过去perent->_right = subRL;subR->_left = perent;Node* ppNode = perent->_perent;//父父节点//更新父节点指针if (subRL)subRL->_perent = perent;perent->_perent = subR;//判断父父节点if (perent == _root){_root = subR;subR->_perent = nullptr;}else{if (ppNode->_left == perent){ppNode->_left = subR;}else{ppNode->_right = subR;}subR->_perent = ppNode;}subR->_bf = perent->_bf = 0;
}
- 和右单旋一样,先将b旋转过去,将subRL变成parent的右子树,将parent变为subR的左孩子,随后更新父节点指针,然后更新根节点,最后更新parent和subR的平衡因子。
完成左单旋后可以更新一下插入函数:
//插入
bool Insert(const pair<K, V>& kv)
{//树为空if (_root == nullptr){_root = new Node(kv);return true;}//树不为空Node* cur = _root;Node* perent = nullptr;while (cur){if (kv.first > cur->_kv.first){perent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first){perent = cur;cur = cur->_left;}else{return false;}}//插入cur = new Node(kv);if (kv.first > perent->_kv.first){perent->_right = cur;}else{perent->_left = cur;}cur->_perent = perent;//更新平衡因子while (perent){if (cur == perent->_left)//插入节点位于左边{--perent->_bf;//平衡因子--}else//插入节点位于右边{++perent->_bf;//平衡因子++}if (perent->_bf == 0)//平衡因子等于0{break;//直接结束}else if (perent->_bf == -1 || perent->_bf == 1)//平衡因子需要继续向上更新{cur = perent;//更新cur为父节点perent = perent->_perent;//更新父节点为父父节点}else if (perent->_bf == -2 || perent->_bf == 2)//不满足AVL树,需要旋转处理{//旋转if (perent->_bf == -2 && cur->_bf == -1)//左边高{RotateR(perent);//右单旋}else if (perent->_bf == 2 && cur->_bf == 1)//右边高{RotateL(perent);//左单旋}//其他情况...break;//旋转完直接结束}else{assert(false);//说明出现bug,报错处理}}return true;
}
- perent->_bf == 2 && cur->_bf == 1,也就是只有parent平衡因子为2并且subR(cur)平衡因子为1的情况,也就是纯粹的右边高,这时使用左单旋。
3.左右双旋
(1)问题引入
- 通过上面两图可以看到,左边高时,如果插入位置不是在a子树,而是插入在b子树,b子树高度从h变成h+1,引发旋转,右单旋无法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的是纯粹的左边高,但是插入在b子树中,10为跟的子树不再是单纯的左边高,对于10是左边高,但是对于5是右边高,这时需要用两次旋转才能解决。
- 判断是不是纯粹的左边高或者右边高,根据图,从新插入节点出发到根节点,如果是一条直线那么就是纯粹的一边高,如果是折线就不是。
(2)解决方法
首先我们子树的表示就直接用抽象图,前面已经解释过了,其实抽象子树最好一点就是其平衡因子不会发生变化,因为是当成一个整体移动。
- 第一步:将子树b拆分,拆分成一个具体节点(比5大比10小)和两棵抽象子树e和f
- 第二步:节点5为首的左子树,单独对这个左子树进行左单旋,将整棵树变为纯粹的左边高。
- 第三步:对整棵树进行右单旋,注意此时需要将上图中节点5的子树和子树f视为两颗抽象子树
因为先进行左单旋,后进行右单旋,所以取名左右双旋
(3)平衡因子更新
其实旋转过程很简单,只需要调用上面已经写好的单旋函数即可。真正难点是更新平衡因子,因为子树b被拆分成了e和f,那么新插入节点就有可能位于e也可能位于f,根据上图,e和f最终成为了根节点的左右孩子节点的右左子树。所以就会影响到根节点和左右孩子节点的平衡因子。
- 第一种:插入在e
- 此时parent=1,subL=0,subLR=0
- 第二种:插入在f
- 此时parent=0,subL=-1,subLR=0
- 其实还有第三种:b子树高度h=0,也就是b就是新插入节点
- 此时parent=0,subL=0,subLR=0
- 现在问题是如何判断插入位置?我们可以根据subLR的平衡因子进行判断,subLR=-1说明左边插入,subLR=1说明右边插入,subLR=0说明自己就是新增节点。
(5)左右双旋代码实现
//左右双旋
void RotateLR(Node* perent)
{Node* subL = perent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;//单旋前先记录平衡因子RotateL(perent->_left);//先对左子树进行左单旋RotateR(perent);//对整体进行右单旋if (bf == -1)//左边高{perent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1)//右边高{perent->_bf = 0;subL->_bf = -1;subLR = 0;}else if (bf == 0)//自己就是新增节点{perent->_bf = 0;subL->_bf = 0;subLR = 0;}else//如果真走到这里说明有bug{assert(false);}
}
- 首先需要注意的是,调用单旋会把平衡因子更新为0,所以需要提前记录subLR的平衡因子。
- 然后,既然调用单旋会把平衡因子更新为0,那为什么还需要写bf==0的情况,这其实是一个好习惯,因为写了bf==0的情况那么无论单旋函数会不会将平衡因子变为0,都不会影响双旋函数更新平衡因子,这就是降低耦合性。
最后,我们继续完善一下插入函数代码中的旋转条件:
//插入
bool Insert(const pair<K, V>& kv)
{//树为空if (_root == nullptr){_root = new Node(kv);return true;}//树不为空Node* cur = _root;Node* perent = nullptr;while (cur){if (kv.first > cur->_kv.first){perent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first){perent = cur;cur = cur->_left;}else{return false;}}//插入cur = new Node(kv);if (kv.first > perent->_kv.first){perent->_right = cur;}else{perent->_left = cur;}cur->_perent = perent;//更新平衡因子while (perent){if (cur == perent->_left)//插入节点位于左边{--perent->_bf;//平衡因子--}else//插入节点位于右边{++perent->_bf;//平衡因子++}if (perent->_bf == 0)//平衡因子等于0{break;//直接结束}else if (perent->_bf == -1 || perent->_bf == 1)//平衡因子需要继续向上更新{cur = perent;//更新cur为父节点perent = perent->_perent;//更新父节点为父父节点}else if (perent->_bf == -2 || perent->_bf == 2)//不满足AVL树,需要旋转处理{//旋转if (perent->_bf == -2 && cur->_bf == -1)//左边高{RotateR(perent);//右单旋}else if (perent->_bf == 2 && cur->_bf == 1)//右边高{RotateL(perent);//左单旋}else if (perent->_bf == -2 && cur->_bf == 1)//左的中间高{RotateLR(perent);//左右双旋}//剩余情况...break;//旋转完直接结束}else{assert(false);//说明出现bug,报错处理}}return true;
}
- 根据图就能发现,需要左右双旋的条件就是,subL的平衡因子=1.
- 也就是:perent->_bf == -2 && cur->_bf == 1
4.右左双旋
同理,我们先对右子树进行右单旋,再对整树进行左单旋。然后分三种情况讨论平衡因子的更新
- 跟左右双旋类似,下面我们将a/b/c子树抽象为高度h的AVL子树进行分析,另外我们需要把b子树的细节进⼀步展开为12和左子树高度为h-1的e和f子树,因为我们要对b的父亲15为旋转点进行右单旋,右单旋需要动b树中的右子树。b子树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察12的平衡因子不同,这里我们要分三个场景讨论。
- 场景1:h>=1时,新增结点插入在e子树,e子树高度从h-1变为h并不断更新12->15->10平衡因子,引发旋转,其中12的平衡因子为-1,旋转后10和12平衡因子为0,15平衡因子为1。
- 场景2:h>=1时,新增结点插入在f子树,f子树高度从h-1变为h并不断更新12->15->10平衡因子, 引发旋转,其中12的平衡因子为1,旋转后15和12平衡因子为0,10平衡因子为-1。
- 场景3:h==0时,a/b/c都是空树,b自己就是⼀个新增结点,不断更新15->10平衡因子,引发旋转,其中12的平衡因子为0,旋转后10和12和15平衡因子均为0
(1)右左双旋代码实现
//右左双旋
void RotateRL(Node* perent)
{Node* subR = perent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;//先记录平衡因子RotateR(perent->_right);//先右单旋RotateL(perent);//再左单旋if (bf == -1){perent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else if (bf == 1){perent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else if (bf == 0){perent->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}else{assert(false);}
}
(2)插入函数最终版
//插入
bool Insert(const pair<K, V>& kv)
{//树为空if (_root == nullptr){_root = new Node(kv);return true;}//树不为空Node* cur = _root;Node* perent = nullptr;while (cur){if (kv.first > cur->_kv.first){perent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first){perent = cur;cur = cur->_left;}else{return false;}}//插入cur = new Node(kv);if (kv.first > perent->_kv.first){perent->_right = cur;}else{perent->_left = cur;}cur->_perent = perent;//更新平衡因子while (perent){if (cur == perent->_left)//插入节点位于左边{--perent->_bf;//平衡因子--}else//插入节点位于右边{++perent->_bf;//平衡因子++}if (perent->_bf == 0)//平衡因子等于0{break;//直接结束}else if (perent->_bf == -1 || perent->_bf == 1)//平衡因子需要继续向上更新{cur = perent;//更新cur为父节点perent = perent->_perent;//更新父节点为父父节点}else if (perent->_bf == -2 || perent->_bf == 2)//不满足AVL树,需要旋转处理{//旋转if (perent->_bf == -2 && cur->_bf == -1)//左边高{RotateR(perent);//右单旋}else if (perent->_bf == 2 && cur->_bf == 1)//右边高{RotateL(perent);//左单旋}else if (perent->_bf == -2 && cur->_bf == 1)//左的中间高{RotateLR(perent);//左右双旋}else if (perent->_bf == 2 && cur->_bf == -1)//右的中间高{RotateRL(perent);//右左双旋}else{assert(false);}break;//旋转完直接结束}else{assert(false);//说明出现bug,报错处理}}return true;
}
最后稍微总结一下双旋的思想:
- 由于插入位置不是纯粹的一边高,所以先针对插入节点的那棵子树,使用单旋将其调整为纯粹的一边高,最终对整体再进行一次单旋即可。
四、AVL树的查找
查找逻辑和普通的二叉搜索树一样,这里不再赘述
//查找
Node* Find(const K& key)
{Node* cur = _root;while (cur){if (key > cur->_kv.first){cur = cur->_right;}else if (key < cur->_kv.first){cur = cur->_left;}else{return cur;}}return nullptr;
}
五、AVL树的平衡检测
主要是检查我们写的代码有没有bug
- 为此,我们需要实现中序遍历:
void _InOrder(Node* root)//中序遍历 {if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right); }
- 当然,和搜索二叉树一样,为了方便使用,选择封装一层,将上面的中序遍历代码放到类的private私有成员中。
- 再将以下封装代码放在类的public共用成员中:
//中序遍历 void InOrder() {_InOrder(_root); }
- 这样直接调用无参的中序遍历就可以使用了,避免了传参。
(1)简单的初步测试
测试代码:
#include <iostream>
#include "AVLTree.h"
using namespace std;void TestAVLTree1()
{AVLTree<int, int> t;//常规测试用例:int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };//特殊的需要双旋场景的测试用例:// int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };//插入for (auto e : a){t.Insert({ e,e });}//中序遍历t.InOrder();
}int main()
{TestAVLTree1();return 0;
}
运行结果:
- 很明显,数据都是从小到大排序的,符号二叉搜索树的特性,说明大方向没问题。
- 但是现在还不能完全保证代码正确,因为有序只能说明是搜索树,不能说明树是平衡的。
- 为此我们还需要写一个程序检查树是否平衡。
(2)平衡检测
- 首先,判断一棵搜索树是否平衡,我们能使用平衡因子判断吗?答案是不能的,因为平衡因子本身就可能是错的,毕竟平衡因子是我们自己进行更新的。
- 这时,我们就需要使用朴实的方法进行检测,如上图,我们需要计算每棵子树的高度,使用高度差来判断树是否平衡,比如上面根节点8,我们先计算出它的左右子树高度,判断高度差的绝对值是否大于2,顺带可以与节点8的平衡因子进行比较判断平衡因子本身是否正确。
- 使用这种方法判断每个节点的左右子树。
实现方法:
- 我们首先需要实现计算高度的函数Height,这个在树的章节详细解释过。
int _Height(Node* root)//计算高度 {if (root == nullptr)return 0;int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1; }
- 然后就是判断平衡的IsBalanceTree函数,这个就是通过计算的左右子树高度,递归来判断树是否平衡,很好理解。比较好的一点就是可以指出出现异常的节点。
bool _IsBalanceTree(Node* root)//判断是否为AVL树 {//空树也是AVL树if (nullptr == root)return true;//计算pRoot结点的平衡因子:即pRoot左右子树的高度差int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int diff = rightHeight - leftHeight;// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者// pRoot平衡因子的绝对值超过1,则⼀定不是AVL树if (abs(diff) >= 2){cout << root->_kv.first << "高度差异常" << endl;return false;}if (root->_bf != diff){cout << root->_kv.first << "平衡因子异常" << endl;return false;}//pRoot的左和右如果都是AVL树,则该树⼀定是AVL树return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right); }
- 这两个函数我们都可以套一层使用,避免传参。
//判断是否为AVL树 bool IsBalanceTree() {return _IsBalanceTree(_root); }//计算高度 int Height() {return _Height(_root); }
测试方法:
我们使用十万个随机值进行插入,然后判断树是否平衡。
void TestAVLTree2()
{const int N = 100000;//测试用例个数vector<int> v;v.reserve(N);//提前扩容srand(time(0));//设置随机数种子for (size_t i = 0; i < N; i++)//生成随机值{v.push_back(rand() + i);}AVLTree<int, int> t;int begin1 = clock();//记录时间for (auto e : v){t.Insert({ e,e });//插入随机值}int end1 = clock();cout << "Insert: " << end1 - begin1 << "(毫秒)" << endl;if (t.IsBalanceTree())//判断平衡{cout << "平衡,是AVL树" << endl;}else{cout << "不平衡,不是AVL树" << endl;}cout << "Hight: " << t.Height() << "(层)" << endl;//计算树的高度cout << "Size: " << t.Size() << "(个)" << endl;//计算树的大小int begin2 = clock();//测试一下查找效率for (size_t i = 0; i < N; i++){t.Find(rand() + i);}int end2 = clock();cout << "Find: " << end2 - begin2 << "(毫秒)" << endl;
}
补充一下size方法:
public://计算大小int Size(){return _Size(_root);}
private:int _Size(Node* root){if (root == nullptr)return 0;int left = _Size(root->_left);int right = _Size(root->_right);return left + right + 1;}
运行结果:
- 你可能发现树的大小只有6万多个,这是因为我们实现的AVL树不支持重复数据。
六、AVL树的删除
关于删除部分,本文章没有做出详解,原因是它比插入部分更加复杂,像笔试面试一般也不会考:
删除大致分为以下三步:
- 按照二叉搜索树树的方式进行删除(主页有)
- 更新平衡因子
- 旋转
有兴趣的可以参考:《殷人昆 数据结构:用面向对象方法与C++语言描述》中有讲解
总结
以上就是本文的全部内容了,感谢支持!
相关文章:
【C++】AVL树实现
目录 前言 一、AVL树的概念 二、AVL树的实现 1.基本框架 2.AVL树的插入 三、旋转 1.右单旋 2.左单旋 3.左右双旋 4.右左双旋 四、AVL树的查找 五、AVL树的平衡检测 六、AVL树的删除 总结 前言 本文主要讲解AVL树的插入,AVL树是在二叉搜索树的基础上&a…...
49.EFT测试与静电测试环境和干扰特征分析
EFT测试与静电测试环境和干扰特征分析 1. EFT/B电快速瞬变脉冲群测试及干扰特征分析2. EFT的干扰特征分析与滤波方法3. ESD静电测试及干扰特征分析 1. EFT/B电快速瞬变脉冲群测试及干扰特征分析 EFT测试是模拟在大的感性设备断开瞬间产生的快速瞬变脉冲群对被测设备的影响。 E…...
html body 设置heigth 100%,body内元素设置margin-top出滚动条(margin 重叠问题)
今天在用移动端的时候发现个问题,html,body 设置 height:100% 会出现纵向滚动条 <!DOCTYPE html> <html> <head> <title>html5</title> <style> html, body {height: 100%; } * {margin: 0;padding: 0; } </sty…...
1688 API 自动化采集实践:商品详情实时数据接口开发与优化
在电商行业竞争日益激烈的当下,实时获取 1688 平台商品详情数据,能够帮助商家分析市场动态、优化选品策略,也能助力数据分析师洞察行业趋势。通过 API 自动化采集商品详情数据,不仅可以提高数据获取效率,还能保证数据的…...
Transformer Decoder-Only 参数量计算
Transformer 的 Decoder-Only 架构(如 GPT 系列模型)是当前大语言模型的主流架构,其参数量主要由以下几个部分组成: 嵌入层(Embedding Layer)自注意力层(Self-Attention Layers)前馈…...
苍穹外卖(数据统计–Excel报表)
数据统计(Excel报表) 工作台 接口设计 今日数据接口 套餐总览接口 菜品总览接口 订单管理接口 编辑代码导入 功能测试 导出运营数据Excel报表 接口设计 代码开发 将模板文件放到项目中 导入Apache POI的maven坐标 在ReportCont…...
如何实现Flask应用程序的安全性
在 Flask 应用中,确保安全性非常关键,尤其是当你将应用部署到公网环境中时。Flask 本身虽然轻量,但通过组合安全策略、扩展库和最佳实践,可以构建一个非常安全的 Web 应用。 一、常见 Flask 安全风险(必须防护…...
【Redis】Redis的主从复制
文章目录 1. 单点问题2. 主从模式2.1 建立复制2.2 断开复制 3. 拓扑结构3.1 三种结构3.2 数据同步3.3 复制流程3.3.1 psync运行流程3.3.2 全量复制3.3.3 部分复制3.3.4 实时复制 1. 单点问题 单点问题:某个服务器程序,只有一个节点(只搞一个…...
趣味编程:四叶草
概述:在万千三叶草中寻觅,只为那一抹独特的四叶草之绿,它象征着幸运与希望。本篇博客主要介绍四叶草的绘制。 1. 效果展示 绘制四叶草的过程是一个动态的过程,因此博客中所展示的为绘制完成的四叶草。 2. 源码展示 #define _CR…...
HTTP 响应状态码总结
一、引言 HTTP 响应状态码是超文本传输协议(HTTP)中服务器对客户端(通常是 Web 浏览器)请求的响应指示。这些状态码是三位数字代码,用于告知客户端请求的结果,包括请求是否成功。响应被分为五个类别&#…...
C语言常见的文件操作函数总结
目录 前言 一、打开和关闭 1.fopen 细节 2.fclos 基本用法示例 二、读写 1.fputc和fgetc 1)fputc 细节 基本用法示例 2)fgetc 细节 基本用法示例 2.fputs和fgets 1)fputs 细节 基本用法示例 2)fgets 细节 基本用法示例 3)puts的使用,以及为什…...
卫宁健康WiNGPT3.0与WiNEX Copilot 2.2:医疗AI创新的双轮驱动分析
引言:医疗AI的双翼时代 在医疗信息化的浪潮中,人工智能技术的深度融入正在重塑整个医疗行业。卫宁健康作为国内医疗健康和卫生领域数字化解决方案的领军企业,持续探索AI技术在医疗场景中的创新应用。2025年5月10日,在第29届中国医院信息网络大会(CHIMA2025)上,卫宁健康…...
【GPT入门】第38课 RAG评估指标概述
这里写自定义目录标题 一、RAG评估指标二、ragas 评估三、trulens 一、RAG评估指标 二、ragas 评估 2.1 ragas介绍 开源地址:https://github.com/explodinggradients/ragas 官方文档:https://docs.ragas.io/en/stable/从文本生成和文本召回两个维度&am…...
深度剖析多模态大模型中的视频编码器算法
写在前面 随着多模态大型语言模型(MLLM)的兴起,AI 理解世界的能力从静态的文本和图像,进一步拓展到了动态的、包含丰富时空信息的视频。视频作为一种承载了动作、交互、场景变化和声音(虽然本文主要聚焦视觉部分)的复杂数据形式,为 MLLM 提供了理解真实世界动态和因果关…...
【递归、搜索与回溯算法】导论
📝前言说明: 本专栏主要记录本人递归、搜索与回溯算法的学习以及LeetCode刷题记录,按专题划分每题主要记录:(1)本人解法 本人屎山代码;(2)优质解法 优质代码ÿ…...
《智能网联汽车 自动驾驶功能道路试验方法及要求》 GB/T 44719-2024——解读
目录 1. 适用范围 2. 关键术语 3. 试验条件 3.1 试验道路 3.2 试验车辆 3.3 试验设备 3.4 试验时间 4. 试验方法及要求 4.1 功能激活 4.2 动态驾驶任务执行 4.3 动态驾驶任务后援 4.4 状态提示 5. 附录A(核心环境要素) 6. 实施要点 原文链接…...
path环境变量满了如何处理,分割 PATH 到 Path1 和 Path2
要正确设置 Path1 的值,你需要将现有的 PATH 环境变量 中的部分路径复制到 Path1 和 Path2 中。以下是详细步骤: 步骤 1:获取当前 PATH 的值 打开环境变量窗口: 按 Win R,输入 sysdm.cpl,点击 确定。在 系…...
实战项目1(02)
目录 任务场景一 【sw1和sw2的配置如下】 任务场景二 【sw3的配置】 【sw4-6的配置】 任务场景一 某公司有生产、销售、研发、人事、财务等多个部门,这些部门分别连接在两台交换机(SW1和SW2)上,现要求给每个部门划分相应的V…...
m1 安装 Elasticsearch、ik、kibana
一、下载安装ES 1、下载地址 ES|download 2、安装 将下载的安装包解压到 要安装的文件目录 关闭 ES 的安全模式 本地文本编辑器打开elasticsearch.yml配置文件,将红箭头指的地方 改为 false3、启动 ES 启动命令 进入 ES 的安装目录,进入bin文件目…...
游戏引擎学习第273天:动画预览
回顾并为一天的内容定下基调 。目前我们正在编写角色的移动代码,实际上,我们已经在昨天完成了一个简单的角色跳跃的例子。所以今天的重点是,开始更广泛地讨论动画,因为我们希望对现有的动画进行调整,让它看起来更加令…...
JVM中的安全点是什么,作用又是什么?
JVM中的安全点(Safepoint) 是Java虚拟机设计中的一个关键机制,主要用于协调所有线程的执行状态,以便进行全局操作(如垃圾回收、代码反优化等)。它的核心目标是确保在需要暂停所有线程时,每个线程…...
游戏引擎学习第271天:生成可行走的点
回顾并为今天的内容设定背景 我们昨天开始编写一些游戏逻辑相关的内容,虽然这部分不是最喜欢的领域,更偏好底层引擎开发,但如果要独立完成一款游戏,游戏逻辑也必须亲自处理。所以我们继续完善这部分内容。事实上,接下…...
FlySecAgent:——MCP全自动AI Agent的实战利器
最近,出于对人工智能在网络安全领域应用潜力的浓厚兴趣,我利用闲暇时间进行了深入研究,并成功开发了一款小型轻量化的AI Agent安全客户端FlySecAgent。 什么是 FlySecAgent? 这是一个基于大语言模型和MCP(Model-Contr…...
DAMA车轮图
DAMA车轮图是国际数据管理协会(DAMA International)提出的数据管理知识体系(DMBOK)的图形化表示,它以车轮(同心圆)的形式展示了数据管理的核心领域及其相互关系。以下是基于用户提供的关键词对D…...
使用vue3-seamless-scroll实现列表自动滚动播放
vue3-seamless-scroll组件支持上下左右无缝滚动,单步滚动,并且支持复杂图标的无缝滚动。 核心特性 多方向无缝滚动 支持上下、左右四个方向的自动滚动,通过 direction 参数控制(默认 up),适用于新闻轮播、…...
Scrapyd 详解:分布式爬虫部署与管理利器
Scrapyd 是 Scrapy 官方提供的爬虫部署与管理平台,支持分布式爬虫部署、定时任务调度、远程管理爬虫等功能。本文将深入讲解 Scrapyd 的核心功能、安装配置、爬虫部署流程、API 接口使用,以及如何结合 Scrapy-Redis 实现分布式爬虫管理。通过本文&#x…...
mac环境配置(homebrew版)
文章目录 【环境配置】HomebrewGitJavaMavenMySQLRedisNacosNode.js 【拓展-mac常见问题】mac文件损坏问题mac必装软件(Java开发版)zsh和bash配置文件区别 【参考资料】 查看每个版本可以用命令brew info xxx ps:每一个环境安装完之后都要关掉…...
19、DeepSeek LLM论文笔记
DeepSeek LLM 1. **引言**2、架构3、多步学习率调度器4、缩放定律1.超参数的缩放定律2. 估计最优模型和数据缩放 5、GQA分组查询注意力汇总deepseekDeepSeek LLM 技术文档总结1. **引言**2. **预训练**3. **扩展法则**4. **对齐(Alignment)**5. **评估*…...
基于LLM的6G空天地一体化网络自进化安全框架
摘要 最近出现的6G空天地一体化网络(SAGINs)整合了卫星、空中网络和地面通信,为各种移动应用提供普遍覆盖。然而,SAGINs的高度动态、开放和异构的性质带来了严重的安全问题。构建SAGINs的防御体系面临两个初步挑战:1)…...
【Mac 从 0 到 1 保姆级配置教程 12】- 安装配置万能的编辑器 VSCode 以及常用插件
文章目录 前言安装 VSCode基础配置常用插件1. 通用开发工具2. 编程语言支持3. 数据库工具4. 主题与界面美化5. 效率工具6. Markdown 工具7. 容器开发8. AI 辅助编程9. 团队协作 最后系列教程 Mac 从 0 到 1 保姆级配置教程目录,点击即可跳转对应文章: 【…...
数据库与SQL核心技术解析:从基础到JDBC编程实战
数据库技术作为现代信息系统的核心,贯穿于数据存储、查询优化、事务管理等关键环节。本文将系统讲解数据库基础知识、SQL语言核心操作、索引与事务机制,并结合Java数据库编程(JDBC)实践,助你构建完整的数据库技术体系。…...
JUC并发编程(上)
一、JUC学习准备 核心知识点:进程、线程、并发(共享模型、非共享模型)、并行 预备知识: 基于JDK8,对函数式编程、lambda有一定了解 采用了slf4j打印日志 采用了lombok简化java bean编写 二、进程与线程 进程和线程概念 两者对比…...
postgres--MVCC
PostgreSQL 的 MVCC(Multi-Version Concurrency Control,多版本并发控制) 是其实现高并发和高性能的核心机制,支持多个事务同时读写数据库而无需加锁阻塞。它的核心思想是通过保留数据的多个版本来避免读写冲突,从而提…...
nanodet配置文件分析
以下是针对 NanoDet-Plus-M-1.5x_416 配置文件的逐模块解析,以及调整参数的作用和影响范围: 1. 模型架构(model) Backbone(骨干网络) backbone:name: ShuffleNetV2model_size: 1.5x # 控制网络宽度&…...
【Linux网络】HTTP
应用层协议 HTTP 前置知识 我们上网的所有行为都是在做IO,(我的数据给别人,别人的数据给我)图片。视频,音频,文本等等,都是资源答复前需要先确认我要的资源在哪台服务器上(网络IP&…...
Unity中AssetBundle使用整理(一)
一、AssetBundle 概述 AssetBundle 是 Unity 用于存储和加载游戏资源(如模型、纹理、预制体、音频等)的一种文件格式。它允许开发者将游戏资源打包成独立的文件,在运行时动态加载,从而实现资源的按需加载、更新以及减小初始安装包…...
CMOS内存的地址空间在主内存空间中吗?
CMOS内存(即CMOS RAM)的地址空间不位于主内存地址空间(如0x00000-0xFFFFF)内,而是通过独立的I/O端口地址进行访问,具体如下: 1. CMOS内存的物理存储与地址机制 CMOS RAM芯片通常集成在主板…...
大模型应用中常说的Rerank是什么技术?
Rerank技术详解 一、定义与基本原理 Rerank(重排序)是一种在信息检索系统中用于优化搜索结果排序的技术,其核心目标是通过二次评估和排序候选文档,提升结果的相关性和准确性。其运作机制通常分为两阶段: 初步检索:使用传统方法(如BM25关键词匹配或Embedding向量检索)…...
Python-MCPInspector调试
Python-MCPInspector调试 使用FastMCP开发MCPServer,熟悉【McpServer编码过程】【MCPInspector调试方法】-> 可以这样理解:只编写一个McpServer,然后使用MCPInspector作为McpClient进行McpServer的调试 1-核心知识点 1-熟悉【McpServer编…...
C 语言数据结构基石:揭开数组名的面纱与计算数组大小
各类资料学习下载合集 https://pan.quark.cn/s/8c91ccb5a474 在前面的文章中,我们已经学习了 C 语言一维数组的定义和初始化。我们知道数组是用来存储一系列相同类型数据的集合,并通过下标来访问每个元素。但是,除了通过下标访问单个元素,数组名本身在 C 语言中也…...
Java高频面试之并发编程-15
hello啊,各位观众姥爷们!!!本baby今天又来报道了!哈哈哈哈哈嗝🐶 面试官:as-if-serial 是什么?单线程的程序一定是顺序执行的吗? as-if-serial 规则 定义: …...
MySQL数据库迁移SQL语句指南
MySQL数据库迁移SQL语句指南 一、基础迁移方法 1. 使用mysqldump进行全量迁移 -- 导出源数据库(在命令行执行) mysqldump -u [源用户名] -p[源密码] --single-transaction --routines --triggers --events --master-data2 [数据库名] > migration…...
Vue:生命周期钩子
深入理解 Vue 的钩子函数(生命周期函数) Vue 的钩子函数(生命周期函数)是 Vue 实例在不同阶段自动调用的函数。可以在 Vue 实例的创建、更新、销毁等阶段插入自己的逻辑。 钩子函数的作用 想象一下,Vue 实例的生命周…...
深入理解设计模式之原型模式(Prototype Pattern)
一、为什么需要原型模式? 在传统对象创建方式中,我们通过new关键字直接调用构造函数创建实例。但当遇到以下场景时: 对象初始化需要消耗大量资源(如数据库连接)需要创建的对象与现有实例高度相似希望屏蔽对象创建的复…...
K8S cgroups详解
以下是 Kubernetes 中 cgroups(Control Groups) 的详细解析,涵盖其核心原理、在 Kubernetes 中的具体应用及实践操作: 一、cgroups 基础概念 1. 是什么? cgroups 是 Linux 内核提供的 资源隔离与控制机制,…...
ARMV8 RK3399 u-boot TPL启动流程分析 --start.S
上电后运行的第一支文件:arch/arm/cpu/armv8/start.S CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK1 #include <asm/arch/boot0.h> 跳转到 arch/arm/include/asm/arch-rockchip/boot0.h CONFIG_SPL_BUILD1 b 1f ROCKCHIP_EARLYRETURN_TO_BROMno TINY_FRAMEWORKno …...
【网络原理】数据链路层
目录 一. 以太网 二. 以太网数据帧 三. MAC地址 四. MTU 五. ARP协议 六. DNS 一. 以太网 以太网是一种基于有线或无线介质的计算机网络技术,定义了物理层和数据链路层的协议,用于在局域网中传输数据帧。 二. 以太网数据帧 1)目标地址 …...
保姆级教程|YOLO11改进】【卷积篇】【4】使用RFAConv感受野注意力卷积,重塑空间特征提取,助力高效提点
《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…...
虚幻引擎5-Unreal Engine笔记之常用核心类的继承关系
虚幻引擎5-Unreal Engine笔记之常用核心类的继承关系 code review! 文章目录 虚幻引擎5-Unreal Engine笔记之常用核心类的继承关系1.UE5中常用核心类的继承关系1.1.简化版1.2.plantuml图1.3.plantuml代码1.4.关于大写字母U和A2.1.组件和类的关系,组件也是类吗&…...
力扣2680题解
记录 2025.5.9 题目: 思路: 1.计算初始或值:首先计算数组中所有元素的按位或结果 allOr,这表示在不进行任何左移操作时数组的或值。 2.计算固定或值:在计算 allOr 的同时,计算一个 fixed 值,…...