[C++][STL源码剖析] 详解AVL树的实现

目录

1.概念

2.实现

2.1 初始化

2.2 插入

2.2.1 旋转(重点)

左单旋

右单旋

双旋

2.❗ 双旋后,对平衡因子的处理

2.3 判断测试

完整代码:

拓展:删除


1.概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下

因此,两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年 发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右 子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

高度之差=右子树高度 - 左子树高度

AVL == 高度平衡二叉树搜索树

由于AVL树的自平衡特性,它适用于需要频繁插入和删除操作的场景,尤其是对于需要快速搜索和有序遍历的数据集合。

平衡为什么不是高度差相等,而是高度差不超过 1?

为了涵盖更多的情况,例如为节点个数为 4 如下,高度差 1 也相对平衡了

为什么 满二叉树和 AVL 树是同一个 level?

增删查改:高度次->O(logN)

最后一 h 层有 2^(h-1)个节点

满二叉树 2^h-1=N

AVL 树 2^h-X=N //最后一行还存在缺失

X 范围:[1, 2^(h-1)-1]

满二叉树和 AVL 树 在量级上都是约等于 log N 的

2.实现

2.1 初始化

AVL树的节点定义包括以下几个属性:

  • 值:每个节点存储的值,可以是任意类型,通常是一个关键字或数据。
  • 左子节点指针:指向当前节点的左子节点的指针。左子节点的值应该小于或等于当前节点的值。
  • 右子节点指针:指向当前节点的右子节点的指针。右子节点的值应该大于当前节点的值。
  • 父节点指针:指向当前节点的父节点的指针。根节点的父节点指针为空。(为了便于后面更好的更新设计的)
  • 平衡因子:表示当前节点的左子树高度和右子树高度之差。平衡因子可以为-1、0或1。

下面是一个示例代码来定义一个AVL树的节点结构:

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;AVLTreeNode(const pair<K, V>& kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0) //balance factor{}
};

