【C++高阶】精通AVL树:全面剖析与深度学习

目录

  • 🚀 前言
  • 一: 🔥 AVL树的性质
  • 二: 🔥 AVL树节点的定义
  • 三: 🔥 AVL树的插入
  • 四: 🔥 AVL树的平衡调整(附动图)
  • 五:🔥 AVL树的验证
    • 5.1 验证有序
    • 5.2 验证平衡
  • 六: 🔥 AVL树的删除
  • 七: 🔥AVL树的性能和完整代码

🚀 前言

AVL树,又称为平衡二叉树,它基于二叉搜索树并通过平衡而得到。

在前面的学习中我们提到,二叉搜索树可以提高搜索数据的效率,但在数据有序的情况下会退化为单支树,此时在树中查找元素就得遍历一整个分支,时间复杂度也会退化至O(N)。

如果有一种算法,可以使二叉搜索树时刻保持左右子树的平衡,就可以避免这种最坏情况。

因此,两位俄罗斯的数学家G.M.Adelson-VelskiiE.M.Landis在1962年发明了AVL树,它不仅解决了二叉搜索树在数据插入和删除时可能产生的失衡问题,更通过旋转操作,使得树的高度始终保持在一个相对较低的水平,从而保证了搜索的高效性。

一: 🔥 AVL树的性质

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

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

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度 O( l o g 2 n log_2 n log2n)

二: 🔥 AVL树节点的定义

AVL树节点的定义通常包含以下几个关键部分:

1.基本元素
在调整失衡的AVL树时,我们需要频繁的访问父节点,所以在AVL树中我们需要使用三叉链,因此AVL树的节点除了包含左右子节点的指针,还需要一个指向父节点的指针。

  • _left:指向节点的左子节点的指针。
  • _right:指向节点的右子节点的指针。
  • _parent:指向节点的父节点的指针。
  • _kv:一个结构体或配对(pair),包含节点的键值(key)和值(value)。这取决于AVL树的具体用途,可能只包含键或包含键值对。

2. 平衡因子(_bf)

  • 一个整数,表示节点左子树和右子树的高度差。AVL树的性质要求任何节点的平衡因子的绝对值不超过1(-1, 0, 1)。
  • 如果左子树比右子树高一层,那么平衡因子就为-1;如果左右子树一样高,平衡因子就为0;如果右子树比左子树高一层,那么平衡因子就为1,这三种情况下AVL树的性质都没有被打破。
  • 按照这个规则,如果平衡因子为-2、2或其他值,则说明左右子树已经失衡,性质被打破。

另外需要说明一下,本文中,我们使用key / value模型的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; 	//第一个数据存储key,第二个数据存储valueint _bf; 			//平衡因子(balance factor)AVLTreeNode(const pair<const K, V>& kv):_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv),_bf(0) 		//新节点左右都为空,平衡因子为0{}
};

三: 🔥 AVL树的插入

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

  • 按照二叉搜索树的方式插入新节点
  • 树的平衡调整以及调整节点的平衡因子

新节点插入后,parent的平衡因子一定需要调整
我们先按照二叉搜索树的规则将节点插入到AVL树中,并判断插入的节点在父节点的左边还是右边

按照平衡因子的规则,如果新节点插入到了父节点的左侧,那么父节点的平衡因子-1
在这里插入图片描述
如果新节点插入到了父节点的右侧,那么父节点的平衡因子+1
在这里插入图片描述
以上,便是新增节点的父节点平衡因子可能的变化情况。

但是! 插入一个节点不但会影响父节点,还可能会影响到祖先节点。

  • 我们观察上面的四种可能,其中左边的两种情况下,插入节点后以父节点为根的子树高度发生了变化;在右边的两种情况下,插入节点后以父节点为根的子树高度没有发生变化。

  • 观察过后可以发现,当父节点的平衡因子从0变为1/-1后,子树高度发生变化;当父节点的平衡因子从1/-1变为0后,子树高度不发生变化

  • 如果以父节点为根的子树高度没有发生变化,那么就不会影响到祖先节点的平衡因子;如果高度变了就会继续向上影响到祖先节点的平衡因子

