数据结构之AVL树

news2024/12/29 7:15:47

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


AVL树的概念

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

因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:

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

所以什么是平衡二叉树?

1.空树

2.或者是具有以下性质的二叉搜索树:
它的左右子树都是AVL树且左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1) 

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

AVL树结构的定义

那我们这里以KV模型的结构实现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;//balence factor

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

这里我们给结点增加一个_parent指针指向它的父亲结点,方便我们后续调整平衡, 带来方便的同时我们也需要去维护每个结点的_parent指针.

template<class K,class V>
class AVLTree
{
    typedef ALVTreeNode<K, V> Node;

public:
	//成员函数

private:
	Node* _root = nullptr;
};

AVL树的操作

插入

AVL树就是在二叉搜索树的基础上引入了平衡因子来控制树的相对平衡,因此AVL树也可以看成是二叉搜索树。所以插入的逻辑其实跟搜索二叉树是一样的,不同的地方在于平衡二叉树插入之后如果整棵二叉树或者其中某些子树不平衡了我们要对插入的结点进行调整使得它重新变的平衡.

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

        //链接父亲指针
		cur->_parent = parent;

		//平衡因子更新
        //..

		return true;
	}
}

平衡因子

什么是平衡因子?
一个结点的平衡因子就是它的左右子树的高度差, 一般是右子树减左子树的高度(我们这里的讲解也统一以右子树-左子树的高度作为平衡因子)。


为什么要更新平衡因子?

我们在AVL树中插入了一个新结点之后, 会不会影响到树中结点的平衡因子?

当然是会的!
因为一旦插入了新的结点,整棵树的高度或者某些子树的高度必然会发生变化,那树的高度发生变化,必然会影响与之关联的结点的平衡因子。


不更新平衡因子行不行?

不行, 为什么呢?
因为如果一棵二叉搜索树是AVL树,那么它必须满足任何一个结点的平衡因子都在[-1, 0, 1]这个范围内。而现在插入新结点会导致平衡因子变化, 那么更新之后, 某些结点的平衡因子可能就不在[-1, 0, 1]这个正常范围内了。那他就不是一棵AVL树了,所以我们才要更新平衡因子,以此来判断这个树还是否是一棵AVL树。

如果不是AVL树, 即有结点的平衡因子不在正常范围内了, 那这棵树的平衡就受到影响了, 那我们就需要对新插入的结点进行调整, 使他变回AVL树。如果插入之后平衡没有受到影响,就不需要调整了。

现在先实现插入新结点后如何更新平衡因子.


如何更新平衡因子?

首先插入一个新结点之后, 可能会影响到哪些结点的平衡因子?

最先影响的肯定是它的祖先, 因为新插入的结点在它祖先的子树上, 那它祖先的子树高度发生变化, 平衡因子必然也会发生变化, 但是会影响祖先的祖先吗?
不一定!可能只影响一部分。

这种情况只影响祖先的平衡因子:

这种情况就影响了全部的祖先的平衡因子:  

 观察可以发现平衡因子更新的规律:

因为平衡因子的计算是右子树高度-左子树高度, 所以, 于新结点的父亲来说:

        1.如果插入在了右子树, 那么父亲的平衡因子就要++

        2.如果插入在了左子树, 那么父亲的平衡因子就要- -

 这时候parent指针的作用就体现出来了:


 parent更新后,是否需要继续往上更新? 

首先parent肯定要更新, 因为插入之后它的子树的高度变了, 什么情况下parent更新完之后还要继续往上更新parent的祖先?取决于parent所在的这棵子树的高度有没有发生变化.

1.如果插入之后以parent根节点的这棵树的高度没有变化, parent的平衡因子更新之后为0那就不会影响parent祖先的平衡因子, 就不需要往上继续更新了.