2.2 插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子
template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:bool Insert(const pair<K, V>& kv){//if (_root == nullptr){_root = new Node(kv);return true;}//搜索找到位置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;//再指回去

插入这部分代码倒是没问题,难的是新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否破坏了AVL树,破坏了AVL树就需要旋转调整再次变成AVL树。

如何根据这三种情况来实现插入和对高度的管理?

新增支点:右子树高度++,左子树高度--

插入会对祖先产生影响,平衡因子为 0 了,就再不会对上面的祖先产生影响了,变 0 就平衡了

对以上插入情况,分析可知

是否继续向上更新依旧:子树的高度是否变化

  1. parent->_bf == 0,说明之前parent->_bf是1或者-1,说明之前parent一边高一边低,而这次的插入是把矮的那边填上了,parent所在子树高度不变,不需要往上继续更新
  2. parent->_bf == 1 或者 -1,说明之前parent->_bf为0,两边一样高,现在插入使一边变得更高了,parent所在子树高度变了,继续往上更新
  3. parent->_bf == 2 或者 -2,说明之前parent->_bf是1或者-1,现在插入导致严重不平衡,违反规则,就地处理—>旋转

什么时候结束呢?

_bf==0 或者更新到了根节点的时候

实现平衡因子的更新

// ... 控制平衡// 更新平衡因子while (parent){if (cur == parent->_left){parent->_bf--;}else // if (cur == parent->_right){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){// 子树不平衡了,需要旋转if (parent->_bf == 2 || cur->bf == 1){RotateL(parent);}break;}else{assert(false);}}return true;}

接下来我们来看看旋转的实现

2.2.1 旋转(重点)

左单旋

[C++] 详解AVL树左旋的实现~

 

旋转的时候需要注意的问题:

  1. 保持他是搜索树
  2. 变成平衡树且降低这个子树的高度

核心操作:

parent->right=cur->left;
cur->left=parent;

如下情况都会用到左旋:

代码:

void RotateL(Node* parent)
{// 保存父节点的右子节点Node* cur = parent->_right;// 保存右子节点的左子节点Node* curleft = cur->_left;// 利用区间性,将子左给父右parent->_right = curleft;if (curleft){// 将右子节点的左子节点作为父节点的右子节点curleft->_parent = parent;}// 将父节点作为右子节点的左子节点cur->_left = parent;// 保存父节点的父节点Node* ppnode = parent->_parent;// 将父节点的父节点指向右子节点parent->_parent = cur;// 判断原父节点是否为根节点if (parent == _root){// 更新根节点为右子节点_root = cur;// 将新根节点的父指针置为空cur->_parent = nullptr;}else{// 判断原父节点是其父节点的左子节点还是右子节点if (ppnode->_left == parent){// 更新父节点的左子节点为右子节点ppnode->_left = cur;}else{// 更新父节点的右子节点为右子节点ppnode->_right = cur;}// 更新右子节点的父指针为父节点的父节点cur->_parent = ppnode;}// 将父节点和右子节点的平衡因子都设置为0,表示树已经平衡parent->_bf = cur->_bf = 0;
}
右单旋

代码:

void RotateR(Node* parent)
{// 获取父节点的左子节点Node* cur = parent->_left;// 获取左子节点的右子节点Node* curright = cur->_right;// 将左子节点的右子节点作为父节点的左子节点parent->_left = curright;if (curright){// 更新左子节点的右子节点的父指针curright->_parent = parent;}// 引入父父节点Node* ppnode = parent->_parent;// 将父节点作为左子节点的右子节点cur->_right = parent;// 更新父节点的父指针parent->_parent = cur;// 判断原父节点是否为根节点if (ppnode == nullptr){// 更新根节点为左子节点_root = cur;// 将新根节点的父指针置为空cur->_parent = nullptr;}else{// 判断原父节点是其父节点的左子节点还是右子节点if (ppnode->_left == parent){// 更新父节点的左子节点为左子节点ppnode->_left = cur;}else{// 更新父节点的右子节点为左子节点ppnode->_right = cur;}// 更新左子节点的父指针cur->_parent = ppnode;}// 将父节点和左子节点的平衡因子都设置为0,表示树已经平衡parent->_bf = cur->_bf = 0;
}
双旋

左右旋转:插入的两种情况,看的是折线情况

直线:单旋 2 1 同号

折线:双旋 2 -1

旋转判断

根据 parent 和 cur 的平衡因子,实现对使用哪种旋转的判断

	else if (parent->_bf == 2 || parent->_bf == -2){// 子树不平衡了,需要旋转if (parent->_bf == 2 && cur->_bf == 1){RotateL(parent);}else if (parent->_bf == -2 && cur->_bf == -1){RotateR(parent);}//异号else if (parent->_bf == 2 && cur->_bf == -1){RotateRL(parent);}else if (parent->_bf == -2 && cur->_bf == 1){RotateLR(parent);}break;}else{assert(false);}}

1.

双旋的结果本质:比 60 小 ,比 30 大的小插入 到 30 下面,找到一个区间中的点

2.❗ 双旋后,对平衡因子的处理

3.h==0 60 本身就是插入的

三种情况,关心 60 的值是-1 0 1

不存在其他奇怪的情况,分别做了 60 的左右

以 RL 为例实现代码:

void RotateRL(Node* parent){Node* cur = parent->_right;Node* curleft = cur->_left;int bf = curleft->_bf;RotateR(parent->_right);RotateL(parent);
//举例思考填写if (bf == 0){cur->_bf = 0;curleft->_bf = 0;parent->_bf = 0;}else if (bf == 1){cur->_bf = 0;curleft->_bf = 0;parent->_bf = -1;}else if (bf == -1){cur->_bf = 1;curleft->_bf = 0;parent->_bf = 0;}else{assert(false);}}

LR 旋转:

平衡因子是根据 curright 初始情况,经过旋转后的图分析分类后得带的

⭕具体而言,先左单旋再右单旋的操作步骤如下:

  • 首先获取节点C的左子节点A(subL)和节点A的右子节点D(subLR);
  • 然后对节点A进行左单旋(RotateL),此时节点C的左子节点应为节点D,节点D的右子节点应为节点A;
  • 最后对节点C进行右单旋(RotateR),此时节点D成为新的子树头节点,节点C成为节点D的右子节点。

最后一部分使用了if语句判断旋转后各个节点的平衡因子,并进行相应的调整,以便使AVL树保持平衡。

  • 如果节点D的平衡因子为1,说明节点D的左子树比右子树高,需要进行右旋操作,这一次旋转中节点C和节点A都向右移动了一位,而节点D的平衡因子变为0,节点A和节点C的平衡因子都变为-1;
  • 如果节点D的平衡因子为-1,说明节点D的右子树比左子树高,需要进行左旋操作,这一次旋转中节点C和节点A都向左移动了一位,而节点D的平衡因子变为0,节点A和节点C的平衡因子都变为1;
  • 如果节点D的平衡因子为0,说明节点D的左右子树高度相等,不需要进行旋转操作,各个节点的平衡因子均设置为0;
  • 如果节点D的平衡因子不是1、-1或者0,则说明AVL树已经失去了平衡,这是一个不合法的状态,应该立即报错退出程序。
  • 经过这两次旋转后,AVL树重新保持了平衡性和有序性。
void RotateLR(Node* parent){Node* cur = parent->_left;Node* curright = cur->_right;int bf = curright->_bf;RotateL(parent->_left);RotateR(parent);
//解耦合,旋转bf 重新定义if (bf == 0){parent->_bf = 0;cur->_bf = 0;curright->_bf = 0;}else if (bf == -1){parent->_bf = 1;cur->_bf = 0;curright->_bf = 0;}else if (bf == 1){parent->_bf = 0;cur->_bf = -1;curright->_bf = 0;}}

2.3 判断测试

test 发现不是根,父亲又是空,是为什么呢?

树的结构出问题了,某次旋转出事了

发现错误就是我们的晋级关键时刻

我们可以根据AVL树的性质来测试

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  1. 它的左右子树都是AVL树
  2. 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

求高度这有个对重载函数的巧妙使用:

当传入的节点rootnullptr(空指针)时,说明到达了树的叶子节点的下一层,此时返回高度为0,因为空树的高度定义为0。

int Height(){return Height(_root);}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;}

对于平衡的测试:

IsBalance(Node* root) 是一个递归函数,其工作流程如下:

  1. 基本情况:如果 rootnullptr,意味着到达了一个空节点,那么认为该子树是平衡的,返回 true
  2. 计算子树高度:计算当前节点的左子树和右子树的高度,分别存储在 leftHightrightHight 变量中。
  3. 检查平衡因子root->_bf 表示当前节点的平衡因子,即右子树的高度减去左子树的高度。如果计算出的实际高度差与存储的平衡因子不匹配,那么输出错误信息并返回 false。这一步是为了验证树的内部数据一致性。
  4. 检查子树平衡性:检查当前节点的左右子树高度差的绝对值是否小于2(即是否平衡)。如果是,则继续递归检查左右子树是否平衡。如果所有的子树都平衡,那么整个树也是平衡的。
bool IsBalance(){return IsBalance(_root);}bool IsBalance(Node* root){if (root == nullptr)return true;int leftHight = Height(root->_left);int rightHight = Height(root->_right);if (rightHight - leftHight != root->_bf){cout << "平衡因子异常:" <<root->_kv.first<<"->"<< root->_bf << endl;return false;}return abs(rightHight - leftHight) < 2&& IsBalance(root->_left)&& IsBalance(root->_right);}private:Node* _root = nullptr;public:int _rotateCount = 0;
};

手动制作条件断点,一定要注意父亲回指的设定

  // 更新父节点的父指针parent->_parent = cur;

对于这个纰漏的处理,来检验和调试这个问题

测试:

int main()
{AVLTree<int, int> tree;// 插入一些节点tree.Insert({10, 10});tree.Insert({20, 20});tree.Insert({30, 30});tree.Insert({40, 40});tree.Insert({50, 50});cout << "树高度: " << tree.Height() << endl;cout << "树是否平衡: " << (tree.IsBalance() ? "是" : "否") << endl;// 插入更多节点来触发旋转tree.Insert({25, 25});tree.Insert({5, 5});tree.Insert({15, 15});cout << "树高度: " << tree.Height() << endl;cout << "树是否平衡: " << (tree.IsBalance() ? "是" : "否") << endl;return 0;
}

发现错误:

拼写错误修正:例如 rotateCount 应为 _rotateCount。parent 不要拼写掉 e。

目前还不知道是为什么,重写了一遍,就跑起来了

完整代码:

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;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 factorAVLTreeNode(const pair<K, V>& kv): _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0){}
};template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public: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;// Update balance factorwhile (parent){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){if (parent->_bf == 2 && cur->_bf == 1)RotateL(parent);else if (parent->_bf == -2 && cur->_bf == -1)RotateR(parent);else if (parent->_bf == 2 && cur->_bf == -1)RotateRL(parent);else if (parent->_bf == -2 && cur->_bf == 1)RotateLR(parent);break;}else{assert(false);}}return true;}void RotateL(Node* parent){++_rotateCount;Node* cur = parent->_right;Node* curleft = cur->_left;parent->_right = curleft;if (curleft){curleft->_parent = parent;}cur->_left = parent;Node* ppnode = parent->_parent;parent->_parent = cur;if (parent == _root){_root = cur;cur->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = cur;}else{ppnode->_right = cur;}cur->_parent = ppnode;}parent->_bf = cur->_bf = 0;}void RotateR(Node* parent){++_rotateCount;Node* cur = parent->_left;Node* curright = cur->_right;parent->_left = curright;if (curright)curright->_parent = parent;cur->_right = parent;Node* ppnode = parent->_parent;parent->_parent = cur;if (ppnode == nullptr){_root = cur;cur->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = cur;}else{ppnode->_right = cur;}cur->_parent = ppnode;}parent->_bf = cur->_bf = 0;}void RotateRL(Node* parent){Node* cur = parent->_right;Node* curleft = cur->_left;int bf = curleft->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 0){cur->_bf = 0;curleft->_bf = 0;parent->_bf = 0;}else if (bf == 1){cur->_bf = 0;curleft->_bf = 0;parent->_bf = -1;}else if (bf == -1){cur->_bf = 1;curleft->_bf = 0;parent->_bf = 0;}else{assert(false);}}void RotateLR(Node* parent){Node* cur = parent->_left;Node* curright = cur->_right;int bf = curright->_bf;RotateL(parent->_left);RotateR(parent);if (bf == 0){parent->_bf = 0;cur->_bf = 0;curright->_bf = 0;}else if (bf == -1){parent->_bf = 1;cur->_bf = 0;curright->_bf = 0;}else if (bf == 1){parent->_bf = 0;cur->_bf = -1;curright->_bf = 0;}else{assert(false);}}int Height(){return Height(_root);}int Height(Node* root){if (root == nullptr)return 0;int leftHeight = Height(root->_left);int rightHeight = Height(root->_right);return max(leftHeight, rightHeight) + 1;}bool IsBalance(){return IsBalance(_root);}bool IsBalance(Node* root){if (root == nullptr)return true;int leftHeight = Height(root->_left);int rightHeight = Height(root->_right);if (rightHeight - leftHeight != root->_bf){cout << "平衡因子异常:" << root->_kv.first << "->" << root->_bf << endl;return false;}return abs(rightHeight - leftHeight) < 2&& IsBalance(root->_left)&& IsBalance(root->_right);}private:Node* _root = nullptr;public:int _rotateCount = 0;
};