因此,我们可以通过判断节点的插入位置来计算父节点的平衡因子,进而判断子树高度是否发生变化,再进一步计算对祖先节点平衡因子的影响,来判断AVL树是否失衡。

至此,我们已经可以开始写插入新节点和更新平衡因子的代码了:

template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:bool insert(const pair<const K, V>& kv){if (_root == nullptr) 		//检测为空树的情况{_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;while (cur) 				//搜索新节点的插入位置{parent = cur;if (kv.first > cur->_kv.first)cur = cur->_right;else if (kv.first < cur->_kv.first)cur = cur->_left;elsereturn false;}cur = new Node(kv);//将父节点与新节点链接//比较新节点和父节点的key判断插入到左边还是右边if (kv.first > parent->_kv.first)   {parent->_right = cur;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}while (cur != _root){//插入节点后除了对父节点造成影响还可能对祖宗节点造成影响//因此随着循环进行,这里的cur不一定为新节点,可以理解为高度发生变化的子树的根节点//更新父节点的平衡因子if (cur == parent->_left)parent->_bf--;elseparent->_bf++;//更新后检测父节点的平衡因子if (parent->_bf == 0) //平衡因子为0说明没有打破性质,跳出循环break;else if (parent->_bf == 1 || parent->_bf == -1) //更新后平衡因子为1或-1说明高度发生变化,改变cur和parent的指向后继续向上更新{cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2) //更新后平衡因子为2或-2.说明已经失衡,需要调整{//不同情况的调整方法...if (parent->_bf == 2){if (cur->_bf == 1){//...}else if (cur->_bf == -1){//...}}else{if (cur->_bf == 1){//...}else if (cur->_bf == -1){//...}}break;}else //平衡因子出现意外情况,报错{assert(false);}}return true;}private:Node* _root = nullptr;
};

通过上述分析我们可以知道:插入后,parent的平衡因子可能有三种情况:0,正负1, 正负2

  • 如果parent的平衡因子为0,说明插入之前parent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功
  • 如果parent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更新成正负1,此时以parent为根的树的高度增加,需要继续向上更新
  • 如果parent的平衡因子为正负2,则parent的平衡因子违反平衡树的性质,需要对其进行旋转处理
  • 接下来我们就重点讲解AVL树的旋转操作

四: 🔥 AVL树的平衡调整(附动图)

如果在一颗原本平衡的AVL树中插入一个新节点,可能会造成失衡,此时需要调整树的结构使之重新平衡,这种调整方法称为旋转。

根据树的原本结构和节点插入位置的不同分为四种情况和四种旋转方式:

(1)新节点插入较高左子树的左侧:右单旋

在这里插入图片描述
问题来了:如何判断插入的新节点的方位呢?

很简单,以上面的情况为例,插入新节点后60的平衡因子变成-2,说明左子树更高,而30的平衡因子变成-1,说明新节点插入到了30的左子树。后面左单旋以及双旋中都同理,我们使用平衡因子就可以判断新节点插入的位置

右单旋代码如下:

void RotateRight(Node *parent) 		//parent为平衡因子发生失衡的节点
{Node *subL = parent->_left; 	//subL为parent的左子节点Node *subLR = subL->_right; 	//subLR为subL的右子节点//parent,subL和subLR三个节点是旋转中唯三需要进行操作的三个节点// 将parent与subLR节点进行链接parent->_left = subLR;if (subLR) //subLR可能为空subLR->_parent = parent;Node *parentParent = parent->_parent; 	//记录parent的父节点if (parent != _root){subL->_parent = parentParent; 	//将subL与parent的父节点链接if (parent == parentParent->_left)parentParent->_left = subL;elseparentParent->_right = subL;}else 		//如果parent为根,旋转后subL成为新的根{_root = subL;subL->_parent = nullptr;}//将subL与parent链接subL->_right = parent;parent->_parent = subL;parent->_bf = subL->_bf = 0; 		//更新平衡因子
}

通过右单旋我们可以看到,原本的_parent、_left以及_right的平衡因子都变成了0

(2)新节点插入较高右子树的右侧:左单旋

在这里插入图片描述
因为左单旋的原理和右单旋是类似的,只要理解了右单旋,加上动图的配合,左单旋和后面的双旋都是很好理解的

左单旋代码如下:

