[C++进阶]AVL树

前面我们说了二叉搜索树在极端条件下时间复杂度为O(n),本篇我们将介绍一种对二叉搜索树进行改进的树——AVL树

一、AVL 树的概念

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

AVL树具有以下性质:

1.AVL树的左右子树都是AVL树

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

AVL树其实就是高度平衡二叉搜索树,

注意:这里的平衡不是相等,而是高度差不超过。其实是因为无法做到高度相等,只能做到高度差不超过1。因为总有一些结点是无法做到满二叉树的

如下是一颗典型的AVL树

对于这种AVL树,它的增删查改效率都是很高的。都是logN级别的复杂度

试想一下:

如果是满二叉树,当它高度为h的时候,他的总结点个数为2^h-1==N。

如果是平衡二叉树的话,当它的高度为h的时候,它的节点个数为2^{h-X==N}。这里的X属于[1,2(h-1)-1],所以它的效率为logN

二、AVL树的实现

1. AVL树的结点定义

如下所示,我们将AVL结点的值定义为一个pair对象,目的是使用key-value模型。bf是一个平衡因子。这里我们还需要使用三叉链结构

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){}};

2. AVL树的插入部分

关于AVL树的插入,我们根据它的特性,不难得知,首先要先将一个值给插入进去,再去考虑控制平衡。

	bool Insert(const pair<K, V>& kv){Node* newnode = new Node(kv);if (_root == nullptr){_root = newnode;return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first > newnode->_kv.first){parent = cur;cur = cur->_left;}else if (cur->_kv.first < newnode->_kv.first){parent = cur;cur = cur->_right;}else{return false;}}if (parent->_kv.first > newnode->_kv.first){parent->_left = newnode;}else{parent->_right = newnode;}newnode->_parent = parent;//平衡return true;}

如上代码所示,与二叉搜索树的方法类似,我们先想办法插入一个值进去。上面我们已经成功插入了一个数据。这一点是不难的。

  1. 我们先申请一个结点
  2. 如果根节点为空,那么我们直接将这个结点交给根节点即可。
  3. 如果根节点不为空,那么由于是二叉搜索树,那么我们先设定两个结点指针,一个指向空称作parent,一个指向根节点称作cur。
  4. 然后我们让cur去与我们要插入结点进行比较,持续迭代,最终我们就可以找出cur为待插入结点的位置,parent为要插入结点的父亲
  5. 然后我们根据待插入的值与父节点进行比较,从而确定插入左子树还是右子树。这一步是非常有必要的。

     在第五步中,我们极其容易犯一个错误,就是我们想当然的认为,cur此时就是要插入的孩子结点,所以我们会写出cur = newnode这样奇葩的代码,其实是万万不可的。我们可以画个内存图了解一下

  6. 插入号了结点以后,我们就可以开始将链接关系了。

3.AVL树的平衡因子的改变

将结点插入以后,我们需要做的就是控制平衡。因为我们AVL树的最终目的还是控制平衡。

我们先来看第一种情况

我们就以这颗树作为例子

它本身已经是一颗AVL树了,我们先考虑对右子树的插入

有以下几种情况:

通过这些图我们可以总结出如何更新平衡因子:

  • 新增在左,parent的平衡因子减减
  • 新增在右,parent的平衡因子加加
  • 更新后的parent的平衡因子==0,说明parent所在的子树高度不变,不再影响祖先,不再沿着祖先的路径往上更新
  • 更新后parent平衡因子==1/-1,说明parent所在子树的高度变化,影响了祖先,需要沿着祖先的路径往上更新
  • 更新后parent的平衡因子==2/-2,说明parent所在子树出现了问题,已经失衡,需要对parent所在子树进行旋转,使其平衡
  • 更新到根节点就可以结束了

最后两种情况出现平衡因子为不为0,-1或1的情况,此时需要进行旋转