因为更新之后为0的话,说明插入之前它的平衡因子为1或者-1, 然后我们在左边或者是右边插入了一个结点, 它的平衡因子就变成了0, 这棵树变平衡了, 高度就没有发生变化

 2.如果插入之后parent这棵子树的高度发生了变化,那parent的平衡因子更新完成后就需要继续往上更新,也就是如果parent的平衡因子更新之后为1或-1, 则parent这棵树的高度发生变化, 需要继续向上更新.

因为parent的平衡因子在更新之后变成了1或者-1说明它更新之前的平衡因子一定是0,说明他之前两边高度是平衡的, 而现在插入之后变为1或-1, 说明右边或者左边高了, 因此高度肯定是变化了, 那就要继续往上更新。

那可能是-2或者2加一减一之后变成-1或1吗?

不可能, 因为AVL树的平衡因子的范围都是在[-1, 0, 1]内的。

 3. parent的平衡因子更新之后为2或-2
如果是2或-2, 那已经不在平衡因子的正常范围内了, 那就说明当前parent所在的这棵子树已经不平衡(通常把这棵树叫做最小不平衡子树),那就不要再往上更新了, 就要去调整结点使这棵最小不平衡子树重变平衡。
怎么调整呢,?要分情况进行旋转, 使它的高度恢复到插入之前, 从而也就不需要再继续往上更新了。
 


平衡因子更新代码实现:

因为不知道要向上更新几次, 所以是一个循环, 循环什么时候结束?

它可能向上更新几次就出现平衡因子为2或-2的情况要开始调整了, 但是不排除可能根节点都更新完了, 整棵树还是平衡的, 不需要作调整

比如这种情况:

//平衡因子更新
while (parent)
{
	if (cur == parent->right)
		parent->_kv++;
	else if (cur == parent->left)
		parent->_kv--;
    //判断是否需要继续向上更新,需要就往上走(bf==1或-1)等待下次循环更新
    //如果不平衡了就进行处理(bf==2或-2),不需要处理不需要调整就break(bf==0)
	if (parent->_kv == 0)
	{
		break;
	}
	else if (parent->_kv == 1 || parent->_kv == -1)
	{
		cur = parent;
		parent = parent->_parent;
	}
	else if (parent->_kv == -2 || parent->_kv == 2)
	{
		//旋转调整
	}
}

接下来重点说一下对于不平衡的情况如何进行调整, 即AVL树的旋转 


AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点, 可能造成不平衡, 此时必须调整树的结构, 使之平衡化。根据节点插入位置的不同, AVL树的旋转分为四种:

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

什么样的情况要进行左单旋呢?

图里面的a,b,c是什么?
这里给的是一个抽象图, a、b、c分别代表三棵高度为h的AVL子树, 这里的h可以为任何整数值(所以h取不同的值(0,1,2,3,4...), 这里具体的情况是有很多种的, 但是针对这一类情况, 我们的处理是统一的),

举个例子:

假如h = 0, 情况只有1种,在60的右节点插入

h = 1, 情况有2种, 在c的左右结点都可以插入

h = 2, 情况有36种, h为2的二叉树形状有3种, a和b分别取3种就是9种, c只能是一颗h为2的满二叉树即平衡因子为0, 所以有4种插入情况, 总共为9×4=36种.

为什么c只能是一颗h为2的满二叉树即平衡因子为0的二叉树?

因为要保证c中插入后平衡因子要一路顺着祖先向上调整, 平衡因子为1或者-1的话一定是再c中插入后c就变成平衡无需向上调整平衡因子或者是c自己插入后本身就不平衡, c就变成了最小不平衡子树, 因此这里讨论的c都是平衡因子为0的.

h=3情况a和b只要高度为3即可,c不一定是满的二叉树,平衡因子为0且插入后因子能一直向上调整即可,情况更复杂, 所以可以看出这里的抽象图的意义, 所有的情况都被一种抽象图给概括了.

如何进行左单旋

