【数据结构】AVL树(图文解析 + 代码实现)

news2024/12/24 21:47:22

目录

1、AVL树的概念

2、AVL树结点的定义

3、AVL树的插入

4、AVL树的旋转

4.1 左单旋

4.2 右单旋

4.3 右左双旋

4.4 左右双旋

5、AVL树的验证

6、AVL树的性能

前面对map/multimap/set/multiset进行了简单的介绍,会大仙,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。本章介绍的AVL树和下一章介绍的红黑树,就是平衡树

1、AVL树的概念

当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

为什么不将树调整为左右子树的高度相等呢?因为有些结点数量下(如2和4个结点),做不到左右子树高度相等,所以最好的平衡二叉树就是高度不超过1

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

上面这棵树的平衡因子是用右子树高度-左子树高度,这不是一定的,也可以反过来,并没有严格规定,这篇文章中的平衡因子都是用右子树高度-左子树高度的

因为AVL树每个结点的左右子树高度差不超过1,所以平衡因子只有在-1、0、1是才是正常的

2、AVL树结点的定义

我们这里实现的是KV模型的AVL树,为了与map对应,两个值是以pair的形式存储。并且每个结点中还要增加一个平衡因子。当插入新结点时,为了检查路径上是否出现异常的平衡因子,还要增加一个指向父亲结点的指针,另外原来还有指向左右孩子的指针,称为三叉链结构

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

根结点的_parent置为nullptr 

3、AVL树的插入

向AVL树中插入一个新结点时,插入的操作是和二叉搜索树一致的,只是插入一个结点后,需要更新一下这个结点到这棵树的根节点路径上部分结点的平衡因子,若是平衡因子出现异常,则需要进行旋转操作。

插入结点,会影响部分祖先结点的平衡因子。因为这里的平衡因子 = 右子树高度 - 左子树高度

结点插入在左子树,其父亲的平衡因子--

结点插入在右子树,其父亲的平衡因子++

是否需要继续往上更新祖先,要看parent所在子树的高度是否发生了变化,此时有3种情况:

1. 更新后parent的平衡因子变成0

说明没插入时parent的平衡因子是-1/1,一边高一边低,插入后,变成两边一样高,parent所在子树的高度没有发生变化,所以不需要向上更新

2. 更新后parent的平衡因子变成-1/1

说明没插入时parent的平衡因子是0,两边一样高,插入后,变成一边高一边低,parent所在子树的高度变高了,所以需要继续向上更新

3. 更新后parent的平衡因子变成-2/2

说明没插入时parent的平衡因子是-1/1,插入结点插入在高的那一边,进一步加剧了parent所在子树的不平衡,已经违反规定,需要旋转处理

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 = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			// 不平衡了,旋转处理
		}
		else
		{
			assert(false);
		}
	}

	return true;
}

4、AVL树的旋转

4.1 左单旋

当新结点插入到右子树的右侧,需要进行左单旋 ----- 右右:左单旋

这里面h>=0,表示a、b、c是高度为h的AVL子树

左单旋的标志就是出现异常的结点的平衡因子为2,并且其右子树的平衡因子为1,那么这个时候就进行左单旋。

左单旋步骤:

1. 将subRL变成parent的右子树

2. 将parent变成subR的左子树

3. 建立subR与parent的父亲结点parentParent的关系

4. 更新parent和subR的平衡因子 

完成代码时,除了要完成每个结点的链接关系,还需要注意重新链接后结点父亲的变化和平衡因子的变化。其中平衡因子只有30和60这两个结点有变化,因为要改变平衡因子,需要改变左右子树的高度,并且这两个结点的平衡因子都变成了0。注意,subRL是有可能为nullptr的,当h等于0时,所以修改其父亲结点时需要判断一下。

void RotateL(Node* parent) // 左单旋
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* parentParent = parent->_parent;
	// 1. 将subRL变成parent的右子树
	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;
	// 2. 将parent变成subR的左子树
	subR->_left = parent;
	parent->_parent = subR;
	// 3. 建立subR与parent的父亲结点parentParent的关系
	if (parentParent == nullptr) // 说明parent就是根结点
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)
			parentParent->_left = subR;
		else
			parentParent->_right = subR;
		subR->_parent = parentParent;
	}
	// 4. 更新parent和subR的平衡因子 
	parent->_bf = subR->_bf = 0;
}

4.2 右单旋

当新插入的结点在左子树的左侧,需要进行右单旋 ----- 左左:右单旋