我们先实现基础的更新平衡因子的函数

		//更新平衡因子cur = newnode;while (parent){if (parent->_left == cur){parent->_bf--;}else if (parent->_right == cur){parent->_bf++;}else{assert(false);}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);}}

4. AVL树的插入之左旋

当一颗树(这棵树有可能是子树,也可能是一颗完整的树)插入一个结点以后,它会变为如下的形状(红色是插入的结点)

一开始,我们的树还是处于平衡状态的,但是当我们插入了一个结点以后,失去了平衡了,所以我们需要对其进行旋转使其进行平衡。

我们不难得知,上面两种树,都是右边的偏高,我们想,可以让cur作为这颗树的根节点,然后parent作为cur的左子树的话,那么就可以使得高度降低1次了。刚好使得我们的AVL树再次平衡了。

如下所示,当我们进行这样的旋转之后,我们会发现cur与parent的平衡因子都变为了0。

当我们插入了这个结点以后,我们就需要控制平衡了,我们控制平衡的核心操作就是

parent->_right = cur->_left;
cur->_left = parent

这两步操作执行完成之后,parent的左右子树高度一定是相同的,所以parent的平衡因子为0,而且cur的左子树的高度也变为了h+1,右子树由于插入了一个值所以也是h+1,恰好平衡因子也为0了。所以这样一来,使得树的高度整体降了一个且树平衡了。

上面的操作固然是很重要很核心的两步,不过要注意,我们的是三叉链模型,所以我们还需要改变_parent的指向,否则关系会十分混乱。而且还需要去考虑parent所代表的子树是不是一颗完整的树还是说一个子树。如果是子树那么又是左子树还是右子树呢?这些都是需要去考虑的。不过在上面的过程中,我们会发现,需要改变的结点一共就三个:parent,cur,cur->left这三个结点的指针需要进行修改。其他的结点,他们原本的位置是哪里,现在的相对位置仍然不变。

上面这种旋转方式其实就是由于右子树高了,所以需要向左旋转,即将一个结点给左边。我们将其称之为左旋这样的话,就能保证在保证它是搜索树的条件下,还能降低这个子树的高度了
 

5.AVL树的左旋图

如上是我们在左旋的情况下的抽象图,为什么要用一个抽象图呢?这是因为我们前面对于左旋的分析并不能完整的表示出左旋的全部情况。h可以是任意值

在我们插入一个新的结点之前,a/b/c都是符合AVL规则的子树

当h为0的时候。

当h为1的时候

以此类推种类会随着h的增大而变多,所以我们用那个图来表示,接下来是代码实现环节:

	void RotateL(Node* parent){Node* pparent = parent->_parent;Node* subR = parent->_right;Node* subRL = subR->_left;//修改parentparent->_right = subRL;parent->_parent = subR;//修改subRLif (subRL){subR->_parent=parent}//修改subRsubR->_left = parent;subR->_parent = pparent;//修改pparent与subRif (pparent == nullptr){_root = sub;}else {if (pparent->_left == parent){pparent->_left = cur;}else if (pparent->_right == parent){pparent->_right = subR;}}parent->_bf = sub->_bf = 0;}
};

6.AVL树的右旋图

讲完了左旋图,我们来讲讲右旋,右旋的抽象图和左旋很相似

与左旋类似,它的三颗子树高度必须满足高度一样,否则就不是AVL树了,或者说插入一个结点之后还是平衡的。

当h==0时:

当h==1时

以此类推种类会随着h的增大而变多,所以我们用那个图来表示,接下来是代码实现环节:

	void RotateR(Node* parent){Node* sub = parent->_left;Node* subR = cur->_right;Node* pparent = parent->_parent;parent->_left = curR;parent->_parent = cur;if (subR){subR->_parent = parent;}sub->_right = parent;sub->_parent = pparent;if (pparent == nullptr){_root = sub;}else{if (pparent->_left == parent){pparent->_left = sub;}else if (pparent->_right == parent){pparent->_right = sub;}}parent->_bf = sub->_bf = 0;}