现在30这个结点的平衡因子是不在正常范围内的, 这棵树是不平衡的且右边高,所以要对30这棵树进行左单旋,怎么左单旋呢?

相当于把30往左边向下旋转,所以叫左单旋。
进行了左单旋之后, 这棵树就重新变成AVL树, 达到平衡状态了,树的高度也降下去了。

降高度是一方面, 在使它变平衡的同时也保持了它依旧是一颗搜索二叉树, 因为AVL树就是平衡的搜索二叉树(旋转过程选择的孩子都是满足搜索树的大小关系的)。

代码:

,

void rotateL(Node* parent)// 旋转的时候传要旋转的子树的根结点即可
{
    //获取需要操作到的几个结点
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* parentParent = parent->_parent;

	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;//h=0时,subRL是空树,就不需要链接父结点了

	subR->_left = parent;
	parent->_parent = subR;

    //parent上面可能还有结点,我们这里旋转的可能是一整棵树,也可能是一棵树中的子树。
    //如果parent就是_root,更改_root,然后_parent置为空,因为根节点的_parent是nullptr
	if (_root == parent)
	{
		_root = subR;
		subR->_parent = nullptr;
	}
    //如果是子树的话,上面还有结点
    //用之前保存的parentParent进行链接
	else
	{
		if(parent == parentParent->_right)
			parentParent->_right = subR;
		else if (parent == parentParent->_left)
			parentParent->_left = subR;
		subR->_parent = parentParent;
	}
    //最后,旋转之后要更新一下平衡因子
	subR->_bf = parent->_bf = 0;
}

什么时候调用左单旋

那我们代码写好了,什么时候调用呢?

如果parent的平衡因子是2, subR(对应我们在更新平衡因子的那个循环里就是cur)的平衡因子是1,此时要进行的就是左单旋 


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

什么情况要进行右单旋?

和左单旋类似, 同样的我们这里讨论的情况是插入之后a的高度要发生变化, 且会影响到当前这棵树(当然它可以是一棵子树)根结点的平衡因子, 导致整棵树不平衡, 这时我们可以用右单旋解决。 

如何操作?

相当于把30往右边向上旋转,所以叫右单旋。
30的右子树比30大,比60小,所以可以做60的左子树, 然后60整棵树都比30大, 所以可以做30的右子树。这样这棵树就重新变平衡了,30成为了新的根结点。 

代码实现: 

void rotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* parentParent = parent->_parent;
    
    //这里换了一种思路,先把子结点链接起来
	subL->_right = parent;
	parent->_left = subLR;

    //然后链接父结点
	parent->_parent = subL;
	if (subLR)
		subLR->_parent = parent;

    //和左单旋一样,判断parent是不是根
	if (_root == parent)
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else
	{
		if (parentParent->_left == parent)
		{
			parentParent->_left = subL;
		}
		else if (parentParent->_right == parent)
		{
			parentParent->_right = subL;
		}
		subL->_parent = parentParent;
	}
    //调整平衡因子
	subL->_bf = parent->_bf = 0;
}

 什么时候调用右单旋?


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

 什么情况进行左右双旋

这里给的是在b插入, 在c插入当然也是左右双旋, 但是插入之后平衡因子的更新会有一些不同,后面会提到, 这还是抽象图, 我们来画几个具象图看一下.

和左单旋中分析的类似,h==0只有一种情况 

h==1有两种情况 

 h==2有36种情况:

 h==3有144*225种:

高度为3的二叉树有C44+C43+C42+C41共15种, b或c是平衡因子为0的h==2的二叉树所以只能是x,

当在b插入b就是x,c可以是xyz中的任意一共3*4,在c插入同理也是3*4种,总计(12*2)*(15*15)种.

所以能看出来抽象图是为了统一各种子情况 

如何进行左右双旋

首先对于这种情况,们如果只进行左或者右的单旋是解决不了问题的

那要进行双旋,怎么做呢?

