【C++进阶】AVL树详解

文章目录

  • 1. AVL树的概念
  • 2. AVL树结点的定义
  • 3. AVL 树的插入
    • 3.1 关于平衡因子
    • 3.2 插入代码
  • 4. AVL 树的旋转逻辑
    • 4.1 不需要旋转
    • 4.2 左旋
    • 4.3 右旋
    • 4.4 双旋
      • 4.4.1 先右后左单旋(RL 旋转)
      • 4.4.2 先左后右单旋(LR 旋转)
    • 4.5 完整插入代码(插入+旋转)
  • 5. AVL 树的验证
    • 5.1 中序遍历打印和计算树的高度
    • 5.2 验证
    • 5.3 数据测试

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

1. AVL树的概念

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

  • 它的左右子树都是 AVL 树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过 1(1/0/-1)
    结点是一个个插入的,有些情况无法做到高度差等于 0(如:2 个节点等偶数个结点)
    平衡因子 = 右子树高度 − 左子树高度 平衡因子=右子树高度-左子树高度 平衡因子=右子树高度左子树高度(我们这里是如此实现)
    平衡因子并不是必须的,它只是一种控制方式,帮助我们更便捷地控制树
    如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度为 O ( l o g 2 n ) O(log_2 n) O(log2n)

2. AVL树结点的定义

template<class K, class V>
struct AVLTreeNode
{AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;int _bf;//balance factor 平衡因子pair <K, V> _kv;AVLTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _bf(0), _kv(kv){}
};

3. AVL 树的插入

ppParentcpCur

  1. =】哦怕【、按二叉搜索树规则插入(复用代)
  2. 更新平衡因子
    1. 插入结点会影响哪些结点的平衡因子呢?——新增结点的部分祖先
  3. 更新原则:
    1. cp 的左边,p->bf--
    2. cp 的右边,p->bf++
      是否继续更新取决于 p 的高度是否变化,是否会影响爷爷结点

3.1 关于平衡因子

pCur 插入后,pParent 的平衡因子一定需要调整,在插入之前,pParent 的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:

  1. 如果pCur插入到pParent的左侧,只需给pParent的平衡因子-1即可
  2. 如果pCur插入到pParent的右侧,只需给pParent的平衡因子+1即可

检查平衡因子

  1. 更新后,p->bf ==0,p 所在的子树高度不变,不会影响爷爷。说明更新前,p 的 bf 是 1 或者-1,p 的矮的那边插入了节点,左右均衡了,p 的高度不变,不会影响爷爷——更新结束
    image.png

  2. 更新后,p->bf==1/-1,p 所在的子树的高度变了,会影响爷爷说明更新前,p 的 bf 是 0,p 的有一边插入,p 变得不均衡,但是不违反规则, p 的高度变了,会影响爷爷,需要往上检查一下——继续往上更新(往上结点走更新规则)
    image.png

  3. 更新后,p->bf==2/-2, 说明 p 所在的子树违反了平衡规则——处理 ->旋转
    image.png

结束条件

  1. c 更新到 root 位置
  2. 更新后,p->bf ==0 结束
  3. 旋转后结束
    旋转让 p 所在的子树高度回到插入之前的状态,不会对上层的 bf 有影响

3.2 插入代码

bool Insert(const pair<K, V>& kv)
{//和二叉搜索树的插入一样的逻辑,复用if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;while (cur)//更新parent和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;}}//更新结点后,开辟出新结点,并使parent结点指向cur//因为在指向cur前的parent是树末端结点,指向nullptrcur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;//容易忽略//更新cur里的parent指向——可以理解为二叉搜索树是树状的双向链表//更新双亲的平衡因子while (parent){//继续向上更新(循环)//直到根节点或遇到一个平衡因子为0的节点为止if (cur == parent->_left)//新结点在当前的parent(那一树)左边插入,父节点的平衡因子--{parent->_bf--;}else//新结点在当前的parent(那一树)右边插入,父节点的平衡因子++{parent->_bf++;}if (parent->_bf == 0)//插入之前parent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功{break;}else if (parent->_bf == 1 || parent->_bf == -1){//如果parent的平衡因子为正负1,// 说明插入前parent的平衡因子一定为0,插入后被更新成正负1,// 此时以parent为根的树的高度增加,需要继续向上更新cur = cur->_parent;parent = parent->_parent;}//检查是否需要旋转else if (parent->_bf == 2 || parent->_bf == -2){//如果parent的平衡因子为正负2,// 则parent的平衡因子违反平衡树的性质,需要对其进行旋转处理//旋转处理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){RotateLR(parent);}else{RotateRL(parent);}break;}else//插入之前AVL树就有问题{assert(false);}}//不需要旋转,或者已经通过旋转恢复树的平衡,那么插入就完成了return true;
}