7. AVL树的双旋

我们在前面的抽象图中,主要讨论的是如果是右子树高,继续往右子树的右孩子插入的话,就会诱发左旋

但是如果我们像上面这种方式去左旋,我们会旋不明白,对吧,有没有人发现无论我们怎么单旋都是不行的!这时我们需要引入一个新的旋转思路,也就是双旋。 

遇到这种情况我们不会?没事,我们可以把上面的这种图转化成我们熟悉的样子:

比如我们先对60这边进行右旋然后我们发现熟悉的左旋,于是再对b左旋

8.AVL树的右左旋

当h==0的时候,即60这个结点就是新插入的结点,他的旋转图如下

当h==1的时候,这个结点可以在60的左边或者右边插入,都是满足条件的。因为都属于右凸出折线插入。仍然是先以90为旋转点进行右旋,然后以30为旋转点进行左旋。可见树平衡

当h==2的时候, 每个子树又呈现出了不同的情况

然而a和d一定是x/y/z中的任意一种,中间这颗子树一定是z形状的。由于我们已经进行了细分,所以中间这棵树我们直接画出具象图

变化共3*3*4=36种,但是无论是如何的变化,我们都无需担心,因为只要我们先对90进行右旋,然后对30进行左旋,就一定可以使得这棵树变为平衡。

9. AVL树的右左双旋的本质

关于右左双旋,我们似乎可以直接复用前面的左旋和右旋的代码。但是这样做的话会出现问题的。因为只是左旋和右旋的话会导致平衡因子被改变为0,但是实际上,平衡因子不为0。也就是说平衡因子需要再次处理一下。

我们在分析一下右左双旋,当h==1的时候,我们当时可以插入在左边或者右边都是可以的

60的左边变成了30的右边,60的右边变成了90的左边,60变成了这棵树的根.

我们通过观察最终平衡因子的变化,似乎就区域于插入到了60的左边还是右边。即取决于插入后60的平衡因子。由于双旋的本质,所以最终插入结束以后,如果是插入到了60的左边,那么左边将被平衡为0,右边将被平衡为1。如果插入到了60的右边,左边被平衡为-1,右边被平衡为0。

如果60本身就是新插入的结点的话,那么最终三个结点都为0

如果插入到了60的左侧,那么最终的变化如下图所示

如果插入到了60的左侧,那么最终的变化如下图所示

10. AVL树的左右双旋

左右双旋与右左双旋是呈镜像关系的

void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 0){subR->_bf = 0;subRL->_bf = 0;parent->_bf = 0;}else if (bf == 1){subR->_bf = 0;subRL->_bf = 0;parent->_bf = -1;}else if (bf == -1){subR->_bf = 1;subRL->_bf = 0;parent->_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);}
}

11.AVL树的验证

1.验证有序

完成了平衡,那么我们这棵AVL树能正常使用嘛?让我们来验证一下吧!

在验证是否符合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);
}

验证:

int main()
{AVLTree<int, int> t;int a[] = { 6, 3, 7, 11, 9, 26, 18, 14, 15 };for (auto e : a){t.Insert({ e, e });}t.InOrder();return 0;
}

运行结果:

2.验证平衡

我们可以利用AVL树的规则,写出如下的代码判断一棵树是否为AVL树

	int _Height(Node* root){if (root == nullptr){return 0;}int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);return leftHeight > rightHeight ? 1 + leftHeight : 1 + rightHeight;}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 << endl;return false;}return abs(rightHeight - leftHeight) < 2 && _IsBalance(root->_left) && _IsBalance(root->_right);}bool IsBalance(){return _IsBalance(_root);}

12. AVL树的删除

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与插入不同的是,删除节点后的平衡因子更新,如果更新后平衡因子是1或者-1,那么说明这棵树是不需要进行调整,如果平衡因子为0,说明原来是1或者-1,需要进行向上调整平衡因子。最差情况下一直要调整到根节点的位置 。他的更新策略与插入是相反的