上面已经说了针对这种情况要进行的是左右双旋,那顾名思义就是先进行一个左单旋(对根的左子树),再进行一个右单旋(对根)

 

将双旋变成单旋后再旋转, 即:先对30进行左单旋, 然后再对90进行右单旋, 旋转完成后再
考虑平衡因子的更新.第一步的左单旋相当于把它转化成右单旋的情况,然后右单旋即可.

我们能发现它就是把60推上去做根,然后60的左右子树分给30的右子树和90的左子树.

代码实现:

那左右双旋的代码的一部分可以直接复用左右单旋,但是左右双旋麻烦的地方在于平衡因子的调节,我们上面提到插入在b和c它们最后平衡因子更新不同

能看到插入在b和c旋转之后它们的平衡因子更新是不一样的,那如何判断在b插入还是在c插入呢?

插入之后60这个结点的平衡因子是不同的

那除此之外还有第三种情况,h为0的时候, 平衡因子的更新又有所不同:

所以,平衡因子的更新这里我们要分三种情况:

1. subLR->bf == 0

2. subLR->bf == 1

3. subLR->bf == -1

(subLR就是图上的60结点)

void rotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR= subL->_right;
	int bf = subLR->_bf;//先记录subLR的平衡因子,分情况进行平衡因子的更新
    
    //左右单旋
	rotateL(parent->_left);
	rotateR(parent);
    
    //分情况进行平衡因子的更新
	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 if (bf == -1)
    {
	    parent->_bf = 1;
	    subL->_bf = 0;
	    subLR->_bf = 0;
    }
	else
	{
		assert(false);
	}
}


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

插入到b这棵树上也可以, 和左右双旋一样, 高度h不同, 就会产生很多不同的情况,但我们可以统一处理 

和左右双旋一样,这样的情况只旋一次是不能达到平衡的,.所以第一次右单旋其实是把它变成左单旋的情况,然后再进行一次左单旋就平衡了。
最终的结果就相当于把60推上去做根,然后60的左右子树分别分给30的右子树和90的左子树。 

右左双旋代码实现 :

与左右双旋一样,平衡因子更新还是三种情况,通过插入之后subRL的平衡因子区分:

1.在c插入,subRL->_bf == 1

2. subRL->_bf == -1

3. subRL->_bf == 0

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

	rotateR(parent->_right);
	rotateL(parent);

	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 if(bf == -1)
	{
		parent->_bf = 0;
		subR->_bf = 1;
		subRL->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

总结

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

(1) pParent的平衡因子为2, 说明pParent的右子树高, 设pParent的右子树的根为SubR
        1.当SubR的平衡因子为1时, 执行左单旋
        2.当SubR的平衡因子为-1时, 执行右左双旋
(2) pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为SubL
        1.当SubL的平衡因子为-1是,执行右单旋
        2.当SubL的平衡因子为1时,执行左右双旋

 旋转完成后,原pParent为根的子树高度降低,已经平衡,不需要再向上更新平衡因子

//旋转
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 if (parent->_bf == -2 && cur->_bf == 1)
	{
		rotateLR(parent);
	}
	else
	{
		assert(false);
	}
	//旋转结束后,不需要再向上更新平衡因子了
	break;
}

AVL树的测试

 验证其为二叉搜索树

我们插入一些数据, 如果中序遍历可得到一个有序的序列, 就说明为二叉搜索树.

补充成员函数中序遍历:

 

 

void test2()
{
	int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	AVLTree<int, int> t;
	for (auto e : a)
	{
		t.Insert(make_pair(e, e));
	}
	t.InOrderPrint();
}

int main()
{
	test2();
	return 0;
}

 

 


验证其为平衡树 

如何验证它是否平衡呢?

我们可以去计算高度, 如果每一个结点左右子树的高度差的绝对值不超过1, 就证明它是平衡的。

为什么不用平衡因子判断呢?