右单旋的标志就是出现异常的结点的平衡因子为-2,并且其左子树的平衡因子为-1,那么这个时候就进行左单旋。 

右单旋的步骤:

1. 将subLR变成parent的左子树

2. 将parent变成subL的右子树

3. 建立subL与parent的父亲结点parentParent的关系

4. 更新parent和subL的平衡因子 

void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* parentParent = parent->_parent;
	// 1. 将subLR变成parent的左子树
	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;
	// 2. 将parent变成subL的右子树
	subL->_right = parent;
	parent->_parent = subL;
	// 3. 建立subL与parent的父亲结点parentParent的关系
	if (parentParent == nullptr) // 说明parent就是根结点
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else
	{
		if (parentParent->_left == parent)
			parentParent->_left = subL;
		else
			parentParent->_right = subL;
		subL->_parent = parentParent;
	}
	// 4. 更新parent和subL的平衡因子 
	parent->_bf = subL->_bf = 0;
}

4.3 右左双旋

当新插入的结点在右子树的左侧,需要先进行右单旋,再进行左单旋 ----- 右左:右左双旋

右左双旋的标志是出现异常的结点的平衡因子是2,其右子树的平衡因子是-1

第一种情况:在右子树的左子树的右子树插入(即c子树插入)

  第二种情况:在右子树的左子树的左子树插入(即b子树插入)

这两种情况都是先进行右单旋,然后进行左单旋,只是旋转完成后,个别结点的平衡因子不同。如何区分是在b插入还是c插入呢?

当h > 0时

1. 插入结点后,若右子树的左子树这个结点的平衡因子是1,则是在c插入的

2. 插入结点后,若右子树的左子树这个结点的平衡因子是-1,则是在b插入的

上面两幅图就是h > 0的两种情况的图

当h == 0 时

3. 插入之前连60这个结点都没有,60这个结点自己就是新增,此时60这个结点的平衡因子是0

右左单旋步骤:

1、计算subRL的平衡因子

2、对subR进行右单旋

3、对parent进行左单旋

4、更新parent、subR、subRL的平衡因子

void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	// 1、计算subRL的平衡因子
	int bf = subRL->_bf;
	// 2、对subR进行右单旋
	RotateR(subR);
	// 3、对parent进行左单旋
	RotateL(parent);
	// 4、更新parent、subR、subRL的平衡因子
	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);
	}
}

4.4 左右双旋

当新插入的结点在左子树的右侧,需要先进行左单旋,再进行右单旋 ----- 左右:左右双旋

左右双旋的标志是出现异常的结点的平衡因子是-2,其左子树的平衡因子是1

第一种情况:在左子树的右子树的左子树插入(即b子树插入)

第二种情况:在左子树的右子树的右子树插入(即c子树插入)

第三种情况:当h == 0时

左右单旋的步骤:

1. 计算subLR的平衡因子

2. 对subL进行左单旋

3. 对parent进行右单旋

4. 更新parent、subR、subRL的平衡因子

void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	// 1. 计算subLR的平衡因子
	int bf = subLR->_bf;
	// 2. 对subL进行左单旋
	RotateL(subL);
	// 3. 对parent进行右单旋
	RotateR(parent);
	// 4. 更新parent、subR、subRL的平衡因子
	if (bf == 0)
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == 1)
	{
		subL->_bf = -1;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

会发现同号单旋,异号双旋

所以此时插入操作的代码为

bool Insert(const pair<K, V>& kv)
{
	// 若_root为空,直接让新插入的结点变成根
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}
	// 若_root不为空,按二叉搜索树的规则找到插入的位置,同时也要记录父亲结点
	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 = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			// 不平衡了,旋转处理
			if (parent->_bf == 2 && parent->_right->_bf == 1)
			{
				RotateL(parent);
			}
			else if (parent->_bf == -2 && parent->_left->_bf == -1)
			{
				RotateR(parent);
			}
			else if (parent->_bf == 2 && parent->_right->_bf == -1)
			{
				RotateRL(parent);
			}
			else
			{
				RotateLR(parent);
			}
			break; // 旋转完后,一定平衡了,所以可以跳出循环
		}
		else
		{
			assert(false);
		}
	}

	return true;
}

注意,旋转完成之后,这颗子树的父亲结点的平衡因子一定是0,所以可以直接跳出循环,不需要继续向上更新平衡因子

5、AVL树的验证

在前面,我们已经实现了插入操作,那么按照这个插入函数获得的真的是一颗AVL树吗?