4. AVL 树的旋转逻辑

下面动画演示了不断将节点插入 AVL 树时的情况,并且演示了左旋(Left Rotation)、右旋(Right Rotation)、右左旋转(Right-Left Rotation)、左右旋转(Left-Right Rotation)以及带子树的右旋(Right Rotation with children)
4aa211fa83339206c904e08ab2576bca.gif|585
当插入一个新结点后当前 parent 平衡因子为 2/-2,这时候需要旋转调整,将以 parent 为根的这棵树调整为 AVL 树。
这时候还要分四种情况:

  • 新结点插入到左子树的左侧。这时候我们需要右单旋。
  • 新结点插入到右子树的右侧。这时候我们需要左单旋。
  • 新结点插入到右子树的左侧。这时候我们需要先右单旋再左单旋。
  • 新结点插入到左子树的右侧。这时候我们需要先左单旋再右单旋。

旋转目的

  1. 保持搜索规则
  2. 当前树从不平衡旋转为平衡
  3. 降低树的高度

旋转有 2 个作用

  1. 让左右子树均衡
  2. 同时使高度下降(或者保持旋转前高度)

4.1 不需要旋转

每个结点的平衡因子都在允许的范围内(1/0/-1)
image.png

4.2 左旋

新结点插入到右子树的右侧。这时候我们需要左单旋
image.png

  1. 不仅要动两个结点,还要考虑 parent
  2. subRL 可能为空——60节点的左孩子可能存在,也可能不存在
  3. 如果 parent结点 在一棵子树则要跟父亲结点进行链接,如果不是子树,subR 可能要跟根进行链接
    30可能是根节点,也可能是子树
    • 如果是根节点,旋转完成后,要更新根节点
    • 如果是子树,可能是parent的父节点的左子树,也可能是右子树
  4. 平衡因子的更新——旋转结束后,平衡因子需要调整的就两个结点,一个是起始parent结点,一个是起始parent结点的右结点(subR),其他结点的左右高度差没有变化。这两个结点调整后平衡因子都变为0
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;//开始重新指向parent->_right = subRL;if (subRL)//subRL有可能是空树//如果该树(结点)存在,则重新指向其父节点subRL->_parent = parent;subR->_left = parent;//还有subR的parent还未处理Node* ppnode = parent->_parent;parent->_parent = subR;if (parent == _root){_root = subR;subR->_parent = nullptr;}else//parent结点是在子树里,所以才需要ppnode,也就是爷爷结点{//subR链接上之前的爷爷结点if (ppnode->_left == parent){ppnode->_left = subR;}else{ppnode->_right = subR;}subR->_parent = ppnode;}parent->_bf = 0;subR->_bf = 0;
}

4.3 右旋

新结点插入到左子树的左侧。这时候我们需要右单旋。
image.png

  1. 不仅要动两个结点,还要考虑 parent
  2. subRL 可能为空—— 60节点 的左孩子可能存在,也可能不存在
  3. 如果 parent结点 在一棵子树则要跟父亲结点进行链接,如果不是子树,subR 可能要跟根进行链接
    30 可能是根节点,也可能是子树
    • 如果是根节点,旋转完成后,要更新根节点
    • 如果是子树,可能是 parent 的父节点的左子树,也可能是右子树
  4. 平衡因子的更新——旋转结束后,平衡因子需要调整的就两个结点,一个是起始 parent 结点,一个是起始 parent 结点的左结点(subL),其他结点的左右高度差没有变化。这两个结点调整后平衡因子都变为 0