void RotateLeft(Node *parent)
{Node *subR = parent->_right;Node *subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;Node *parentParent = parent->_parent;if (parent != _root){subR->_parent = parentParent;if (parent == parentParent->_left)parentParent->_left = subR;elseparentParent->_right = subR;}else{_root = subR;subR->_parent = nullptr;}subR->_left = parent;parent->_parent = subR;parent->_bf = subR->_bf = 0;
}

同样:原本的_parent、_left以及_right的平衡因子都变成了0

(3)新节点插入较高左子树的右侧:先左单旋再右单旋(左右双旋)

这种情况又可以分为两种情况:

在这里插入图片描述

不过这两种情况都属于在较高左子树的右侧插入,处理方式都是相同的,唯一的区别在于最后旋转完成后,更新平衡因子时的值不同。

  • 还存在一种情况就是30本身就是新插入的节点没有bc子树,这点会在代码里体现比较容易理解这里就不赘述了

接下来我们以上面的那个情况为例展示左右双旋的过程:
在这里插入图片描述
而下面的情况和上面的情况唯一的区别在于,最后更新的平衡因子不同
在这里插入图片描述
如何去决定每个节点更新后的平衡因子呢?可以看到这两种情况中,如果在b下面插入新节点,那么旋转过后30和60的平衡因子更新成0,90的平衡因子更新成1;如果在c下面插入新节点,则是60和90的平衡因子更新成0,30的平衡因子更新成-1

关键一点:而新节点究竟插入到了b下面还是在c下面,我们可以通过插入节点后60的平衡因子来判断
在这里插入图片描述
左右双旋代码如下:

void RotateLR(Node *parent)
{Node *subL = parent->_left;Node *subLR = subL->_right;int bf = subLR->_bf; //记录插入节点后subLR的平衡因子RotateLeft(subL); //先左单旋RotateRight(parent); //再右单旋//更新平衡因子//通过前面记录的平衡因子判断更新的情况if (bf == 0){parent->_bf = subL->_bf = subLR->_bf = 0;}else if (bf == 1){subL->_bf = -1;parent->_bf = subLR->_bf = 0;}else if (bf == -1){parent->_bf = 1;subL->_bf = subLR->_bf = 0;}else{assert(false);}
}

(4)新节点插入较高右子树的左侧:先右单旋再左单旋(右左双旋)

这种情况和左右双旋的情况原理一样,我们直接上动图和代码
右左双旋的代码如下:
在这里插入图片描述

void RotateRL(Node *parent)
{Node *subR = parent->_right;Node *subRL = subR->_left;int bf = subRL->_bf;RotateRight(subR);RotateLeft(parent);if (bf == 0){parent->_bf = subR->_bf = subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = subRL->_bf = 0;}else if (bf == -1){subR->_bf = 1;parent->_bf = subRL->_bf = 0;}else{assert(false);}
}

现在四个旋转的函数都实现了,完整的插入函数代码如下:

bool Insert(const pair<K, V>& kv)
{if (_root == nullptr) {_root = new Node(kv);return true;}Node* cur = _root, *parent = nullptr;while (cur){if (cur->_kv.first == kv.first) return false;else if (cur->_kv.first < kv.first) {parent = cur;cur = cur->_right;}else {parent = cur;cur = cur->_left;}}cur = new Node(kv);if (kv.first > parent->_kv.first) parent->_right = cur;else parent->_left = cur;cur->_parent = parent;										// 链接父亲//更新平衡因子while (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{RotateLR(parent);						   //  先左旋再右旋  (左边高,孩子右边高, 父子异号)}break;}else {assert(false);}}return true;
}

总结:

假如以parent为根的子树不平衡,即parent的平衡因子为2或者-2,分以下情况考虑

  1. parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR
  • 当subR的平衡因子为1时,执行左单旋
  • 当subR的平衡因子为-1时,执行右左双旋
  1. parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为subL
  • 当subL的平衡因子为-1是,执行右单旋
  • 当subL的平衡因子为1时,执行左右双旋

五:🔥 AVL树的验证

5.1 验证有序

最重要的插入节点部分完成了,不过在验证是否符合AVL树性质前,我们首先需要验证其是否是一棵二叉搜索树