首先, 不是所有的AVL树的实现里面都有平衡因子的, 只是我们这里采用了平衡因子, 这是AVL树的一种实现方法而已。其次,, 我们不敢保证我们自己写到代码计算出来的平衡因子一定是正确的。

所以, 我们来写一个通过高度差来判断是否平衡的函数:

bool IsBalance()
{
	return	_IsBalance(_root);
}

bool _IsBalance(Node* _root)
{
	if (_root == nullptr)
		return true;
	
	int left = _Height(_root->_left);
	int right = _Height(_root->_right);
	return abs(left - right) <= 1 
		&& _IsBalance(_root->_left) 
		&& _IsBalance(_root->_right);
}

int _Height(Node* _root)
{
	if (_root == nullptr)
		return 0;

	int left = _Height(_root->_left);
	int right = _Height(_root->_right);
	return left > right ? left + 1 : right + 1;
}
void test2()
{
	int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	AVLTree<int, int> t;
	for (auto e : a)
	{
		t.Insert(make_pair(e, e));
	}
	t.InOrderPrint();
	cout << endl;
	cout << t.IsBalance() << endl;
}

int main()
{
	test2();
	return 0;
}


判断平衡因子的更新是否正确 

计算一下高度差,看他和平衡因子相不相等就行了: 

 


大量随机数构建AVL树进行测试

上面的测试数据量比较小, 且不够随, 下面我们生成一些随机数来构建AVL树, 测试一下:

 

void test3()
{
	srand(time(nullptr));
	const int N = 1000;

	AVLTree<int, int> t;
	for (int i = 0;i<N;i++)
	{
		int x = rand();
		t.Insert(make_pair(x,x));
	}
	t.InOrderPrint();
	cout << endl;
	cout << t.IsBalance() << endl;
}

int main()
{
	test3();
	return 0;
}


AVL树查找

AVL树的查找那和搜索二叉树是一样的,可以看之前搜索二叉树的文章。


AVL树的删除(了解)

AVL树的删除操作了解即可

AVL树的删除操作主要分为以下几个步骤:

删除操作首先还是按照搜索树的规则先找到这个结点, 然后又要像二叉搜索树的删除一样, 分为三种情况: 左子树为空, 右子树为空, 左右都不为空

AVL树删除还要注意更新平衡因子:

和插入相反, 删除右结点的话, 父结点的平衡因子--,  删除左结点的话, 父结点的平衡因子++, 然后判断需不需要向上继续更新平衡因子, 插入的时候如果父结点平衡因子变为1或-1就要向上更新, 因为此时子树的高度变了, 一路上祖先的平衡因子就要跟着变化, 现在删除相反, 父结点平衡因子变为1或-1子树的高度并没有变化, 因为是从0变化过来的, 子树的高度没有变, 不会影响上一层, 就结束了.

而结点平衡因子变为0, 就代表子树的高度变化, 比如1变为0的话, 就代表高的那棵树被删除了, 这棵树的高度就变了, 需要向上更新平衡因子了, 直至到达根节点或不需要进一步调整为止。 


上面都是只更新平衡因子的情况, 如果更新后平衡因子为2或-2, 需要旋转:
 

 

左右双旋就不演示了


AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树, 其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度, 即 O(logN)

但是如果要对AVL树做一些结构修改的操作,性能非常低下。
比如: 插入时要维护其绝对平衡, 旋转的次数比较多, 更差的是在删除时, 有可能一直要让旋转持续到根的位置
因此: 如果需要一种查询高效且有序的数据结构, 而且数据的个数为静态的(即不会改变), 可以考虑AVL树, 但一个结构经常修改, 就不太适合。

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

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

相关文章

【 Docker: 数据卷挂载】

