【数据结构】【C++】AVL树的模拟实现(插入、判断、旋转)

文章目录

  • 1 概念
  • 2 实现
    • 2.1 AVL树结点的定义
    • 2.2 AVL树的插入
      • 2.2.1 AVL树的插入规则
      • 2.2.2 旋转
        • 2.2.2.1 左单旋
        • 2.2.2.2 右单旋
        • 2.2.2.3 左右双旋
        • 2.2.2.4 右左双旋
      • 2.2.3 总结
  • 3 平衡判断
  • 4 删除
  • 5 源码

1 概念

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

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

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

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

关于平衡因子:
平衡因子是左右高度之差,实现时可以定义为右子树高度减左子树高度,也可以定义为左子树高度减右子树高度(因为是用绝对值判断是否合法)

平衡因子不是AVL树中必须设计的,只是一种帮助我们控制树的方式,但是使用平衡因子会比较方便。

2 实现

2.1 AVL树结点的定义

AVL树究其本质,也是一棵二叉树,所以结点定义需要包括左右孩子指针,同时还需要一个指向父亲的指针方便之后的操作。另外,AVL树通常是一个<K, V>模型的数据结构,所以内部的数据域用一个pair存储。

template <class K,class V>
struct AVLTreeNode
{
	AVLTreeNode* _left;
	AVLTreeNode* _right;
	AVLTreeNode* _parent;
	int _bf;	// banance factor(平衡因子),这里用右子树高度减左子树高度表示
	pair<K, V> _kv;

	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
		, _kv(kv)
	{}
};

2.2 AVL树的插入

2.2.1 AVL树的插入规则

由于AVL树也是一棵搜索二叉树,因此也需要遵循搜索二叉树的规则进行插入。

如果插入节点大于当前遍历结点,则在当前结点的右子树中继续寻找插入
如果插入节点小于当前遍历结点,则在当前结点的左子树中继续寻找插入
如果插入节点等于当前结点,不能插入,返回false

上面我们也提到,AVL树是高度平衡的二叉搜索树,所以在遵循二叉搜索树插入规则的基础上还需要进行特殊的判断和处理。

显然,在对一棵二叉树进行插入操作时,是有可能改变这棵树的高度的。而对于AVL树,如果执行插入操作后,使得某一个结点的平衡因子绝对值不再为0或者1,那么表示AVL树被破坏了,此时我们就需要执行一些处理维护AVL树。

先来思考一下,如果我们对AVL树进行了插入操作,哪些结点的平衡因子会发生变化呢?

其实很容易发现,当我插入一个结点以后,可能会导致高度发生变化。由于平衡二叉树的特性,就有可能导致该结点到根结点路径上的结点平衡因子都可能会发生变化,那么就需要分情况讨论。

如果插入结点之后,以该结点父亲为根结点的子树高度没有变化,那么对AVL树是没有破坏性的,但是一般来说,父结点的平衡因子会发生变化,如下图1情形,插入结点以后没有影响到父结点为根的子树高度,但是影响父结点的平衡因子需要更新:
Pasted image 20240329174803.png

这里设成功插入结点后该结点为c,其父结点为p。由我们给定的平衡因子的定义(右子树高度减去左子树高度),如果c为p的左孩子,那么父亲的平衡因子就会减少1;反之就会增加1。

那我们就发现,在AVL树中插入一个结点,一定会影响到的是该结点的父结点的平衡因子,因为插入会导致该父结点的左右子树高度关系发生变化,但是是否会继续向上影响祖先结点呢?这就需要根据情况讨论了。