拓展:删除

插入到 0,不用更改

删除到 0,还要更改

删除会更加的复杂,平衡因子的更新,旋转等等,将上面的思路总结和拓展一下,大家有兴趣可以看看如下的实现代码:

bool Erase(const pair<T, V>& kv)
{if (_root == nullptr)return false;

首先,检查树是否为空。如果树为空,直接返回 false,表示删除失败。

    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{break;}}if (cur == nullptr)return false;

这部分代码用于在树中查找要删除的节点。通过比较当前节点 cur 的键值 cur->_kv.first 与要删除的键值 kv.first,决定向左子树还是右子树继续搜索。最终,cur 将指向要删除的节点,parentcur 的父节点。如果找不到该键值,返回 false

    // 处理删除节点的三种情况if (cur->_left == nullptr){if (parent == nullptr){_root = cur->_right;if (_root)_root->_parent = nullptr;}else{if (cur == parent->_left){parent->_left = cur->_right;parent->_bf++;}else{parent->_right = cur->_right;parent->_bf--;}if (cur->_right)cur->_right->_parent = parent;}}else if (cur->_right == nullptr){if (parent == nullptr){_root = cur->_left;if (_root)_root->_parent = nullptr;}else{if (cur == parent->_left){parent->_left = cur->_left;parent->_bf++;}else{parent->_right = cur->_left;parent->_bf--;}if (cur->_left)cur->_left->_parent = parent;}}else // 左右子树都不为空{Node* successorParent = cur;Node* successor = cur->_right;while (successor->_left){successorParent = successor;successor = successor->_left;}cur->_kv = successor->_kv;if (successorParent->_left == successor){successorParent->_left = successor->_right;successorParent->_bf++;}else{successorParent->_right = successor->_right;successorParent->_bf--;}if (successor->_right)successor->_right->_parent = successorParent;cur = successor;parent = successorParent;}delete cur;