可以和 RotateL 进行比对,代码整体相似,细节除外

void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR){subLR->_parent = parent;}subL->_right = parent;Node* ppnode = parent->_parent;parent->_parent = subL;if (parent == _root){_root = subL;subL->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = subL;}else{ppnode->_right = subL;}subL->_parent = ppnode;}subL->_bf = 0;parent->_bf = 0;
}

4.4 双旋

4.4.1 先右后左单旋(RL 旋转)

新结点插入到右子树的左侧。这时候我们需要先右单旋再左单旋
image.png
注:它 subRL 是分开来(便于细分情况),原本就是一个整体
image.png|375
总的来看,中间值(如:60) 最终会作为(子树的)根,同时 平衡因子 为 0

这里我们看到,parent 的右子树的左侧插入新结点后导致 parent 不平衡,我们的策略是:先对 90 进行左单旋,再对 30 进行右单旋。

这里我们需要注意的是:我们可以调用上面已经写好的左单旋和右单旋的函数,但是,左单旋和右单旋的函数只对它们的 parent 结点及其左结点或者右结点的平衡因子作出了调整,并且都调整为了 0。我们观察上述抽象图,我们调整后,会有三个结点的平衡因子需要作出调整(结点 30、60、90)!

考虑以下三种情况:

  • 新结点插入的是 60 的左子树:插入后三个结点的平衡因子为 30 (bf==-2),90 (bf==-1),60 (bf==-1),调整后的三个平衡因子为 30 (bf==0),90 (bf==1),60 (bf==0)。
    image.png

  • 新结点插入的是 60 的右子树:插入后三个结点的平衡因子为 30 (bf==-2),90 (bf==-1),60 (bf==1),调整后的三个平衡因子为 30 (bf==-1),90 (bf==0),60 (bf==0)。
    image.png

  • 新结点插入的是一棵原本仅有 2 个结点的 AVL 树:插入后的三个结点的平衡因子为 30 (bf==2),90 (bf==-1),60 (bf==0),调整后的三个平衡因子为 30 (bf==0),90 (bf==0),60 (bf==0)。
    image.png

  • 根据旋转前 subRL->_bf 来判断新的结点在哪里插入

  • 并以此来确定如何更新 parentsubR平衡因子

void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;//记录旋转之前该结点的bfRotateR(subR);RotateL(parent);subRL->_bf = 0;if (bf == 1)//在c插入{parent->_bf = -1;subR->_bf = 0;}else if (bf == -1)//在b插入{parent->_bf = 0;subR->_bf = -1;}else if (bf == 0)//60作为被插入的结点{parent->_bf = 0;subR->_bf = 0;}else{assert(false);}
}

4.4.2 先左后右单旋(LR 旋转)

新结点插入到左子树的右侧。这时候我们需要先单左旋再右单旋
image.png
parent 的左子树的右侧插入新结点后导致 parent 不平衡,我们的策略是:先对 30 进行左单旋,再对 90 进行右单旋。

这里我们需要注意的是:我们可以调用上面已经写好的左单旋和右单旋的函数,但是,左单旋和右单旋的函数只对它们的 parent 结点及其左结点或者右结点的平衡因子作出了调整,并且都调整为了 0。我们观察上述抽象图,我们调整后,会有三个结点的平衡因子需要作出调整(结点 30、60、90)!

考虑以下三种情况:

  • 新结点插入的是 60 的左子树:插入后三个结点的平衡因子为 90 (bf==-2),30 (bf==1),60 (bf==-1),调整后的三个平衡因子为 90 (bf==1),30 (bf==0),60 (bf==0)。
    image.png

  • 新结点插入的是 60 的右子树):插入后三个结点的平衡因子为 90 (bf==-2),30 (bf==1),60 (bf==1),调整后的三个平衡因子为 90 (bf==0),30 (bf==-1),60 (bf==0)。
    image.png

  • 新结点插入的是一棵原本仅有 2 个结点的 AVL 树:插入后的三个结点的平衡因子为 90 (bf==-2),30 (bf==1),60 (bf==0),调整后的三个平衡因子为 90 (bf==0),30 (bf==0),60 (bf==0)。
    image.png