所以,我们需要进行验证。验证只需要计算每个结点的左右树高,判断左右树高的差值小于2即可

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;
}
bool _IsBalanceTree(Node* root)
{
	// 空树也是AVL树
	if (root == nullptr) return true;
	// 计算pRoot节点的平衡因子:即pRoot左右子树的高度差
	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);
	int diff = rightHeight - leftHeight;
	// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
	// pRoot平衡因子的绝对值超过1,则一定不是AVL树
	if (abs(diff) >= 2 || root->_bf != diff)
		return false;
	// pRoot的左和右如果都是AVL树,则该树一定是AVL树
	return _IsBalanceTree(root->_left) && _IsBalanceTree(root -> _right);
}

这两个函数需要写在AVL树内部,因为用到了_root

6、AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2 (N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

template<class K,class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	// 三叉链
	AVLTreeNode* _left;
	AVLTreeNode* _right;
	AVLTreeNode* _parent;
	int _bf; // 平衡因子
	AVLTreeNode(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:
	AVLTree() = default;
	~AVLTree()
	{
		Destory(_root);
		_root = nullptr;
	}
	AVLTree(const AVLTree<K,V>& kv)
	{
		_root = Copy(kv._root);
	}
	AVLTree operator=(AVLTree<K, V> kv)
	{
		swap(_root, kv._root);
	}
	bool Insert(const pair<K, V>& kv)
	{
		// 若_root为空,直接让新插入的结点变成根
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		// 若_root不为空,按二叉搜索树的规则找到插入的位置,同时也要记录父亲结点
		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 = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				// 不平衡了,旋转处理
				if (parent->_bf == 2 && parent->_right->_bf == 1)
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && parent->_left->_bf == -1)
				{
					RotateR(parent);
				}
				else if (parent->_bf == 2 && parent->_right->_bf == -1)
				{
					RotateRL(parent);
				}
				else
				{
					RotateLR(parent);
				}
				break; // 旋转完后,一定平衡了,所以可以跳出循环
			}
			else
			{
				assert(false);
			}
		}

		return true;
	}
	void InOrder()
	{
		_InOrder(_root);
	}
	bool IsBalanceTree()
	{
		return _IsBalanceTree(_root);
	}
	int Height()
	{
		return _Height(_root);
	}
	int Size()
	{
		_Size(_root);
	}
private:
	int _Size(Node* root)
	{
		return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
	}
	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;
	}
	bool _IsBalanceTree(Node* root)
	{
		// 空树也是AVL树
		if (root == nullptr) return true;
		// 计算pRoot节点的平衡因子:即pRoot左右子树的高度差
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		int diff = rightHeight - leftHeight;
		// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
		// pRoot平衡因子的绝对值超过1,则一定不是AVL树
		if (abs(diff) >= 2 || root->_bf != diff)
			return false;
		// pRoot的左和右如果都是AVL树,则该树一定是AVL树
		return _IsBalanceTree(root->_left) && _IsBalanceTree(root -> _right);
	}
	void RotateL(Node* parent) // 左单旋
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* parentParent = parent->_parent;
		// 1. 将subRL变成parent的右子树
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;
		// 2. 将parent变成subR的左子树
		subR->_left = parent;
		parent->_parent = subR;
		// 3. 建立subR与parent的父亲结点parentParent的关系
		if (parentParent == nullptr) // 说明parent就是根结点
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (parent == parentParent->_left)
				parentParent->_left = subR;
			else
				parentParent->_right = subR;
			subR->_parent = parentParent;
		}
		// 4. 更新parent和subR的平衡因子?
		parent->_bf = subR->_bf = 0;
	}
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* parentParent = parent->_parent;
		// 1. 将subLR变成parent的左子树
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;
		// 2. 将parent变成subL的右子树
		subL->_right = parent;
		parent->_parent = subL;
		// 3.?建立subL与parent的父亲结点parentParent的关系
		if (parentParent == nullptr) // 说明parent就是根结点
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (parentParent->_left == parent)
				parentParent->_left = subL;
			else
				parentParent->_right = subL;
			subL->_parent = parentParent;
		}
		// 4.更新parent和subL的平衡因子
		parent->_bf = subL->_bf = 0;
	}
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		// 1、计算subRL的平衡因子
		int bf = subRL->_bf;
		// 2、对subR进行右单旋
		RotateR(subR);
		// 3、对parent进行左单旋
		RotateL(parent);
		// 4、更新parent、subR、subRL的平衡因子
		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;
		// 1. 计算subLR的平衡因子
		int bf = subLR->_bf;
		// 2. 对subL进行左单旋
		RotateL(subL);
		// 3. 对parent进行右单旋
		RotateR(parent);
		// 4.?更新parent、subR、subRL的平衡因子
		if (bf == 0)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == -1)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
	void Destory(Node* root)
	{
		if (root == nullptr) return;
		Destory(root->_left);
		Destory(root->_right);
		delete root;
	}
	Node* Copy(Node* root)
	{
		if (root == nullptr) return nullptr;
		Node* newNode = new Node(root->_kv);
		newNode->_left = Copy(root->_left);
		newNode->_right = Copy(root->_right);
		return newNode;
	}
	void _InOrder(Node* _root)
	{
		if (_root == nullptr) return;
		_InOrder(_root->_left);
		cout << _root->_kv.first << "--" << _root->_kv.second << "--" << _root->_bf << endl;
		_InOrder(_root->_right);
	}
	Node* _root;
};

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

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