这一部分处理删除节点的三种情况:

  1. 左子树为空:直接用右子树替代删除节点。如果删除节点是根节点,直接更新根节点 _root。否则,更新父节点的左或右子树指针,并调整平衡因子。
  2. 右子树为空:直接用左子树替代删除节点。如果删除节点是根节点,直接更新根节点 _root。否则,更新父节点的左或右子树指针,并调整平衡因子。
  3. 左右子树都不为空:找到右子树中的最小节点(即中序后继节点),用这个节点替代当前节点。然后删除中序后继节点,并调整其父节点的指针和平衡因子。
    // 更新平衡因子并处理旋转bool isLRUpdated = true;while (parent){if (!isLRUpdated){if (cur == parent->_left)parent->_bf++;elseparent->_bf--;}isLRUpdated = false;if (parent->_bf == 1 || parent->_bf == -1)return true;else if (parent->_bf == 0){cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){Node* higherChild;int sign;if (parent->_bf > 0){sign = 1;higherChild = parent->_right;}else{sign = -1;higherChild = parent->_left;}if (higherChild->_bf == 0){if (sign > 0){RotateL(parent);parent->_bf = 1;higherChild->_bf = -1;}else{RotateR(parent);parent->_bf = -1;higherChild->_bf = 1;}return true;}else if (higherChild->_bf == sign){if (sign == 1)RotateL(parent);elseRotateR(parent);}else{if (sign == 1)RotateRL(parent);elseRotateLR(parent);}cur = parent;parent = cur->_parent;}else{assert(false);}}return true;
}