void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);//旋转结束,重置bfif (bf == -1){subLR->_bf = 0;subL->_bf = 0;parent->_bf = 1;}else if (bf == 1){subLR->_bf = 0;subL->_bf = -1;parent->_bf = 0;}else if (bf == 0){subLR->_bf = 0;subL->_bf = 0;parent->_bf = 0;}else{assert(false);}
}

4.5 完整插入代码(插入+旋转)

bool Insert(const pair<K, V>& kv)
{//和二叉搜索树的插入一样的逻辑,复用if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;while (cur)//更新parent和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;}}//更新结点后,开辟出新结点,并使parent结点指向cur//因为在指向cur前的parent是树末端结点,指向nullptrcur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;//容易忽略//更新cur里的parent指向——可以理解为二叉搜索树是树状的双向链表//更新双亲的平衡因子while (parent){if (cur == parent->_left)//新结点在左边插入,父节点的平衡因子--{parent->_bf--;}else//新结点在右边插入,父节点的平衡因子++{parent->_bf++;}if (parent->_bf == 0)//插入之前parent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功{break;}else if (parent->_bf == 1 || parent->_bf == -1){//如果parent的平衡因子为正负1,// 说明插入前parent的平衡因子一定为0,插入后被更新成正负1,// 此时以parent为根的树的高度增加,需要继续向上更新cur = cur->_parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){//如果pParent的平衡因子为正负2,// 则pParent的平衡因子违反平衡树的性质,需要对其进行旋转处理//旋转处理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){RotateLR(parent);}else{RotateRL(parent);}break;}else//插入之前AVL树就有问题{assert(false);}}return true;
}void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)//subRL有可能是空树subRL->_parent = parent;subR->_left = parent;//还有subR的parent还未处理Node* ppnode = parent->_parent;parent->_parent = subR;if (parent == _root){_root = subR;subR->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = subR;}else{ppnode->_right = subR;}subR->_parent = ppnode;}parent->_bf = 0;subR->_bf = 0;
}void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR){subLR->_parent = parent;}subL->_right = parent;Node* ppnode = parent->_parent;parent->_parent = subL;if (parent == _root){_root = subL;subL->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = subL;}else{ppnode->_right = subL;}subL->_parent = ppnode;}subL->_bf = 0;parent->_bf = 0;
}void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);//旋转结束,重置bfif (bf == -1){subLR->_bf = 0;subL->_bf = 0;parent->_bf = 1;}else if (bf == 1){subLR->_bf = 0;subL->_bf = -1;parent->_bf = 0;}else if (bf == 0){subLR->_bf = 0;subL->_bf = 0;parent->_bf = 0;}else{assert (false);}
}void RotateRL (Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR (subR);RotateL (parent);subRL->_bf = 0;if (bf == 1)//在 c 插入{parent->_bf = -1;subR->_bf = 0;}else if (bf == -1)//在 b 插入{parent->_bf = 0;subR->_bf = -1;}else if (bf == 0)//60 作为被插入的结点{parent->_bf = 0;subR->_bf = 0;}else{assert (false);}
}

5. AVL 树的验证

5.1 中序遍历打印和计算树的高度

void _InOrder(Node* root)
{if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << "[" << root->_bf << "]" << endl;_InOrder(root->_right);
}
void InOrder()
{_InOrder(_root);
}
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;
}

5.2 验证

  1. 验证其为二叉搜索树
    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    • 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
    • 节点的平衡因子是否计算正确

在这里有两种方式:前序和(后序+引用参数),前者易于理解,后者高效且值得细品