在之前讲解二叉搜索树中提到过,如果中序遍历能够得到一个有序的序列,就说明是二叉搜索树

中序遍历代码如下:

void InOrder()
{_InOrder(_root);cout << endl;
}void _InOrder(Node *root)
{if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " "; // key/value模型,我们只打印key即可_InOrder(root->_right);
}

5.2 验证平衡

要验证是否符合AVL树性质,只需要检测它的所有节点的子树高度差不超过1即可

需要注意的是,这里不可以直接通过判断平衡因子的绝对值是否大于1来验证平衡,因为平衡因子是不客观的,可以被修改

因此,我们通过递归来得到每棵子树的高度并进行判断即可

bool IsBalance()
{return _IsBalance(_root);
}bool _IsBalance(Node *root)
{if (root == nullptr)return true;int leftHeigit = _Height(root->_left);int rightHeight = _Height(root->_right);if (rightHeight - leftHeigit != root->_bf){cout << root->_kv.first << "平衡因子异常" << endl;return false;}return abs(rightHeight - leftHeigit) <= 1 && _IsBalance(root->_left) && _IsBalance(root->_right);
}int Height()
{return _Height(_root);
}int _Height(Node *root)
{if (root == nullptr)return 0;int higher = max(_Height(root->_left), _Height(root->_right));return higher + 1;
}

六: 🔥 AVL树的删除

AVL树的删除并不是本文的重点,因为其原理我们在前面已经学习过了

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置

七: 🔥AVL树的性能和完整代码

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)。但是如果要对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<const K, V>& kv):_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv),_bf(0){}
};template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public:bool insert(const pair<const K, V>& kv){if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){parent = cur;if (kv.first > cur->_kv.first)cur = cur->_right;else if (kv.first < cur->_kv.first)cur = cur->_left;elsereturn false;}cur = new Node(kv);if (kv.first > parent->_kv.first){parent->_right = cur;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}while (cur != _root){if (cur == parent->_left)parent->_bf--;elseparent->_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){if (cur->_bf == 1){RotateLeft(parent);}else if (cur->_bf == -1){RotateRL(parent);}}else{if (cur->_bf == 1){RotateLR(parent);}else if (cur->_bf == -1){RotateRight(parent);}}break;}else{assert(false);}}return true;}void RotateLeft(Node* parent) //新节点插入较高右子树的右侧:左单旋{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if(subRL)subRL->_parent = parent;Node* parentParent = parent->_parent;if (parent != _root){subR->_parent = parentParent;if (parent == parentParent->_left)parentParent->_left = subR;elseparentParent->_right = subR;}else{_root = subR;subR->_parent = nullptr;}subR->_left = parent;parent->_parent = subR;parent->_bf = subR->_bf = 0;}void RotateRight(Node* parent) //新节点插入较高左子树的左侧:右单旋{Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;Node* parentParent = parent->_parent;if (parent != _root){subL->_parent = parentParent;if (parent == parentParent->_left)parentParent->_left = subL;elseparentParent->_right = subL;}else{_root = subL;subL->_parent = nullptr;}subL->_right = parent;parent->_parent = subL;parent->_bf = subL->_bf = 0;}void RotateRL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateRight(subR);RotateLeft(parent);if (bf == 0){parent->_bf = subR->_bf = subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = subRL->_bf = 0;}else if (bf == -1){subR->_bf = 1;parent->_bf = subRL->_bf = 0;}else{assert(false);}}void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateLeft(subL);RotateRight(parent);if (bf == 0){parent->_bf = subL->_bf = subLR->_bf = 0;}else if (bf == 1){subL->_bf = -1;parent->_bf = subLR->_bf = 0;}else if (bf == -1){parent->_bf = 1;subL->_bf = subLR->_bf = 0;}else{assert(false);}}void InOrder(){_InOrder(_root);cout << endl;}bool IsBalance(){return _IsBalance(_root);}int Height(){return _Height(_root);}size_t Size(){return _Size(_root);}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;elsereturn cur;}return nullptr;}
private:void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}bool _IsBalance(Node* root){if (root == nullptr)return true;int leftHeigit = _Height(root->_left);int rightHeight = _Height(root->_right);if (rightHeight - leftHeigit != root->_bf){cout << root->_kv.first << "平衡因子异常" << endl;return false;}return abs(rightHeight - leftHeigit) <= 1 && _IsBalance(root->_left)&& _IsBalance(root->_right);}int _Height(Node* root){if (root == nullptr)return 0;int higher = max(_Height(root->_left), _Height(root->_right));return higher + 1;}size_t _Size(Node* root){if (root == nullptr)return 0;return _Size(root->_left) + _Size(root->_right) + 1;}
private:Node* _root = nullptr;
};