背景 Docker只提供了容器运行的必备依赖&#xff0c;但是一些编辑等操作的依赖是不支持的&#xff0c;如vi操作容器内部文件、将静态资源拷贝到容器内来等。 docker pull nginx docker run -d -p 81:80 --namemynginx -v D:/docker/nginx/www:/usr/share/nginx/www -v D:/dock…

性能测试资源监控宝藏工具:iDB Dashboard

iDB Dashboard 是 TiDB 自 4.0 版本起提供的图形化界面&#xff0c;可用于监控及诊断 TiDB 集群。TiDB Dashboard 内置于 TiDB 的 PD 组件中&#xff0c;无需独立部署&#xff1a; https://docs.pingcap.com/zh/tidb/v4.0/dashboard-intro

【GEE】8、Google 地球引擎中的时间序列分析【时间序列】

1简介 在本模块中&#xff0c;我们将讨论以下概念&#xff1a; 处理海洋的遥感图像。 从图像时间序列创建视频。 GEE 中的时间序列分析。 向图形用户界面添加基本元素。 2背景 深水地平线漏油事件被认为是有史以来最大的海上意外漏油事件。该井释放了超过 490 万桶石油&am…

实用知识(工作中常用)

mybatis-plus联表查询 pom.xml坐标 <!-- mybatis-plus-join --> <dependency><groupId>com.github.yulichang</groupId><artifactId>mybatis-plus-join</artifactId><version>1.2.4</version> </dependency>使用步骤&…

虚拟环境中使用的Python不是当前虚拟环境的,解决方法

every blog every motto: You can do more than you think. https://blog.csdn.net/weixin_39190382?typeblog 0. 前言 在虚拟环境中使用的python和pip不是虚拟环境的pip安装不到当前的虚拟环境中…等 解决方法 1. 解决办法 打开配置文件 vim ~/.bashrc把如下代码注释即…

Gradle笔记 三 Gradle的项目周期和settings 文件

文章目录 项目的生命周期settings 文件 项目的生命周期 Gradle 项目的生命周期分为三大阶段: Initialization -> Configuration -> Execution. 每个阶段都有自己的职责,具体如下图所示: ● Initialization 阶段主要目的是初始化构建, 它又分为两个子过程,一个是执行 I…

刚柔相济铸伟业 ——访湖南顺新金属制品科技有限公司董事长张顺新

时代在变&#xff0c;唯初心不改。 精致、谦虚、谨慎、儒雅、温和——他就是张顺新&#xff0c;湖南顺新金属制品科技有限公司、湖南顺新供应链管理有限公司董事长&#xff0c;民建长沙市委常委&#xff0c;民建湖南省环资委副主任&#xff0c;省、市民建企联会常务副会长&…

加速度jsudo:小企业会遇到哪些瓶颈期?

什么是瓶颈期&#xff1f;瓶颈期&#xff0c;就是你无论怎么努力&#xff0c;成绩都是上不去&#xff0c;还是停留在原地&#xff1b;而自己表现的还是很匆忙&#xff0c;却不知道如何下手&#xff1f;就像水桶效益一样&#xff0c;水桶的木板高度层次不齐&#xff0c;像极了自…

如何使用Leangoo领歌管理Sprint Backlog

什么是Sprint Backlog&#xff1f; Sprint Backlog是Scrum的主要工件之一。在Scrum中&#xff0c;团队按照迭代的方式工作&#xff0c;每个迭代称为一个Sprint。在Sprint开始之前&#xff0c;PO会准备好产品Backlog&#xff0c;准备好的产品Backlog应该是经过梳理、估算和优先…

STM32笔记—定时器

目录 一、TIM简介 二、基本定时器&#xff08;TIM6和TIM7&#xff09; 1. TIM6和TIM7简介 2. TIM6和TIM7的主要特性 3. TIM6和TIM7的功能 3.1 时基单元 3.2 计数模式 3.3 时钟源 三、通用定时器 1. TIMx(2、3、4、5)简介 2. TIMx主要功能 3. 时钟选择 4. 影子寄存器…