//前序
bool _IsBalance(Node* root)
{if (root == nullptr){return true;}int leftHeight = Height(root->_left);int	rightHeight = Height(root->_right);if (abs(rightHeight - leftHeight) >= 2){cout << root->_kv.first << "不平衡" << endl;return false;}if (rightHeight - leftHeight != root->_bf){cout << root->_kv.first << "平衡因子异常" << endl;return false;}return _IsBalance(root->_left) && _IsBalance(root->_right);
}
bool IsBalance()
{return _IsBalance(_root);
}
  1. 基础情况:
    如果 root 为 nullptr(即树为空),那么它自然是平衡的,所以返回 true。
  2. 计算左右子树的高度:
    使用_Height 函数计算左子树和右子树的高度。
  3. 检查高度差:
    计算左右子树的高度差(abs (leftHeight - rightHeight))
    如果这个高度差大于或等于 2,说明树不是 AVL 树,所以打印出当前根节点的值(可能是为了调试目的),并返回 false。
  4. 检查平衡因子:
    AVL 树的每个节点通常都有一个平衡因子(BF),它是右子树高度减去左子树高度的结果。
    接下来,代码检查右子树高度减去左子树高度得到的差值是否等于当前节点的平衡因子(root->_bf)。
    如果不等,说明平衡因子不正确,可能是在之前的旋转或插入/删除操作中出现了错误,所以打印出当前根节点的值(为了调试)并返回 false。
  5. 递归检查子树:
    最后,代码递归地检查左子树和右子树是否都是 AVL 树。只有当两个子树都是 AVL 树时,当前树才是 AVL 树。

优点就是简单直观。
但是,这方法缺点很明显

  • 重复计算高度:每次检查节点的平衡状态时,都需要调用 Height 函数,导致子树的高度被多次重复计算,效率较低。
  • 效率低:由于高度的重复计算,导致时间复杂度较高,是 O ( n 2 ) O(n^2) O(n2)

以下方法没有用到上述的 Height() 函数

//后序
bool _IsBalance(Node* root, int& height)//用了后序,高度还是重复计数了,所以增加了一个引用参数
{if (root == nullptr){height = 0;return true;}int leftHeight = 0,rightHeight = 0;//递归调用 `_IsBalance` 函数,分别检查左子树和右子树的平衡性,同时计算左右子树的高度。//如果左子树或右子树不平衡,直接返回 `false`。if (!_IsBalance(root->_left,leftHeight) || !_IsBalance(root->_right, rightHeight)){return false;}//检查当前节点的平衡性://如果左右子树高度差的绝对值大于等于 2,输出当前节点的键值并返回 `false` 表示不平衡。if (abs(rightHeight - leftHeight) >= 2){cout << root->_kv.first << "不平衡" << endl;return false;}//如果左右子树高度差不等于当前节点的平衡因子 `_bf`,输出当前节点的键值并返回 `false` 表示平衡因子异常。if (rightHeight - leftHeight != root->_bf){cout << root->_kv.first << "平衡因子异常" << endl;return false;}height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;//计算当前节点的高度并通过引用参数 `height` 返回。return true;
}
bool IsBalance()
{int height = 0;return _IsBalance(_root,height);
}

优点:

  • 避免重复计算:通过引用参数 height,避免了对子树高度的重复计算。例如,在遍历左子树和右子树时,会计算子树的高度并通过引用参数传递给父节点,父节点可以直接使用这些高度值,而不需要再次计算。
  • 后序遍历:该算法采用后序遍历的方式,确保在处理当前节点之前已经处理完左右子树,从而保证在返回当前节点的高度时,左右子树的高度已经计算完成。
  • 高效性:时间复杂度是 O(n),因为每个节点只被遍历一次,每个节点的高度也只被计算一次。
  • 平衡因子检查:不仅检查高度差是否在允许范围内,还检查每个节点的平衡因子 _bf 是否正确,确保 AVL 树的完整性。

5.3 数据测试

void TestAVLTree1()
{int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };AVLTree<int, int> t;for (auto e : a){if (e == 14){int x = 0;}t.Insert(make_pair(e, e));//它会自动推导模板参数的类型}t.InOrder();cout << t.IsBalance() << endl;
}

image.png|360
返回 1 即为确实是 AVL 树

关于调试
1、先看是插入谁导致出现的问题
2、打条件断点,画出插入前的树(打印、日志)
3、单步跟踪,对比图一一分析细节原因

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

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

相关文章

C语言 | Leetcode C语言题解之第301题删除无效的括号