如下图2,插入结点以后,不仅影响了p的平衡因子,也影响了p父亲的平衡因子(因为p所在子树的高度发生变化。而对于第一张图中的情形,是不会影响到p的父亲的。
![[Pasted image 20240329175508.png]]

我们会发现,上述两张图的区别是,当插入后p的平衡因子变为0时(图1),表示左右子树高度是一样的,那么肯定在插入前左右子树高度相差1,也就是插入前p的平衡因子为±1,此时没有改变p所在子树的整体高度,不会影响p的父亲。

当插入后p的平衡因子变为1时(图2),表示p之前的平衡因子一定是0,插入一个节点后p所在子树的高度一定变化,因此对于p的父亲,其左右子树高度差肯定也会变化。

为什么只能从0变成1,而不能从2变成1?
因为这是在高度平衡的搜索二叉树。如果插入前某个结点的平衡因子为2,表示插入前的树本身就不是一个AVL树。对于合法的AVL树,情况一定如上。

当插入后p的平衡因子变为2(即右子树高度比左子树高2)时,表示这时出现了不合法的AVL树,因此我们就需要对其进行一些处理使其变为AVL树。处理的方式其实就是旋转,下文中将继续叙述。

以上我们总结以下AVL树插入的规则:

  1. 按照搜索二叉树插入规则插入
  2. 更新平衡因子
    • 更新原则:
      如果c是p的左孩子, p -> _bf –
      如果c是p的右孩子, p -> _bf ++

    • 是否需要继续更新取决于p所在子树的高度是否变化:
      如果更新后,p -> _bf == 0,表示更新前p -> _bf 一定为±1,更新之后p的高度没有变化,不会影响p的父亲,结束,返回true

      如果更新后,p -> _bf == ±1,表示更新前p -> _bf 一定为0,更新之后p变得不均衡,但是并没有违反规则。p的高度一定发生变化,会影响p的父亲,那么需要继续向上更新**(最坏更新到root位置)**

      如果更新后,p -> _bf == ±2,表示p所在的子树违反了平衡规则。那么就需要进行特殊处理使其合法,这个处理就是旋转。旋转以后的树就合法了,结束更新(旋转可以让p所在子树高度回到插入之前的状态,不会对上层的平衡因子有影响)

综上,更新结束的标志有三点:

  1. 更新到root位置,结束
  2. 当前更新位置的parent的平衡因子为0,结束
  3. 执行旋转操作以后,结束

2.2.2 旋转

旋转的目的:

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

以下是旋转的一个示例:
![[Pasted image 20240401220131.png]]

![[Pasted image 20240401220147.png]]

2.2.2.1 左单旋

左单旋的意思就是:一个结点是其父结点的右孩子,经过旋转操作以后,父亲变为该结点的左孩子,而该结点的左孩子变成父亲的右孩子

左单旋发生的场景:

parent -> _bf == 2 && cur -> _bf == 1
![[Pasted image 20240401223018.png]]

void RotateL(Node* parent)
	{
		Node* subR = parent->_right;	// 父亲的右孩子
		Node* subRL = subR->_left;		// 父亲右孩子的左孩子

		parent->_right = subRL;
		if(subRl)
			subRL->_parent = parent;

		subR->_left = parent;
		Node* ppnode = parent->_parent;//记录当前父亲的父亲
		parent->_parent = subR;

		if (parent == _root)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (parent == ppnode->_left)
			{
				ppnode->_left = subR;
			}
			else
			{
				ppnode->_right = subR;
			}
			subR->_parent = ppnode;
		}

		// 更改平衡因子
		subR->_bf = 0;
		parent->_bf = 0;
	}

旋转的细节:

  • 更改结点指针指向时,还要更改对应_parent指针的指向。
  • subR的左指针可能为空,注意空指针解引用的问题。
  • subR要链接到parent -> _parent 的相应左 / 右指针(与parent的位置一致)
  • 如果parent是root,还需要更改root
  • 最后要记得修改平衡因子(单旋以后,parent和cur的平衡因子都是0,因此单旋是非常健康的旋转操作)
2.2.2.2 右单旋

右单旋就是左单旋的对称情况,对照左单旋的代码更改即可。

// 右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;	// 父亲的左孩子
		Node* subLR = subL->_right;		// 父亲左孩子的右孩子

		parent->_left = subLR;
		// subL可能为空,要注意空指针的问题
		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 (parent == ppnode->_left)
			{
				ppnode->_left = subL;
			}
			else
			{
				ppnode->_right = subL;
			}
			subL->_parent = ppnode;
		}

		// 更改平衡因子
		subL->_bf = 0;
		parent->_bf = 0;
	}