13.AVL树的性能和代码

AVL树追求的是严格平衡,因此可以保证查找时高效的时间复杂度O(logN),但是如果我们需要频繁的对其进行旋转来维护平衡,一定程度上会影响效率,尤其是删除节点时的最差情况下我们可能需要一路旋转到根的位置。

相对于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;
};

如有错误欢迎指正

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

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

相关文章

人工智能和大模型的简介

文章目录 前言一、大模型简介二、大模型主要功能1、自然语言理解和生成2、文本总结和翻译3、文本分类和信息检索4、多模态处理三、大模型的技术特性1、深度学习架构2、大规模预训练3、自适应能力前言 随着技术的进步,人工智能(Artificial Intelligence, AI)和机器学习(Mac…

【微服务】springboot 整合表达式计算引擎 Aviator 使用详解

目录 一、前言 二、表达式计算框架概述 2.1 规则引擎 2.1.1 什么是规则引擎 2.1.2 规则引擎用途 2.1.3 规则引擎使用场景 2.2 表达式计算框架 2.2.1 表达式计算框架定义 2.2.2 表达式计算框架特点 2.2.3 表达式计算框架应用场景 2.3 表达式计算框架与规则引擎异同点 …

Pytorch详解-Pytorch核心模块

Pytorch核心模块 一、Pytorch模块结构_pycache__Cincludelibautogradnnoptimutils 二、Lib\site-packages\torchvisiondatasetsmodelsopstransforms 三、核心数据结构——Tensor&#xff08;张量&#xff09;在深度学习中&#xff0c;时间序列数据为什么是三维张量&#xff1f;…

Imagen:重塑图像生成领域的革命性突破

目录 引言 一、Imagen模型的技术原理 1. 模型概述 2. 工作流程 3. 技术创新 二、Imagen模型的应用实例 1. 创意设计 2. 虚拟角色制作 3. 概念可视化 三、Imagen模型的优势与挑战 1. 优势 2. 挑战 四、Imagen模型的未来发展方向 1. 图像生成质量的提升 2. 多模态…

SIPp uac.xml 之我见

https://sipp.sourceforge.net/doc/uac.xml.html 这个 uac.xml 有没有问题呢&#xff1f; 有&#xff01; 问题之一是&#xff1a; <recv response"200" rtd"true" rrs"true"> 要加 rrs, 仔细看注释就能看到 问题之二是&#xff1…

vue3补充

form表单重置 const { proxy } getCurrentInstance()!; // 获取挂载在全局的上下文proxy.resetForm(ruleFormRef); // 在el-form中清空ref为ruleFormRef的表单注&#xff1a;不推荐使用 不推荐的原因 类型安全问题&#xff1a; 当在 TypeScript 环境中使用时&#xff0c;…

算法leecode笔记

具体代码&#xff1a; class Solution { public:vector<int> twoSum(vector<int>& nums, int target) {unordered_map<int, int> hashtable;for (int i 0; i < nums.size(); i) {auto it hashtable.find(target - nums[i]);if (it ! hashtable.end…

Rust编写Windows服务

文章目录 Rust编写Windows服务一&#xff1a;Windows服务程序大致原理二&#xff1a;Rust中编写windows服务三&#xff1a;具体实例 Rust编写Windows服务 编写Windows服务可选语言很多, 其中C#最简单。本着练手Rust语言&#xff0c;尝试用Rust编写一个服务。 一&#xff1a;Win…

Git之如何删除Untracked文件(六十八)

简介&#xff1a; CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布&#xff1a;《Android系统多媒体进阶实战》&#x1f680; 优质专栏&#xff1a; Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a; 多媒体系统工程师系列【…

【编程基础知识】mysql根据某个int字段计算到每一行为止的累加值怎么实现

一、方式一&#xff1a;窗口函数 在MySQL中&#xff0c;可以使用窗口函数&#xff08;Window Functions&#xff09;来计算每一行的累加值。如果你使用的是MySQL 8.0或更高版本&#xff0c;可以使用 SUM() 窗口函数结合 OVER() 子句来实现这个需求。 假设你有一个名为 sales 的…

Oracle数据库中的动态SQL(Dynamic SQL)

Oracle数据库中的动态SQL是一种在运行时构建和执行SQL语句的技术。与传统的静态SQL&#xff08;在编写程序时SQL语句就已经确定&#xff09;不同&#xff0c;动态SQL允许开发者在程序执行过程中根据不同的条件或用户输入来构建SQL语句。这使得动态SQL在处理复杂查询、存储过程中…

【计算机网络】UDP 协议详解及其网络编程应用

文章目录 一、引言二、UDP1、UDP的协议格式2、UDP 报文的解包和分用3、UDP面向数据报的特点 三、UDP输入输出四、UDP网络编程 一、引言 UDP&#xff08;User Datagram Protocol&#xff0c;用户数据报协议&#xff09;是一种网络通信协议&#xff0c;它属于传输层的协议。是一…

PostgreSQL - tutorial

本文翻译整理自&#xff1a;官方文档 Preface 和 第一部分&#xff08;I. Tutorial&#xff09; 有需要的可以前往官方文档查看&#xff1a;https://www.postgresql.org/docs/15/index.html 文章目录 序言1.什么是PostgreSQL&#xff1f;2. PostgreSQL简史2.1 伯克利POSTGRES项…

【linux】ln 命令

ln 命令在 Linux 系统中用于创建链接&#xff08;links&#xff09;&#xff0c;它允许你创建一个文件的引用&#xff0c;指向该文件系统中的另一个位置。这种链接可以是硬链接&#xff08;hard link&#xff09;或软链接&#xff08;软连接&#xff0c;也称为符号链接&#xf…

HTTP中的Cookie与Session

一、背景 HTTP协议是无状态无连接的。 无状态&#xff1a;服务器不会保存客户端历史请求记录&#xff0c;每一次请求都是全新的。 无连接&#xff1a;服务器应答后关闭连接&#xff0c;每次请求都是独立的。 无状态就导致服务器不认识每一个请求的客户端是否登陆过。 这时…

【贪心算法】贪心算法

贪心算法简介 1.什么是贪心算法2.贪心算法的特点3.学习贪心的方向 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1f603; 1.什么是贪心算法 与其说是…

Spring为什么要用三级缓存解决循环依赖?

Spring为什么要用三级缓存解决循环依赖&#xff1f; 1. Spring是如何创建一个bean对象2. Spring三级缓存2.1 一级缓存&#xff1a;单例池&#xff0c;经历过完整bean生命&#xff0c;单例Bean对象2.2 二级缓存&#xff1a;提前暴露的Bean2.3 三级缓存&#xff1a;打破循环 3. S…

计算机网络通关学习(一)

简介 之前我通过王道的考研课进行了计算机网络的学习&#xff0c;但是在秋招准备过程中发现之前的笔记很多不足&#xff0c;学习的知识不够深入和巩固&#xff0c;所以再重新对《图解HTTP》&《图解TCP/IP》进行深度学习后&#xff0c;总结出了此篇博客&#xff0c;由于内容…

【C#】添加临时环境变量

在C#中&#xff0c;可以通过System.Environment类来添加临时环境变量。临时环境变量只在当前进程中有效&#xff0c;进程结束后变量即失效&#xff0c;不会写入系统的Path中。 using System;class Program {static void Main(){// 设置临时环境变量Environment.SetEnvironment…

08_Python数据类型_字典

Python的基础数据类型 数值类型&#xff1a;整数、浮点数、复数、布尔字符串容器类型&#xff1a;列表、元祖、字典、集合 字典 字典&#xff08;Dictionary&#xff09;是一种可变容器模型&#xff0c;它可以存储任意类型对象&#xff0c;其中每个对象都存储为一个键值对。…