题目&#xff1a; 题解&#xff1a; struct Next { int l, r, cl, cr; };void vec_push(char*** v, int* n, char* s) {if (!(*n & *n1)) {*v realloc(*v, sizeof(char*) * ((*n << 1) | 1));}(*v)[(*n)] s; }void dfs(const char* s, int i, struct Next next[],…

图片格式怎么转换?这几种图片格式转换方法简单又高效

图片已成为我们日常生活与工作中不可或缺的一部分。然而&#xff0c;不同平台和应用往往对图片格式有着特定的要求&#xff0c;这就使得图片格式的转换成为了一项必备技能。下面给大家分享5种能够简单高效的转换图片格式方法&#xff0c;快来一起学习下吧。 方法一&#xff1a;…

JavaFX布局-TitledPane

JavaFX布局-TitledPane 常用属性textcontentgraphicexpandedcollapsibleanimated 实现方式Javafxml 提供了一个可折叠的标题栏和一个内容区域内容区域可以嵌套其他布局 常用属性 text 设置标题 titledPane.setText("测试标题");content 内容区域&#xff0c;可以单…

【MySQL是怎样运行的 | 第二篇】MySQL三大日志文件

文章目录 2.MySQL三大日志文件2.1日志文件列表2.1.1 redo log2.1.2 bin log2.1.3 undo log 2.2redo log日志详讲2.3 binglog和redo log有什么区别&#xff1f;2.4一条更新语句的执行过程 2.MySQL三大日志文件 2.1日志文件列表 redo log&#xff1a;重做日志&#xff0c;记录了…

JDK 8 升级 17 及 springboot 2.x 升级 3.x 指南

JDK 8 升级 17 简介 从 JDK 8 升级到 JDK 17 的过程中&#xff0c;有几个主要的变化&#xff0c;特别是 Java Platform Module System (JPMS) 的引入&#xff0c;以及一些包路径的调整。以下是与 JDK 17 相关的一些重要变化&#xff1a; Java Platform Module System (JPMS) …

面试题:MySQL 索引

1. 谈一下你对于MySQL索引的理解?(为什么MySQL要选择B+树来存储索引) MySQL的索引选择B+树作为数据结构来进行存储,使用B+树的本质原因在于可以减少IO次数,提高查询的效率,简单来说就是可以保证在树的高度不变的情况下存储更多的数据: IO效率的提高:在MySQL数据库中,…

E19.【C语言】练习:数组

有序序列合并 描述 输入两个升序排列的序列&#xff0c;将两个序列合并为一个有序序列并输出。 数据范围&#xff1a; 1≤n,m≤1000 &#xff0c; 序列中的值满足 0≤val≤30000 输入描述&#xff1a; 输入包含三行&#xff0c; 第一行包含两个正整数n, m&#xff0c;用空…

大模型的经典面试问题及答案

大语言模型&#xff08;LLM&#xff09;在人工智能中变得越来越重要&#xff0c;在各个行业都有应用。随着对大语言模型专业人才需求的增长&#xff0c;本文提供了一套全面的面试问题和答案&#xff0c;涵盖了基本概念、先进技术和实际应用。如果你正在为面试做准备&#xff0c…

ABB机器人EGM功能简单应用

EGM中使用的是Protocol Buffer2的协议&#xff0c;从Egm.proto模板文件中可以了解到&#xff0c;协议版本没有定义默认为proto2&#xff0c;目前最新版本为proto3。PC安装了RobotStudio 6.08后&#xff0c;在如下路径可以找到egm.proto模板文件。 C:\ProgramData\ABB Industri…

ISP-LSC

1. 概述 Lens shading分为两个部分&#xff0c;亮度均匀性&#xff08;luma shading correction&#xff09;&#xff0c;色彩均匀性&#xff08;colour shading correction&#xff09;。 lens 的各位置和中心的透射率不同&#xff1a;luma shading lens 对不同光谱的shift 不…

NOILinux2.0安装