2.2.2.3 左右双旋

双旋又是什么样的情况呢?我们先以左右双旋的情景来分析。假设有如下情景:
![[Pasted image 20240406205916.png]]

对于如上情形,在30的右边插入结点以后,30的平衡因子变为1,而其父亲的平衡因子变为-2,此时就会触发左右双旋。

如果我们继续按照右单旋的思路执行旋转,读者可以自行尝试,总之最后会发现旋转了个寂寞。

那么就说明,对于上述这种情况,单旋是走不通的,所以就需要新的旋转策略。为了更好的解释这个旋转策略,以以下的例子进行说明
![[Pasted image 20240406210238.png]]

上图中,我们将30的右子树划分为两部分。不难发现,无论新插入的结点在60的左子树还是右子树,都会引发90平衡因子变为-2。

首先先看30为根的这个树,先假设新插入结点在60的左子树,我们来看看对其进行左单旋会发生什么呢
![[Pasted image 20240406210559.png]]

再看这种情况,是不是有点类似于右单旋的样子?此时如果再对90进行右单旋操作
![[Pasted image 20240406210816.png]]

最后得到的树就是合法的AVL树了。

其实,无论第一步的插入是在60的左子树还是右子树,运用上述的策略得到的都是合法的AVL树,如果插入在右子树,旋转之后的树就是这样的。
![[Pasted image 20240406211042.png]]

那么我们就分析出了左右双旋操作的一部分:

前提:cur -> _bf == 1 && parent -> _bf == -2
操作:先对cur执行左单旋;再对parent执行右单旋

分析结束其实可以发现,左右双旋的旋转部分是比较容易得,但是旋转以后还需要修改平衡因子,而双旋平衡因子的修改就比较复杂了。
![[Pasted image 20240406211726.png]]
在这里插入图片描述

其实,对于情况1和2,就是上述我们给到的两种旋转示例。从上面两张图中就可以看出,更新之后60的bf永远是0,而30和90的bf就要根据更新之前60的bf来判断。

而如果更新之前,60本身就是要新增的结点,那么在双旋之后,三个结点的bf都应该是0。

所以,更新后的平衡因子如何改变,本质是取决去60这个节点,也就是parent -> _left -> _right的平衡因子是多少。

    // 左右双旋
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;
		RotateL(parent->_left);
		RotateR(parent);

		if (bf == 1)
		{
			subLR->_bf = 0;
			subL->_bf = -1;
			parent->_bf = 0;
		}
		else if (bf == -1)
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 0)
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

![[Pasted image 20240405215056.png]]

2.2.2.4 右左双旋

类比左右双旋,可以写出右左双旋的情况,与左右双旋属于对称情形。

	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		RotateR(parent->_right);
		RotateL(parent);

		if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

2.2.3 总结

根据上述分析,我们就可以写出一个完善的插入函数

	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;

		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 = 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)
				{
					RotateLR(parent);
				}
				else
				{
					RotateRL(parent);
				}

				// 旋转完成以后树就合法了
				break;
			}
			else
			{
				// 插入之前AVL树已经异常
				assert(false);
			}
		}
		return true;
	}

3 平衡判断

判断一棵AVL树是否确实平衡其实是有必要的。这里不能单纯按照平衡因子判断,因为我们不能保证插入过程中平衡因子的改变是正确的,而且AVL树是不方便调试的,因此我们需要用最为朴素的方法,即判断树中每一个结点,其两棵子树的高度差是否等于该结点的平衡因子。