这一部分用于在删除节点后更新平衡因子并处理旋转,以保持树的平衡:

  1. 平衡因子为 ±1:子树高度没有变化,直接返回。
  2. 平衡因子为 0:子树高度减少,继续向上更新平衡因子。
  3. 平衡因子为 ±2:子树严重不平衡,需要旋转。根据较高子树的平衡因子选择合适的旋转方式:
    • 如果较高子树的平衡因子为 0,进行单旋转。
    • 如果较高子树的平衡因子与父节点相同,进行单旋转。
    • 如果较高子树的平衡因子与父节点不同,进行双旋转。

通过这些操作,就可以确保树在删除节点后仍然保持平衡啦

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/49679.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

遇到Websocket就不会测了?别慌,学会这个Jmeter插件轻松解决....

websocket 是一种双向通信协议&#xff0c;在建立连接后&#xff0c;websocket服务端和客户端都能主动向对方发送或者接收数据&#xff0c;而在http协议中&#xff0c;一个request只能有一个response&#xff0c;而且这个response也是被动的&#xff0c;不能主动发起。 websoc…

【研路导航】保研英语面试高分攻略,助你一路过关斩将

面试攻略之 千锤百炼英语口语 写在前面 在保研面试中&#xff0c;英语口语往往是让许多同学感到头疼的一部分。如何在面试中展现出自信和流利的英语表达能力&#xff0c;是我们今天要探讨的主题。以下是一些有效的英语口语练习方法和常见题型解析&#xff0c;帮助你在保研面试…