以上就是AVL搜索树的图解与完整实现过程,欢迎在评论区留言,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉
在这里插入图片描述

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

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

相关文章

c++初阶篇(三):内联函数及auto关键字

1.内联函数 1.1 概念 以inline修饰的函数叫做内联函数&#xff0c;编译时C编译器会在调用内联函数的地方展开&#xff0c;没有函数调 用建立栈帧的开销&#xff0c;内联函数提升程序运行的效率。 如果在上述函数前增加 inline 关键字将其改成内联函数&#xff0c;在编译期间编…

RNN循环递归网络讲解与不掉包python实现

这里写目录标题 1.算法简介2. RNN算法原理2.1 RNN基本结构介绍2.2 计算流程 3.完整训练代码 1.算法简介 参考论文&#xff1a;Elman J L. Finding structure in time[J]. Cognitive science, 1990, 14(2): 179-211.&#xff0c;谷歌被引次数超16000! 说到循环递归结构就不得不…

秒杀案例-分布式锁Redisson、synchronized、RedLock

模拟秒杀 源码地址前期准备创建数据库表导入数据dependenciespomControllerTSeckillProductTseckillProductServiceTseckillProductServiceImplTseckillProductMapperTseckillProductMapper.xml使用JMeter压力测试开始测试超卖现象原因解决办法更改数据库库存500进行JMeter压力…

运维锅总详解Kubernetes之Kubelet

本文尝试从Kubelet的发展历史、实现原理、交互逻辑、伪代码实现及最佳实践5个方面对Kubelet进行详细阐述。希望对您有所帮助&#xff01; 一、kubelet发展历史 Kubelet 是 Kubernetes 中的核心组件之一&#xff0c;负责管理单个节点上的容器运行。它的发展历史和功能演进是 Kub…

【LeetCode】222. 完全二叉树的个数

什么是计算机基础&#xff1f;如果本题能够用二分二进制二叉树的方式解出本题&#xff0c;那么我可以认为你的计算机基础就很好了。很久以来&#xff0c;我一直认为自己的计算机基础好&#xff0c;但是自刷题以来&#xff0c;跟网上这么多优秀的同学相比&#xff0c;我发现我实…

五分钟学会 Docker Registry 搭建私有镜像仓库

在上一篇文章《前端不懂 Docker &#xff1f;先用它换掉常规的 Vue 项目部署方式》中&#xff0c;我们学习了如何使用 aliyun 私有镜像仓库&#xff0c;也了解到可以使用 Docker Registry 搭建私有镜像仓库。这篇文章就分享下实操过程。 registry 是官方提供的 registry 镜像&…

WEB前端09-前端服务器搭建(Node.js/nvm/npm)

前端服务器的搭建 在本文中&#xff0c;我们将介绍如何安装和配置 nvm&#xff08;Node Version Manager&#xff09;以方便切换不同版本的 Node.js&#xff0c;以及如何设置 npm&#xff08;Node Package Manager&#xff09;使用国内镜像&#xff0c;并搭建一个简单的前端服…

类和对象(三)

默认成员函数 接下来继续看剩下的两个默认成员函数。 const成员函数 将const修饰的成员函数称之为const成员函数&#xff0c;const修饰成员函数放到成员函数参数列表的后 ⾯。const实际修饰该成员函数隐含的this指针&#xff0c;表明在该成员函数中不能对类的任何成员进⾏修…

秋招突击——7/17——复习{二分查找——搜索插入位置、搜索二维矩阵,}——新作{链表——反转链表和回文链表,子串——和为K的子数组}