这里我们利用递归的方式实现,同时,我们可以利用后序遍历的思想,即从最低层开始判断。如果按照前序遍历的顺序,我们会发现树底部的部分会进行多次重复判断。当我们以后序遍历的顺序判断时,可以通过输出型参数的方式将底部子树高度带出,这样就减少了重复计算。

	bool _IsBalence(Node* root,int& height)
	{
		if (root == nullptr)
		{
			height = 0;
			return true;
		}
		int leftHeight = 0;
		int rightHeight = 0;
		// 将左右子树高度以输出型参数的方式带出
		if (!_IsBalence(root->_left, leftHeight) || !_IsBalence(root->_right, rightHeight))
		{
			return false;
		}
			
		if (abs(rightHeight - leftHeight) >= 2)
		{
			cout << "不平衡" << endl;
			return false;
		}
			
		if (rightHeight - leftHeight != root->_bf)
		{
			cout << "平衡因子异常" << endl;
			return false;
		}
		// 左右子树的最大高度值加上根节点这一层即为以该结点为根的子树高度
		height = max(leftHeight, rightHeight) + 1;

		return true;
	}

	bool IsBalence()
	{
		int height = 0;
		return _IsBalence(_root, height);
	}

4 删除

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

但是AVL树的删除相较于插入更为复杂,实际中很少会考察与代码相关的知识,如有兴趣可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。

5 源码

源码中还包含了Height、Size等基础操作。

#include <iostream>
#include <cassert>
using namespace std;

template <class K,class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	int _bf;	// banance factor(平衡因子)
	pair<K, V> _kv;

	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
		, _kv(kv)
	{}
};

template <class K, class V>
class AVLTree
{
public:
	typedef AVLTreeNode<K,V> Node;

	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;

		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 = 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)
				{
					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;
		// subR可能为空,要注意空指针的问题
		if(subRL)
			subRL->_parent = parent;

		subR->_left = parent;
		Node* ppnode = parent->_parent;//记录当前父亲的父亲
		parent->_parent = subR;

		if (parent == _root)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (parent == ppnode->_left)
			{
				ppnode->_left = subR;
			}
			else
			{
				ppnode->_right = subR;
			}
			subR->_parent = ppnode;
		}

		// 更改平衡因子
		subR->_bf = 0;
		parent->_bf = 0;
	}

	// 右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;	// 父亲的左孩子
		Node* subLR = subL->_right;		// 父亲左孩子的右孩子

		parent->_left = subLR;
		// subL可能为空,要注意空指针的问题
		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 (parent == ppnode->_left)
			{
				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);

		if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		RotateR(parent->_right);
		RotateL(parent);

		if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	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(Node* root)
	{
		if (root == nullptr)
			return 0;
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);

		return max(leftHeight, rightHeight) + 1;
	}

	int Height()
	{
		return _Height(_root);
	}

	bool _IsBalence(Node* root,int& height)
	{
		if (root == nullptr)
		{
			height = 0;
			return true;
		}
		int leftHeight = 0;
		int rightHeight = 0;
		// 将左右子树高度以输出型参数的方式带出
		if (!_IsBalence(root->_left, leftHeight) || !_IsBalence(root->_right, rightHeight))
		{
			return false;
		}
			
		if (abs(rightHeight - leftHeight) >= 2)
		{
			cout << "不平衡" << endl;
			return false;
		}
			
		if (rightHeight - leftHeight != root->_bf)
		{
			cout << "平衡因子异常" << endl;
			return false;
		}
		// 左右子树的最大高度值加上根节点这一层即为以该结点为根的子树高度
		height = max(leftHeight, rightHeight) + 1;

		return true;
	}

	bool IsBalence()
	{
		int height = 0;
		return _IsBalence(_root, height);
	}

	size_t _size(Node* root)
	{
		if (root == nullptr)
			return 0;
		return _size(root->_left) + _size(root->_right) + 1;
	}

	size_t size()
	{
		return _size(_root);
	}

	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < key)
			{
				cur = cur->_right;
			}
			else if (cur->_kv.first > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}

		return NULL;
	}

private:
	Node* _root = nullptr;
};

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

论文速读:Do Generated Data Always Help Contrastive Learning?