相关文章

力扣 二分查找

二分查找基础篇。 题目 class Solution {public int searchInsert(int[] nums, int target) {int l 0, r nums.length - 1;while(l < r) {int mid l((r-l)>>1);//(lr)/2if(nums[mid]<target)lmid1;else rmid-1;}return l;//处理边界&#xff0c;设定数组的左半…

剪画小程序:相册里的视频变成微信动图表情包,很简单!

Hello&#xff0c;各位小伙伴们好啊&#xff01; 今天小画告诉大家怎么把微信里的视频变成微信专属表情包 让你的聊天变得更加有趣&#xff01; 一、首先&#xff0c;我们要将那些有趣的视频保存到手机相册里 在微信聊天时遇到眼前一亮的视频或在短视频平台上刷到的有趣的视…

linux虚拟机主机配置网卡

问题复现 我的虚拟主机了连不上远程工具windTerm ,但是我的另一台虚拟主机可以连上 我的解决思路 ping ip 地址 发现能够 ping 通 查看 ifconfig 配置信息 我对比另一个虚拟主机 发现了我的子网掩码netmask有问题 解决方式 第一种 连接配置 配置 ipv4.addresses 192.168.1…

JAVA毕业设计153—基于Java+Springboot+小程序的校园维修管理系统小程序(源代码+数据库)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于JavaSpringboot小程序的校园维修管理系统小程序(源代码数据库)153 一、系统介绍 本项目分为用户、维修员、管理员三种角色 1、用户&#xff1a; 注册、登录、报修申报、报修…

顺序表和单链表的经典算法题

目录 前言 一、基础思想&#xff08;数组&#xff09; 1. 移除元素 2.删除有序元素的重复项 3.合并两个有序数组 二、单链表算法 1.移除链表元素 2.翻转链表 3.合并两个有序的链表 前言 Hello,小伙伴们&#xff0c;今天我们来做一个往期知识的回顾&#xff0c;今天我将…

Spring源码(六)--BeanFactory 实现与继承关系

BeanFactory 实现与继承关系 这些接口和类的源码&#xff0c; 每一个都可以看一下。 ListableBeanFactory 由bean工厂实现的BeanFactory接口的扩展&#xff0c;这些bean工厂可以枚举它们所有的bean实例&#xff0c;而不是按客户端请求逐个按名称进行bean查找。 Hierarchic…

C# yaml 配置文件的用法(一)

目录 一、简介 二、yaml 的符号 1.冒号 2.短横杆 3.文档分隔符 4.保留换行符 5.注释 6.锚点 7.NULL值 8.合并 一、简介 YAML&#xff08;YAML Aint Markup Language&#xff09;是一种数据序列化标准&#xff0c;广泛用于配置文件、数据交换和存储。YAML的设计目标是…

Kotlin 协程 — 基础

Kotlin 协程 — 基础 协程已经存在一段时间了&#xff0c;关于它的各种文章也很多。但我发现想要了解它还比较费时&#xff0c;所以我花了一段时间才真正理解了协程的基础知识以及它的工作原理。因此&#xff0c;我想分享一些我理解到的内容。 什么是协程&#xff1f; 协程代表…

【MySQL进阶之路 | 高级篇】事务的ACID特性

1. 数据库事务概述 事务是数据库区别于文件系统的重要特性之一&#xff0c;当我们有了事务就会让数据库始终保持一致性&#xff0c;同时我们还能通过事务的机制恢复到某个时间点&#xff0c;这样可以保证给已提交到数据库的修改不会因为系统崩溃而丢失。 1.1 基本概念 事务&…

企业微信获客助手广告平台深度回传/双回传设置教程参考

很多商家在使用【转化宝】进行推广时只采用了单回传&#xff0c;其实很多情况下单回传即可满足推广模型优化需求&#xff1b;但是最近很多专业化广告运营的代投或运营都开始采用双回传&#xff0c;【转化宝】支持抖音巨量引擎、百度营销广告、快手广告、腾讯广告等均支出深度优…

微信小程序开发:项目程序代码构成

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

开源模型应用落地-LangChain高阶-记忆组件-ConversationBufferMemory正确使用(一)

一、前言 LangChain 的记忆组件发挥着至关重要的作用&#xff0c;其旨在协助大语言模型&#xff08;LLM&#xff09;有效地留存历史对话信息。通过这一功能&#xff0c;使得大语言模型在对话过程中能够更出色地维持上下文的连贯性和一致性&#xff0c;进而能够像人类的记忆运作…

【网络安全】构建稳固与安全的网络环境:从“微软蓝屏”事件中汲取的教训

发生什么事了&#xff1f; 近日&#xff0c;一次由微软视窗系统软件更新引发的全球性“微软蓝屏”事件&#xff0c;不仅成为科技领域的热点新闻&#xff0c;更是一次对全球IT基础设施韧性与安全性的深刻检验。这次事件&#xff0c;源于美国电脑安全技术公司“众击”提供的一个…

中断和EXIT原理介绍

中断和EXIT原理介绍 一、中断的介绍&#xff1f;二、EXIT的介绍1.EXIT作用2.EXIT的详情3.EXIT中AFIO复用的作用4.STM32中AFIO复用作用 一、中断的介绍&#xff1f; 二、EXIT的介绍 EXTI&#xff08;Extern Interrupt&#xff09;外部中断 1.EXIT作用 EXTI可以监测指定GPIO口…

Java学习笔记(五)数组、冒泡排序

Hi i,m JinXiang ⭐ 前言 ⭐ 本篇文章主要介绍Java数组、冒泡排序使用以及部分理论知识 &#x1f349;欢迎点赞 &#x1f44d; 收藏 ⭐留言评论 &#x1f4dd;私信必回哟&#x1f601; &#x1f349;博主收将持续更新学习记录获&#xff0c;友友们有任何问题可以在评论区留言 …

力扣SQL50 指定日期的产品价格 双重子查询 coalesce

Problem: 1164. 指定日期的产品价格 coalesce 的使用 简洁版 &#x1f468;‍&#x1f3eb; 参考题解 select distinct p1.product_id,coalesce((select p2.new_pricefrom Products p2where p2.product_id p1.product_id and p2.change_date < 2019-08-16order by p2.…

Web前端Promise

Promise介绍与使用 Promise是什么&#xff1f; 1.抽象表达&#xff1a; Promise是一门新的技术&#xff08;ES6规范&#xff09;Promise是JS中进行异步编程的新解决方案备注&#xff1a;旧方案是单纯使用回调函数 2.具体表达&#xff1a; 从语法上来说&#xff1a;Promise…

xmind--如何快速将Excel表中多列数据,复制到XMind分成多级主题

每次要将表格中的数据分成多级时&#xff0c;只能复制粘贴吗 快来试试这个简易的方法吧 这个是原始的表格&#xff0c;分成了4级 步骤&#xff1a; 1、我们可以先按照这个层级设置下空列&#xff08;后买你会用到这个空列&#xff09; 二级不用加、三级前面加一列、四级前面加…

前端:Vue学习 - 购物车项目

前端&#xff1a;Vue学习 - 购物车项目 1. json-server&#xff0c;生成后端接口2. 购物车项目 - 实现效果3. 参考代码 - Vuex 1. json-server&#xff0c;生成后端接口 全局安装json-server&#xff0c;json-server官网为&#xff1a;json-server npm install json-server -…

C++ 设计模式(五)——状态模式

状态模式 序言理解源码 序言 设计模式只是一个抽象的设计模式方法&#xff0c;并不是一个固定使用的搭配&#xff0c;就算是普通switch语句&#xff0c;Map&#xff0c;乃至状态机都是状态模式的其中一种实现方法 状态模式看起来好像和策略模式差不多&#xff0c;主要是其的侧…