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

news2024/11/15 10:20:57

文章目录

  • 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;
};

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

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

相关文章

软件测试/测试开发丨接口测试学习笔记分享

一、Mock 测试 1、Mock 测试的场景 前后端数据交互第三方系统数据交互硬件设备解耦 2、Mock 测试的价值与意义 不依赖第三方数据节省工作量节省联调 3、Mock 核心要素 匹配规则&#xff1a;mock的接口&#xff0c;改哪些接口&#xff0c;接口哪里的数据模拟响应 4、mock实…

【每日刷题】Day15

【每日刷题】Day15 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; 目录 1. 141. 环形链表 - 力扣&#xff08;LeetCode&#xff09; 2. 142. 环形链表 II - 力扣&#xff08;LeetCode&#xff09; 3. 143. 重…

基于Python的微博舆论分析,微博评论情感分析可视化系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

IDEA 本地库引入了依赖但编译时找不到

在使用 IDEA 开发 Maven 项目的过程中&#xff0c;有时会遇到本地库引入了依赖&#xff0c;但编译时报找不到这个依赖&#xff0c;可以使用命令处理。 打开 Terminal。 执行清理命令。 mvn clean install -Dmaven.test.skiptrue执行更新命令。 mvn -U idea:idea

YOLO-World——S

文章目录 Abstract成果 MethodPre-training Formulation: Region-Text PairsModel ArchitectureYOLO DetectorText EncoderText Contrastive HeadTraining with Online VocabularyInference with Offline Vocabulary Re-parameterizable Vision-Language PANText-guided CSPLay…

string类——常用函数模拟(C++)

本篇中&#xff0c;将会详细的介绍 Cpp 中 string 的使用&#xff0c;以及 string 类常用函数的模拟实现。对于 string 的内置函数来说&#xff0c;存在很多很冗余的用法&#xff0c;很多函数都有很多种用法&#xff0c;本篇将会讲解常用内置函数的常用用法&#xff0c;模拟函数…

Pytest小技巧:高效获取自动化测试结果

自动化测试用例在执行完成后&#xff0c;我们想要很清楚的查看到测试用例的执行结果&#xff0c;我们可以通过Pytest中的Hooks来进行获取吗&#xff1f; 其中Pytest中存在多个Hooks的函数&#xff0c;小编今天先简单介绍其中一种&#xff0c;通过pytest_runtest_makereport 获…

若依vue中关于字典的使用

文章目录 字典管理页面列表点击某个字典类型展示具体字典数据修改某一条字典数据 字典的应用一般用于select多选框中代码实现根据字典Dict的value获取Label&#xff0c;类似于通过key获得value 源码解析 字典管理页面 列表 点击某个字典类型展示具体字典数据 修改某一条字典数…

04_UART串口发送数据

1.配置芯片&#xff0c;如果PA9,PA10的UART引脚被占用&#xff0c;会自动进行重映射 2.代码 int main(void) {uint8_t temp[]"test";/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*…

windows的jar包开机自启动【搬代码】

感觉最方便的就是放到启动项目里操作步骤 winR 输入&#xff1a;shell:startup回车或点击确定 3.将自己jar包右键创建快捷方式 4.然后放进去 5.重启电脑&#xff0c;浏览器输入网址&#xff0c;就可以看到重启成功了 另外一个就是放入.exe文件的快捷方式 首先&#xff0c;…

C语言洛谷题目分享(9)奇怪的电梯

目录 1.前言 2.题目&#xff1a;奇怪的电梯 1.题目描述 2.输入格式 3.输出格式 4.输入输出样例 5.说明 6.题解 3.小结 1.前言 哈喽大家好啊&#xff0c;前一段时间小编去备战蓝桥杯所以博客的更新就暂停了几天&#xff0c;今天继续为大家带来题解分享&#xff0c;希望大…

网络管理实验二、SNMP服务与常用的网管命令

1 常用的网管命令 1.1 网络状态监视命令 包括以下命令&#xff1a;Ipconfig、ping、nslookup、dig、host ipconfig 作用&#xff1a;用来显示本机所有网卡的基本信息&#xff08;IP、掩码、网关、工作状态&#xff09;&#xff1b;用法&#xff1a;ipconfig展示&#xff1a;…

Python的国际化和本地化【第162篇—国际化和本地化】

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 随着全球化的发展&#xff0c;多语言支持在软件开发中变得越来越重要。Python作为一种流行的…

软件架构静态演化

1.静态演化需求 软件架构静态演化的需求是广泛存在的&#xff0c;可以归结为两个方面。 &#xff08;1&#xff09;设计时演化需求。在架构开发和实现过程中对原有架构进行调整&#xff0c;保证软件实现与架构的一致性以及软件开发过程的顺利进行。 &#xff08;2&#xff09;运…

二期 1.3 Spring Cloud Alibaba微服务组件Nacos注册中心介绍

文章目录 一、注册中心有什么用?二、注册中心对比三、Nacos是什么?3.1 Nacos 基本概念3.2 Nacos 主要功能3.3 Nacos 优势一、注册中心有什么用? 谈起微服务架构,总会提到注册中心,它是微服务架构必不可少的组件之一,那么注册中心作用到底是什么? 话说微服务架构下 服务…

Qt---控件的基本属性

文章目录 enabled(控件可用状态)geometry(位置和尺寸)简单恶搞程序 windowIcon(顶层 widget 窗口图标)使用 qrc 机制 windowOpacity(窗口的不透明值)cursor(当鼠标悬停空间上的形状)自定义鼠标图标 toolTip(鼠标悬停时的提示)focusPolicy(控件获取焦点的策略)styleSheet(通过CS…

Navicat连接SQL server出现:[IM002] [Microsoft][ODBC 驱动程序管理器] 未发现数据源名称并且未指定默认驱动程序(0)

问题 解决方法 一 找到Navicat的安装路径&#xff0c;然后找到sqlncli_x64.msi文件并安装&#xff0c;安装成功后重启Navicat重新进行连接&#xff0c;看是否成功。 解决方法 二 如果方法一没有找到找到sqlncli_x64.msi 还是Navicat的安装路径&#xff0c;然后找到msodbcsql_64…

【网络编程】Linux网络内核结构以及分布剖析

hello &#xff01;大家好呀&#xff01; 欢迎大家来到我的网络编程系列之Linux网络内核结构以及分布剖析&#xff0c;在这篇文章中&#xff0c;你将会学习到在Linux内核中如何实现网络数据的输入和输出的&#xff0c;并且我会给出源码进行剖析&#xff0c;以及手绘UML图来帮助…

实现iOS App代码混淆

简介 在开发iOS应用程序时&#xff0c;保护代码安全是至关重要的。代码混淆是一种常用的技术&#xff0c;可以增加逆向工程的难度&#xff0c;防止他人对代码的篡改和盗用。本文将介绍如何实现iOS App代码混淆的步骤和操作方法。 整体流程 下面是实现iOS App代码混淆的整体流…