在对比学习领域&#xff0c;最近很多研究利用高质量生成模型来提升对比学习 给定一个未标记的数据集&#xff0c;在其上训练一个生成模型来生成大量的合成样本&#xff0c;然后在真实数据和生成数据的组合上执行对比学习这种使用生成数据的最简单方式被称为“数据膨胀”这与数据…

案例三 BeautifulSoup之链家二手房

本案例用到列表&#xff0c;函数&#xff0c;字符串等知识点&#xff0c;知识点参考链接如下&#xff1a; python基础知识&#xff08;一&#xff09;&输入输出函数 python基础知识&#xff08;二&#xff09;&基本命令 python基础知识&#xff08;三&#xff09;&…

如何在Linux通过docker搭建Plik文件系统并实现无公网IP管理内网文件

文章目录 1. Docker部署Plik2. 本地访问Plik3. Linux安装Cpolar4. 配置Plik公网地址5. 远程访问Plik6. 固定Plik公网地址7. 固定地址访问Plik 本文介绍如何使用Linux docker方式快速安装Plik并且结合Cpolar内网穿透工具实现远程访问&#xff0c;实现随时随地在任意设备上传或者…

盒子模型+响应式布局 + 原型链与继承

盒子模型 是什么 css布局基础,规定了元素在页面上如何呈现,以及元素之间的空间关系 由content paddingbordermargin四部分组成 为什么 盒子模型分为 标准盒子模型: 元素的宽度与高度 只包括content IE盒子模型: 元素的宽度与高度 包括content,padding,border 在实际操作中…

Python实现时间序列ARIMA模型(附带超详细理论知识和完整代码实现)

文章目录 0 结果1 介绍2 建模2.1 预备知识2.1.1 ADF检验结果&#xff08;单位根检验统计量&#xff09;2.1.2 差分序列的白噪声检验&#xff08;这里使用Ljung-Box检验&#xff09;2.1.3 ARIMA模型&#xff08;差分整合移动平均自回归模型&#xff09;的三个参数:p&#xff0c;…

如何在横向渗透攻击中寻到一线生机

横向渗透&#xff0c;作为计算机网络中的一种攻击技术&#xff0c;展现出了攻击者如何巧妙地利用同一级别系统间的漏洞和弱点&#xff0c;扩大其网络访问权限。与纵向渗透不同&#xff0c;横向渗透不关注权限的垂直提升&#xff0c;而是更侧重于在同一层级内扩展影响力。 横向…

【教程】将Vue项目打包为exe项目的教程-我的第一个原生Vue项目

文章目录 前言项目介绍正文&#xff1a;Vue打包exe过程及注意事项1. &#xff08;重要&#xff09;进入我们自己的项目&#xff0c;修改公共路径为相对路径2. &#xff08;重要&#xff09;关于VueRouter的必要修改3. 前端打包4. 拉取electron-quick-start项目5. 修改配置文件6…

【Excel】使用VBA宏简单自定义Excel软件界面

改行做经济师学习Excel&#xff0c;偶有心得&#xff0c;摘录于此&#xff0c;备忘。 言简意赅&#xff0c;仅供自用。 1 实现效果 在Excel的左上角可添加按钮&#xff0c;该按钮的功能可由我们自己通过编写代码定义&#xff0c;能实现特定功能&#xff0c;并且在所有打开的…

Java算法之时间复杂度和空间复杂度的概念和计算

1. 算法效率 如何去衡量一个算法的好坏&#xff1f; 通常我们从时间效率和空间效率两个方面去分析算法的好坏。时间效率即时间复杂度&#xff0c;空间效率被称为空间复杂度。时间复杂度主要是衡量一个算法的运行速度&#xff0c;而空间复杂度主要衡量一个算法所需要的额外空间…

业务与数据的终极对决:如何让大数据成为企业的超能力?

在数字化转型的浪潮中&#xff0c;企业如同在茫茫数据海洋中航行的船只&#xff0c;而数据资产管理就是指引航向的罗盘。但是&#xff0c;当业务需求与数据脱节、数据孤岛林立、业务流程与数据流程不同步、以及业务增长带来的数据管理挑战成为阻碍&#xff0c;我们该如何突破重…