LoRA:低秩自适应

LoRA:低秩自适应 本章节是对轻松上手微调大语言模型——QLORA篇中提到的LoRA的原理解释。 背后动机 现今模型的参数量变得越来越大&#xff0c;对预训练模型进行全微调变得越来越不可行。为了解决这个问题有了LoRA&#xff08;Low-Rank Adaption&#xff09;的诞生。将可训练…

Nginx制作下载站点

使用nginx制作一个类似nginx官网的下载站点 如何制作一个下载站点,首先需要ngx_http_autoindex_module模块 该模块处理以斜杠(“/”)结尾的请求&#xff0c;并生成目录列表。 nginx编译的时候会自动加载该模块&#xff0c;但是该模块默认是关闭的&#xff0c;需要使用下来指令…

3 FreeRTOS移植(从FREERTOS官网移植进自己的工程)

3 FreeRTOS移植 1 获取FreeRTOS源码&#xff08;熟悉&#xff09;1.1 介绍源码内容1.2 FreeRTOS内核1.2.1 Demo文件夹1.2.2 Source文件夹1.2.2.1 portable文件夹 2 FreeRTOS手把手移植&#xff08;掌握&#xff09;&#xff08;重要&#xff09;2.1 移植步骤 3 系统配置文件说明…

GraphHopper-map-navi_路径规划、导航(web前端页面版)

文章目录 一、项目地址二、踩坑环境三、问题记录3.1、graphhopper中地图问题3.1.1. getOpacity不存在的问题3.1.2. dispatchEvent不存在的问题3.1.3. vectorLayer.set(background-maplibre-layer, true)不存在set方法3.1.4. maplibre-gl.js.map不存在的问题3.1.5. Uncaught Ref…

学习记录:ESP32控制舵机 FREERTOS BLE

控制舵机 PWM信号 PWM信号是一种周期性变化的方波信号&#xff0c;它有两个关键参数&#xff1a; 周期&#xff08;Period&#xff09;&#xff1a;一个完整的PWM信号的时间长度&#xff0c;通常用秒&#xff08;s&#xff09;或毫秒&#xff08;ms&#xff09;表示。占空比…

FFmpeg解复用器如何从封装格式中解析出不同的音视频数据

目录 1、ffmpeg介绍 2、FFMPEG的目录结构 3、FFmpeg的格式封装与分离 3.1、数据结构 3.2、封装和分离(muxer和demuxer) 3.2.1、Demuxer流程 3.2.2、Muxer流程 4、总结 4.1、播放器 4.2、转码器 C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续…

微服务上(黑马)

文章目录 微服务011 认识微服务1.1 单体架构1.2 微服务1.3 SpringCloud 2 微服务拆分2.1 熟悉黑马商城2.2 服务拆分原则2.2.1.什么时候拆2.2.2.怎么拆 2.3 拆分服务2.3.1 拆分商品管理功能模块2.3.2 拆分购物车功能模块 2.4 远程调用2.4.1 RestTemplate2.4.2.远程调用 2.5 总结…