湖泊河道水质蓝藻浮漂监测案例

湖泊河道水质蓝藻浮漂监测案例 湖泊和河道的浮漂水质监测和蓝藻治理是现代城市环境管理的重要内容之一。随着人类社会的发展&#xff0c;水污染问题越来越严重&#xff0c;而湖泊和河道作为水资源的重要组成部分&#xff0c;其水质的变化和污染程度直接关系到人们的生命安全和…

qt+opengl 三维坐标系(三)

文章目录 前言一、深度测和投影矩阵、观察矩阵二、绘制坐标系三、添加箭头四、添加文字五、放大缩小六、旋转七、移动八、完整代码总结 前言 效果如图 一、深度测和投影矩阵、观察矩阵 这部分不明白&#xff0c;网上查的都是这个步骤&#xff0c;用起来也没问题。 void MOp…

avue 表单自定义标题无效问题(avue 表单自定义标题,当prop含有大写字母时失效)

问题描述&#xff1a; avue 表单自定义标题&#xff0c;官方文档可以生效&#xff0c;项目中不生效。多方排查发现&#xff0c;当prop含有大写字母时失效。 代码展示&#xff1a; <avue-form v-model"form" :option"option"><template #classN…

腾讯待办关停后,导出的ics文件怎么导入iPhone手机日历?

ics文件是一种通用日历格式保存的日历文件&#xff0c;不少提醒待办类软件支持导出ics文件&#xff0c;诸如大家所熟知的腾讯待办就支持导出成ics文件&#xff0c;腾讯待办的主要功能一待办事项和日程管理位置&#xff0c;设置好时间提醒后&#xff0c;可通过公众号端口弹出提醒…

c语言练习第10周(1~5)

根据公式求和 输入样例20输出样例 534.188884 #include<stdio.h> #include<math.h> int main() {int i,n;scanf("%d", &n);double s 0,t0;for (i 1; i < n; i) {t t sqrt(i);s s t;}printf("%.6lf", s);return 0; } 第一行输入…

STA——绪论

一、概述 静态时序分析&#xff08;简称STA&#xff09;是用来验证数字设计时序的技术之一&#xff0c;另外一种验证时序的方法是时序仿真&#xff0c;时序仿真可以同时验证功能和时序。“时序分析”这个术语就是用来指代“静态时序分析“或”时序仿真“这两种方法之一&#xf…

uniapp H5预览PDF支持手势缩放、分页、添加水印、懒加载、PDF下载

效果预览 项目说明 uniapp vue2 node&#xff1a;v14.18.3 npm&#xff1a; 6.14.15 安装pdfh5.js插件 pdfh5 - npm (npmjs.com)pdfh5.js 基于pdf.js和jQuery pdfh5 - npm (npmjs.com) npm install pdfh5 由于我安装最新的pdfh5.js后运行时报错 所以我选择降低版本,可能是node…

太细了:美团一面连环夺命20问,搞定就60W起

说在前面 在40岁老架构师尼恩的&#xff08;50&#xff09;读者社群中&#xff0c;经常有小伙伴&#xff0c;需要面试美团、京东、阿里、 百度、头条等大厂。 下面是一个小伙伴成功拿到通过了美团一面面试&#xff0c;现在把面试真题和参考答案收入咱们的宝典。 通过美团一面…

获取代码中所有的中文并排除注释代码

在给项目添加国际化适配时&#xff0c;需要吧代码中的中文一个一个替换成 $t(xxx.xxx) 这种代码&#xff0c;但是一个一个找中文比较麻烦&#xff0c;而且容易遗漏&#xff0c;于是就有了下面的代码&#xff0c;可以快速的帮我们找出对应文件中的所有中文&#xff0c;并且把中文…

链表OJ——环形链表初阶与进阶

呀哈喽&#xff0c;我是结衣。 环形链表1 描述 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数…