transformer上手(7)—— 快速分词器

1 快速分词器 Hugging Face 共提供了两种分分词器&#xff1a; 慢速分词器&#xff1a;Transformers 库自带&#xff0c;使用 Python 编写&#xff1b;快速分词器&#xff1a;Tokenizers 库提供&#xff0c;使用 Rust 编写。 特别地&#xff0c;快速分词器除了能进行编码和解…

单链表链表专题

1 链表的概念 概念&#xff1a;链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 链表的结构跟⽕⻋⻋厢相似&#xff0c;淡季时⻋次的⻋厢会相应减少&#xff0c;旺季时⻋次的⻋厢会额外增加⼏节。只 需要…

Redis实现延迟任务的几种方案

&#x1f3f7;️个人主页&#xff1a;牵着猫散步的鼠鼠 &#x1f3f7;️系列专栏&#xff1a;Java全栈-专栏 &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正 目录 1.前言 2.Redis如何实现延迟任务&#xff1f; 3.代码实现 3.1. 过期键通知事…

技术速递|为 .NET iOS 和 .NET MAUI 应用程序添加 Apple 隐私清单支持

作者&#xff1a;Gerald Versluis 排版&#xff1a;Alan Wang Apple 正在推出一项隐私政策&#xff0c;将隐私清单文件包含在针对 App Store 上的 iOS、iPadOS 和 tvOS 平台的新应用程序和更新应用程序中。请注意&#xff0c;至少目前 macOS 应用程序被排除在外。 隐私清单文件…

这部经典之作,时隔六年迎来重磅升级!

&#x1f345; 作者简介&#xff1a;哪吒&#xff0c;CSDN2021博客之星亚军&#x1f3c6;、新星计划导师✌、博客专家&#x1f4aa; &#x1f345; 哪吒多年工作总结&#xff1a;Java学习路线总结&#xff0c;搬砖工逆袭Java架构师 &#x1f345; 技术交流&#xff1a;定期更新…

Niobe WiFi IoT开发板OpenHarmony内核编程开发——Semaphore

本示例将演示如何在Niobe WiFi IoT开发板上使用cmsis 2.0 接口进行信号量开发 Semaphore API分析 osThreadNew() osThreadId_t osThreadNew(osThreadFunc_t func, void *argument,const osThreadAttr_t *attr )描述&#xff1a; 函数osThreadNew通过将线程添加到活动线程列表…

TG-12F使用SDK对接阿里生活物联网平台

文章目录 前言一、注意二、准备1. 安装Ubuntu&#xff08;版本20.04 X64&#xff09;程序运行时库。按顺序逐条执行命令&#xff1a;2. 安装Ubuntu&#xff08;版本20.04 X64&#xff09;依赖软件包。按照顺序逐条执行命令&#xff1a;3. 安装Python依赖包。按照顺序逐条执行命…

vscode 打代码光标特效

vscode 打代码光标特效 在设置里面找到settings 进入之后在代码最下方加入此代码 "explorer.confirmDelete": false,"powermode.enabled": true, //启动"powermode.presets": "fireworks", // 火花效果// particles、 simple-rift、e…

FFmpeg: 自实现ijkplayer播放器--04消息队列设计

文章目录 播放器状态转换图播放器状态对应的消息&#xff1a; 消息对象消息队列消息队列api插入消息获取消息初始化消息插入消息加锁初始化消息设置消息参数消息队列初始化清空消息销毁消息启动消息队列终止消息队列删除消息 消息队列&#xff0c;用于发送&#xff0c;设置播放…

eclipse导入maven项目与配置使用本地仓库

前言 本人润国外了&#xff0c;发现不能用收费软件IDEA了&#xff0c;需要使用eclipse&#xff0c;这个免费。 但是早忘了怎么用了&#xff0c;在此总结下。 一、eclipse导入本地项目 1.选这个&#xff1a;open projects from file system… 2.找到项目文件夹&#xff0c;…