顺序表算法题

在学习了顺序表专题后&#xff0c;了解的顺序表的结构以及相关概念后就可以来试着完成一些顺序表的算法题了&#xff0c;在本篇中将对三道顺序表相关的算法题进行讲解&#xff0c;希望能对你有所帮助&#xff0c;一起加油吧&#xff01;&#xff01;&#xff01; 1.移除元素 2…

nginx转发netty长链接(nginx负载tcp长链接配置)

首先要清楚一点&#xff0c;netty是长链接是tcp连接不同于http中负载在http中配置server监听。长连接需要开启nginx的stream模块(和http是并列关系) 安装nginx时注意开启stream&#xff0c;编译时加上参数 --with-stream &#xff08;其他参数根据自己所需来加&#xff09; …

脊髓损伤的小伙伴锻炼贴士

Hey小伙伴们~&#x1f44b; 今天要跟大家聊一个超燃又超温馨的话题&#xff01;&#x1f31f; 对于我们脊髓损伤的小伙伴们来说&#xff0c;保持身体活力&#xff0c;不仅是健康的小秘诀&#xff0c;更是拥抱美好生活的超能量哦&#xff01;&#x1f4aa; #脊髓损伤# 首先&…

Cache 替换策略--PLRU算法详解

一、引言 LRU&#xff08;Least Recently Used&#xff09;是 cache 的经典替换策略之一&#xff0c;但当 Cache 的路数比较大时&#xff08;多路组相连结构&#xff09;&#xff0c;实现 LRU 的硬件开销就会变得很大。现代处理器一般会考虑使用 PLRU&#xff08;pseudo-LRU&a…

一文带你搞懂C++运算符重载

7. C运算符重载 C运算符重载 什么是运算符重载 运算符重载赋予运算能够操作自定义类型。 运算符重载前提条件&#xff1a; 必定存在一个自定义类型 运算符重载实质: 就是函数调用 友元重载 类重载 在同一自定义类型中&#xff0c;一个运算符只能被重载一次 C重载只能重载…

vue element-ui日期控件传参

前端&#xff1a;Vue element-ui <el-form-item label"过期时间" :rules"[ { required: true, message: 请选择过期时间, trigger: blur }]"><el-date-picker v-model"form.expireTime" type"date" format"yyyy-MM-dd&…

【C++】透析类和对象(下)

有不懂的可以翻阅我之前文章&#xff01; 个人主页&#xff1a;CSDN_小八哥向前冲 所属专栏&#xff1a;CSDN_C入门 目录 拷贝构造函数 运算符重载 赋值运算符重载 取地址运算符重载 const成员函数 取地址重载 再探构造函数 初始化列表 类型转换 static成员 友元 内…

MySQL查询执行(二):order by工作原理

假设你要查询城市是“杭州”的所有人名字&#xff0c; 并且按照姓名排序返回前1000个人的姓名、 年龄。 假设这个表的部分定义是这样的&#xff1a; -- 创建表t CREATE TABLE t (id int(11) NOT NULL,city varchar(16) NOT NULL,name varchar(16) NOT NULL,age int(11) NOT N…

Docker 搭建Elasticsearch详细步骤

本章教程使用Docker搭建Elasticsearch环境。 一、拉取镜像 docker pull docker.elastic.co/elasticsearch/elasticsearch:8.8.2二、运行容器 docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-n

maven clean报错:Failed to delete xxxxx\target\xxxx.jar

问题描述 今天使用maven的clean命令时候出错如下&#xff1a; 分析问题 target文件可能时编译的文件被其他程序占用&#xff0c;导致资源无法回收 解决问题 1 打开任务管理器 右键任务栏。进入任务管理器 点击详细信息 2 进入 点击性能&#xff0c;点击打开资源监视器 …