文章目录 引言新作二分模板二分查找——搜索插入位置复习实现 搜索二维矩阵复习实现 新作反转链表个人实现参考实现 回文链表个人实现参考实现 和为K的子数组个人实现参考实现 总结 引言 今天算法得是速通的&#xff0c;严格把控好时间&#xff0c;后面要准备去面试提前批了&a…

C语言实例-约瑟夫生者死者小游戏

问题&#xff1a; 30个人在一条船上&#xff0c;超载&#xff0c;需要15人下船。于是人们排成一队&#xff0c;排队的位置即为他们的编号。报数&#xff0c;从1开始&#xff0c;数到9的人下船&#xff0c;如此循环&#xff0c;直到船上仅剩15人为止&#xff0c;问都有哪些编号…

C语言 | Leetcode C语言题解之第260题只出现一次的数字III

题目&#xff1a; 题解&#xff1a; int* singleNumber(int* nums, int numsSize, int* returnSize) {int xorsum 0;for (int i 0; i < numsSize; i) {xorsum ^ nums[i];}// 防止溢出int lsb (xorsum INT_MIN ? xorsum : xorsum & (-xorsum));int type1 0, type2…

【Mysql】Docker下Mysql8数据备份与恢复

[TOC] 【Mysql】Docker下Mysql8数据备份与恢复 1 创建Mysql容器 格式 docker run -d --name容器名称 -p 宿主端口号:3306 -e MYSQL_ROOT_PASSWORDmysql密码 -e MYSQL_PASSWORDmysql密码 -e TZAsia/Shanghai -v 宿主目录-数据:/var/lib/mysql -v 宿主目录-备份数据:/back…

多态性概念 OOPS

大家好&#xff01;今天&#xff0c;我们将探讨面向对象编程 (OOP) 中的一个基本概念 - 多态性。具体来说&#xff0c;我们将重点介绍其三种主要形式&#xff1a;方法重载、方法覆盖和方法隐藏。对于任何使用 OOP 语言&#xff08;例如 C#&#xff09;的程序员来说&#xff0c;…

NET 语言识别,语音控制操作、语音播报

System.Speech. 》》System.Speech.Synthesis; 语音播报 》》System.Speech.Recognition 语音识别 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Speech.Recog…

mac二进制安装operator-sdk

0. 前置条件 1. 安装go 安装步骤略。 1. 下载operator-sdk源码包 https://github.com/operator-framework/operator-sdk 1.1 选择适合当前go版本的operator版本&#xff0c;在operator-sdk/go.mod文件中可以查看Operator-sdk使用的go版本。 2. 编译 源码包下载后&#x…

C语言航空售票系统

以下是系统部分页面 以下是部分源码&#xff0c;需要源码的私信 #include<stdio.h> #include<stdlib.h> #include<string.h> #define max_user 100 typedef struct ft {char name[50];//名字char start_place[50];//出发地char end_place[50];//目的地char …

JAVA 异步编程(线程安全)二

1、线程安全 线程安全是指你的代码所在的进程中有多个线程同时运行&#xff0c;而这些线程可能会同时运行这段代码&#xff0c;如果每次运行的代码结果和单线程运行的结果是一样的&#xff0c;且其他变量的值和预期的也是一样的&#xff0c;那么就是线程安全的。 一个类或者程序…

多线程初阶(二)- 线程安全问题

目录 1.观察count 原因总结 2.解决方案-synchronized关键字 &#xff08;1&#xff09;synchronized的特性 &#xff08;2&#xff09;如何正确使用 语法格式 3.死锁 &#xff08;1&#xff09;造成死锁的情况 &#xff08;2&#xff09;死锁的四个必要条件 4.Java标准…

若依二次开发

口味改造 原&#xff1a; 改造&#xff1a; 1./** 定义口味名称和口味列表的静态数据 */ 2.改变页面样式 3.定义储存当前选中的口味列表数组&#xff0c;定义改变口味名称时更新当前的口味列表 4.改变页面样式 6.格式转换 7.定义口味列表获取焦点时更新当前选中的口味列表

【DGL系列】简单理解graph.update_all和spmm的区别

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 目录 背景介绍 源码分析 小结一下 背景介绍 我们在看GNN相关的论文时候&#xff0c;都会说到邻接矩阵与特征矩阵之间是用到了spmm&#xff0c;在很久…