NOI官方已发布NOILinux2.0&#xff0c;可是如何安装使用呢&#xff1f;我来教你。 首先下载VMWare和NOILinux2.0的ios&#xff0c;当然你用什么虚拟机软件都可以。这里我用的是VMware。 NOIlinux2.0的下载链接&#xff1a; NOI Linux 2.0发布&#xff0c;将于9月1日起正式启用…

Cesium 体积云效果

Cesium 体积云效果&#xff08;局部&#xff09; 原理&#xff1a;RayMarching光线步进噪声&#xff0c;需要修改源码让cesium支持3D纹理&#xff08;texImage3D&#xff09; 源码修改思路&#xff1a;Cesium中使用Sampler3D&#xff0c;3D纹理&#xff0c;实现体渲染 感谢思…

Debian12 安装Docker 用 Docker Compose 部署WordPress

服务器准备&#xff1a; 以root账号登录&#xff0c;如果不是root&#xff0c;后面指令需要加sudo apt update apt install apt-transport-https ca-certificates curl gnupg lsb-release添加GPG密钥&#xff0c;推荐国内源 curl -fsSL https://mirrors.aliyun.com/docker…

【Sklearn-驯化】一文搞懂很难的条件随机场系列算法:hmm、crf以及实践

【Sklearn-驯化】一文搞懂很难的条件随机场系列算法&#xff1a;hmm、crf以及实践 本次修炼方法请往下查看 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我工作、学习、实践 IT领域、真诚分享 踩坑集合&#xff0c;智慧小天地&#xff01; &#x1f387; 免费获取相…

【Python从入门到进阶】61、Pandas中DataFrame对象的操作(二)

接上篇《60、Pandas中DataFrame对象的操作&#xff08;一&#xff09;》 上一篇我们讲解了DataFrame对象的简介、基本操作及数据清洗相关的内容。本篇我们来继续讲解DataFrame对象的统计分析、可视化以及数据导出与保存相关内容。 一、DataFrame的统计分析 在数据分析和处理中…

C#与欧姆龙PLC 通信——fins udp协议

前言 欧姆龙PLC在工控领域占有很大的市场份额,在上位机编程领域,实现上位机和欧姆龙PLC的通信也是必备的技能,上位机和PLC可以通过fins udp和fins tcp协议通信,本文介绍的是fins udp协议,该协议具有传输速度快的特点,为了帮助大家学习fins udp协议,我编写了“欧姆龙Fin…

从零开始学习网络安全渗透测试之基础入门篇——(四)反弹SHELL不回显带外正反向连接防火墙出入站文件上传下载

一、反弹SHELL 反弹SHELL&#xff08;Reverse Shell&#xff09;是一种网络攻击技术&#xff0c;它允许攻击者在一个被入侵的计算机上执行命令&#xff0c;即使该计算机位于防火墙或NAT&#xff08;网络地址转换&#xff09;之后。通常&#xff0c;当攻击者无法直接连接到目标…

基于OPENMV实现一个单目测速测试

1、测试平台&#xff1a;OpenMV4H7Plus开发板​ 使用的相机是 MT9V034 相机对应为752x480黑白图像&#xff0c;为全局快门 2、测速的时候&#xff0c;仅选取最大物体的抓取&#xff0c;这里采用了几个步骤&#xff1a; ①、直接计算灰度的帧差 ②、对帧差进行阈值处理以获取mot…

【代码随想录训练营第42期 Day7打卡 LeetCode 454.四数相加II 383. 赎金信 15. 三数之和 18. 四数之和

目录 一、做题心得 二、题目及题解 454.四数相加II 题目链接 题解 383. 赎金信 题目链接 题解 15. 三数之和 题目链接 题解 18. 四数之和 题目链接 题解 三、小结 一、做题心得 今天是代码随想录训练营打卡的第七天&#xff0c;做的也是同昨天一样的哈希表部分…

CUDA编程之grid和block详解

CUDA 文章目录 CUDAgrid和block基本的理解1维 遍历2维 遍历3维 遍历3维 打印对应的thread grid和block基本的理解 Kernel&#xff1a;Kernel不是CPU&#xff0c;而是在GPU上运行的特殊函数。你可以把Kernel想象成GPU上并行执行的任务。当你从主机&#xff08;CPU&#xff09;调…