数据结构进阶:AVL树与红黑树

news2025/5/21 8:40:18

目录

前言

AVL树

定义

结构

插入

AVL树插入的大致过程

更新平衡因子

旋转 

右单旋

左单旋

左右双旋

右左双旋

实现 

红黑树

定义

性质

结构

插入

实现

总结 


前言

在学习了二叉搜索树之后,我们了解到其有个致命缺陷——当树的形状呈现出一边倒的情况时,搜索的效率会退化,由O(logN)退化到O(N)。

为了解决这个问题,前人发明了AVL树红黑树用以解决这个问题,

二者本质就是时刻注意将树调整为平衡或近似平衡的状态,以保证二叉的结构,从而确保其功能的效率。区别就在于平衡的实现逻辑不同。

AVL树

定义

AVL树是一棵高度平衡的二叉树,要么是一棵空树,

要么是具备以下性质的二叉搜索树:左右子树都是AVL树,左右子树高度差的绝对值<=1

为了方便计算高度差,我们在实现时引入了平衡因子(balance factor),用以记录左右子树的高度差。平衡因子 = 右子树高度 - 左子树高度。所以平衡因子的值就只能为0 / 1 / -1;分别表示平衡、右子树高一层、左子树高一层。当然平衡因子不是必须的,但有了它我们能够更好的观察AVL树。

为什么不要求高度差为0呢?显然,假如只有两个结点,就不可能存在此情况。

AVL树因其完全平衡性,使得其效率能够保持在O(logN),这就是控制平衡的回报,往下我们将学习插入时如何控制平衡。

结构

AVL树是BSTree(二叉搜索树)的改进,所以大体结构与前文是一致的,只不过需要在结点中维护一个平衡因子的变量,可直接参考实现即可。

插入

AVL树插入的大致过程

1.根据二叉搜索树的插入方法插入结点;

2.更新平衡因子;(新增结点影响祖先结点的高度,因此要从下往上更新到根的平衡因子。但未必会走到根,在中间就可能结束,具体情况具体分析)

2.更新过程中检查平衡因子是否有异常,有异常就要进行旋转,否则插入结束。(旋转的本质就是调整平衡,让较高的子树高度下降一层)

更新平衡因子

平衡因子 = 右子树高度 - 左子树高度(即在parent的左子树插入就-1,右子树插入就+1)。

更新后的结果决定是否继续向上更新。

【1】更新后为0,意味着原来是-1->0或1->0,即原来是一边高一边低,并且是在低的位置插入结点,对于parent而言子树高度不变,更新就结束了。

【2】更新后结果为1或-1,则意味着0->1或0->-1,即在parent两边高度相同的情况下插入,插入后仍符合要求,但parent的高度相当于增加1,会影响其祖先的左右子树高度差,于是需要继续向上更新。

【3】更新后结果为2或-2,则意味着1->2或-1->-2,即原来是一边高一边低,却在高的位置插入结点,此时parent的左右子树高度差不符合要求,要进行旋转调节平衡。旋转的本质是通过降低较高子树的一层高度而使左右高度差回到插入前的平衡状态,这样相当于parent子树高度不变,不会影响祖先,所以旋转后不需要再向上更新,可以直接结束。

【4】在【2】的情况不断向上更新直到根,根为+-1就结束了,为+-2则要旋转。

【2】【3】情况结合。

更新到根。 

更新到中间位置结束。 

旋转 

旋转既要保持原来的结构,还要降低被旋树的高度。

但到底要怎么旋?还需分具体情况而论,共有四种情况:左/右单旋,左右/右左双旋。

插入后需要旋转的位置总共就4个,先分出两种大情况:要么在左子树,要么在右子树。

最后就可分为左子树的左,左子树的右,右子树的左,右子树的右。

看起来好像是笔者在自嗨,这总结的情况有点太抽象了。下面用图来展示。

当然这还是抽象图,但至少能够直观看出4种情况了。(如若还是看不清,可以假设只有两个结点,把子树abc当作空结点来看)

具体怎么操作?君且安坐,听笔者慢慢道来。 

右单旋

右单旋本质是以parent为根的右树往下压一层。

右单旋一共就三步,先以10为parent,5为subL,先把subL的右孩子subLR给parent的左,

                                再让parent作为subL的右孩子,

                                最后调整subL与parent->parent的关系。

本例较为简单,parent为根,根的父亲为空,调整步骤简单未在图中显示。

上述步骤在写代码时仍有许多细节问题,请读者自行探索。

左单旋

左单旋本质是以parent为根的左树往下压一层。

左单旋一共就三步,先以10为parent,5为subR,先把subR的右孩子subRL给parent的左,

                                再让parent作为subR的右孩子,

                                最后调整subR与parent->parent的关系。

本例较为简单,parent为根,根的父亲为空,调整步骤简单未在图中显示。

上述步骤在写代码时仍有许多细节问题,请读者自行探索。

左右双旋

双旋的本质是把subLR推举成根,让subL和parent分别做其左右孩子,然后自己的孩子也分别交给他们。(这里的subLR是8)

步骤:先左单旋subL,再右单旋parent。

无论新结点插入在e或f,最终e或f都要交给别人的,不影响旋转逻辑。

右左双旋

与左右双旋同理,

先右单旋subR,再左单旋parent

实现 

#pragma once
#include <iostream>
#include <assert.h>
using namespace std;

template<class K, class V>
struct AVLTree_Node
{
	typedef AVLTree_Node<K, V> Node;
	pair<K, V> _kv;
	Node* _left;
	Node* _right;
	Node* _parent;
	int _bf;

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

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

public:
	bool insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		Node* cur = _root;
		Node* parent = cur;

		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->_left = cur;
			cur->_parent = parent;
		}
		else if (kv.first > parent->_kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}

		parent = cur->_parent;
		while (parent)
		{
			if (parent->_left == cur)
				parent->_bf--;
			else if (parent->_right == cur)
				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 && cur->_bf == -1)
				{
					RotateR(parent);
					return true;
				}
				else if (parent->_bf == 2 && cur->_bf == 1)
				{
					RotateL(parent);
					return true;
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
					return true;
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRL(parent);
					return true;
				}
			}
			else
			{
				assert(false);
			}

		}
		return true;
	}
	
	bool find(const K& k)
	{
		Node* cur = _root;

		while (cur)
		{
			if (k < cur->_kv.first)
			{
				cur = cur->_left;
			}
			else if (k > cur->_kv.first)
			{
				cur = cur->_right;
			}
			else
			{
				return true;
			}
		}
		
		return false;
	}
	
	void Inorder()
	{
		_inorder(_root);
	}
	int Height()
	{
		return _Height(_root);
	}
	bool IsBalanceTree()
	{
		return _IsBalanceTree(_root);
	}
private:
	
	bool _IsBalanceTree(Node* root)
	{
		// 空树也是AVL树
		if (nullptr == root)
			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)
		{
			cout << root->_kv.first << "高度差异常" << endl;
			return false;
		} 
		if(root->_bf != diff)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		} 
		// pRoot的左和右如果都是AVL树,则该树⼀定是AVL树
		return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
	}
	void _inorder(Node* root)
	{
		if (root == nullptr)
			return;
		_inorder(root->_left);
		cout << root->_kv.first<<" ";
		_inorder(root->_right);
	}
	

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

		int left_height = _Height(root->_left);
		int right_height = _Height(root->_right);

		return left_height > right_height ? left_height + 1 : right_height + 1;
	}

	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		if(subLR)
			subLR->_parent = parent;

		Node* pp = parent->_parent;
		subL->_right = parent;
		parent->_parent = subL;
		if (pp == nullptr)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (pp->_left == parent)
			{
				pp->_left = subL;
			}
			else if (pp->_right == parent)
			{
				pp->_right = subL;
			}

			subL->_parent = pp;
		}
		parent->_bf = subL->_bf = 0;
	}
	void RotateL(Node* parent)
	{

		Node* subR = parent->_right;
		Node* subRL = subR->_left;

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

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

		parent->_bf = subR->_bf = 0;
	}

	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;

		RotateL(subL);
		RotateR(parent);

		if (bf == 0)
		{
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			parent->_bf = 0;
			subLR->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		RotateR(subR);
		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);
		}
	}
private:
	Node* _root = nullptr;
};

红黑树

红黑树是一棵更抽象一点的二叉搜索树,它既没有AVL树那么直观,平衡也不如AVL树那么完美,可是我们使用的map、set等容器底层都是用红黑树实现的。

这就很奇怪了,接下来便让我们一探究竟。

定义

红黑树是一棵空树或是一颗满足以下原则的二叉搜索树:

【1】结点不是红色就是黑色

【2】根节点必须为黑色

【3】红色结点的孩子必须是黑色结点或空,即红色不能连续

【4】从任意结点出发,到达其所有NULL结点的简单路径上,黑色结点数量相同

性质

上面的条件决定了红黑树的性质:

【1】最长路径的长度 <= 最短路径的长度的2倍

先根据[原则4]极端假设,最短路径有bh个黑色结点

再根据[原则2][原则3]可知,红色不能连续,极端场景下,最长路径由一黑一红间隔构成,那么最长路径上的结点个数就为2*bh

最后综合所有原则,理论上最短路径皆为黑色,最长路径一黑一红,不是每棵树都能有的极端情况。假设任意路径上的结点个数为x,则bh <= x <= 2*bh

(假设极端情况)

【2】插入时只能插入红色结点

假设插入的是黑色,那么根据[原则4],任意结点出发到其所有null的路径上,黑色结点数量相同。在某一支单独插入结点,会影响到各支上的黑色结点个数相等的情况,难以调整。

而反之插入红色结点,不违反[原则4],但可能违反[原则3]红色结点不能连续。但此时,并不会影响到其他支,只需要在本支上调整颜色即可。

为了不大动干戈,调整所有分支,因此不能插入黑色结点,只能插入影响规模小,易于调整的红色结点。而且若在黑色结点下插入,符合任一原则,不需要进行任何调整,也能提高效率。

【3】效率

已知最长路径<=最短路径的2倍,那么如果N为结点总个数,最短路径的高度为h,最长路径的高度为2*h,则满足2^h - 1 <= N <= 2^2h - 1。可推出 h = logN,最坏情况走最长路径也只为2*logN,所以时间复杂度还是O(logN)。

红⿊树的表达相对AVL树要抽象⼀些,AVL树通过⾼度差直观的控制了平衡。红⿊树通过4条规则的颜⾊约束,间接的实现了近似平衡,但二者效率仍在同⼀档次,相对⽽⾔,插⼊相同数量的结点,红⿊树的旋转次数是更少的,因为他对平衡的控制没那么严格。而旋转次数少了就使得效率上优于AVL树,多往下走几层并不会显著影响其效率。所以,也就回答了刚开始的疑问,set、map当然是用更高效的红黑树实现了。
 

结构

结点需要新增一个记录颜色的成员,可以用枚举来实现。

详情见实现。

插入

红黑树虽然因规则多,看起来抽象,但理解之后会发现,其插入确实比AVL树方便。

一共就分为两大种情况,红插黑,红插红。(只插入红色结点,上文有解释)

红插黑是最简单的情况,无需任何处理。

红插红就稍微麻烦点,但大体也只有三种情况。不过在了解这三种情况之前,还需要知道一件事情,——此时,插入的结点记作child, 被插入的记作parent,那么已知child、parent都为红,可以知道parent->_parent即grandparent必为黑(或空)因为插入前树完好,应符合原则。既然child,parent,grandparent的颜色都是确定的,现在唯一的未知量就是uncle了(grandparent的另一个孩子)。

uncle分为不存在,存在黑色,存在红色三种情况。(实际上uncle不存在和存在且为黑是可以合并的一种情况。)

【1】uncle存在且为红:变色

将p和u由红变黑,g变红。这样既能确保红黑相间,又能保持两分支黑色结点数相同。

但有个问题是,g变红后,g的parent也为红(原g为黑),这样又红红连续了,因此还要继续向上变色,直到g的parent为黑或是为根结点时停下。

注意这里插在p的左右是无所谓的。

【2】uncle不存在或存在且为黑:变色+旋转(旋转的转法和AVL树的转法是一致的,可以直接copy。)

(1)变色+单旋

在p外侧插入(只显示了p的新孩子,若有另一个孩子必为黑,按单旋规则给g没问题)

调整方法:单旋g,p变黑,g变红,结束调整。

 (2)变色+双旋

在p的内侧插入就要双旋,把c推举成根,并把c变黑,g变红,就结束调整了。

总结一下,就这两种大情况,理解后其实是要比AVL树还更好理解的(滑稽) 

实现

#pragma once
#include<iostream>

using namespace std;

enum Color{
	Red,
	Black
};

template<class K,class V>
struct RBTree_Node
{
	typedef RBTree_Node Node;

	RBTree_Node(const pair<K,V>& kv)
		:_kv(kv), _parent(nullptr), _left(nullptr), _right(nullptr),_col(Red)
	{}

	pair<K, V> _kv;
	Node* _parent;
	Node* _left;
	Node* _right;
	Color _col;
};

//---------------------------------------------------------------------------------------

template<class K,class V>
class RBTree
{
	typedef RBTree_Node<K,V> Node;

public:

	bool insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = Black;
			return true;
		}

		Node* cur = _root;
		Node* parent = cur;
		while (cur)
		{
			if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}

		Node* node = new Node(kv);
		node->_col = Red;
		if (kv.first < parent->_kv.first)
		{
			parent->_left = node;
		}
		else
		{
			parent->_right = node;
		}
		node->_parent = parent;

		while (parent && parent->_col == Red)
		{
			Node* grandparent = parent->_parent;
			if (parent && parent == grandparent->_left)
			{
				Node* uncle = grandparent->_right;
				if (uncle && uncle->_col == Red)
				{
					parent->_col = uncle->_col = Black;
					if(grandparent!=_root)
						grandparent->_col = Red;
					cur = grandparent;
					parent = cur->_parent;
				}
				else if (uncle == nullptr || uncle->_col == Black)
				{
					if (cur == parent->_left)
					{
						RotateR(grandparent);
						parent->_col = Black;
						grandparent->_col = Red;
					}
					else if (cur == parent->_right)
					{
						RotateL(parent);
						RotateR(grandparent);
						cur->_col = Black;
						grandparent->_col = Red;
					}
					else
					{
						return false;
					}
					break;
				}
			}
			else if (parent && parent == grandparent->_right)
			{
				Node* uncle = grandparent->_left;
				if (uncle && uncle->_col == Red)
				{
					parent->_col = uncle->_col = Black;
					grandparent->_col = Red;
					cur = grandparent;
					parent = cur->_parent;
				}
				else if (uncle == nullptr || uncle->_col == Black)
				{
					if (cur == parent->_right)
					{
						RotateL(grandparent);
						parent->_col = Black;
						grandparent->_col = Red;
					}
					else if (cur == parent->_left)
					{
						RotateR(parent);
						RotateL(grandparent);
						cur->_col = Black;
						grandparent->_col = Red;
					}
					else
					{
						return false;
					}
					break;
				}
			}
			else
			{
				return false;
			}
		}

		return true;
	}

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

	void Inorder()
	{
		_Inorder(_root);
	}

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

	bool isRBTree()
	{
		if (_root == nullptr)
		{
			return true;
		}
		if (_root->_col == Red)
		{
			return Red;
		}

		int refBN = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == Black)
			{
				refBN++;
			}
			cur = cur->_left;
		}

		return check(_root, 0, refBN);
	}
private:

	bool check(Node* root, int curBN, int refBN)
	{
		if (root == nullptr)
		{
			if (curBN != refBN)
			{
				cout << "存在路径黑结点数异常" << endl;
				return false;
			}

			return true;
		}

		if (root->_col == Red && root->_parent && root->_parent->_col == Red)
		{
			cout << root->_kv.first << "存在连续红结点" << endl;
			return false;
		}

		if (root->_col == Black)
			curBN++;

		return check(root->_left, curBN, refBN)
			&& check(root->_right, curBN, refBN);
	}

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

		int lh = _Height(root->_left);
		int rh = _Height(root->_right);

		return lh > rh ? lh + 1 : rh + 1;
	}

	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;
		_Inorder(root->_left);
		cout << root->_kv.second<<" ";
		_Inorder(root->_right);
	}

	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		Node* pp = parent->_parent;
		subL->_right = parent;
		parent->_parent = subL;
		if (pp == nullptr)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (pp->_left == parent)
			{
				pp->_left = subL;
			}
			else if (pp->_right == parent)
			{
				pp->_right = subL;
			}

			subL->_parent = pp;
		}
	}
	void RotateL(Node* parent)
	{

		Node* subR = parent->_right;
		Node* subRL = subR->_left;

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

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

	}

	
private:
	Node* _root = nullptr;
};

总结 

AVL树比较直观,红黑树比较抽象。但二者都是能够被理解的。

重点在于不断去模拟二者不同的控制平衡的逻辑,当然也考察了二叉树的知识。

二者都涉及了旋转,怎么旋是好理解的,可实现代码时还有诸多细节,算是很好的锻炼。

不要求手撕,但其本质还是要熟悉的。

(附:未涉及删除操作,有兴趣可以参考相关数据结构教材)

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

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

相关文章

基于Spring Boot + Vue的教师工作量管理系统设计与实现

一、项目简介 随着高校信息化管理的发展&#xff0c;教师工作量管理成为教务系统中不可或缺的一部分。为此&#xff0c;我们设计并开发了一个基于 Spring Boot Vue 的教师工作量管理系统&#xff0c;系统结构清晰&#xff0c;功能完备&#xff0c;支持管理员和教师两个角色。…

完善网络安全等级保护,企业需注意:

在数字化转型加速的当下&#xff0c;网络安全成为企业发展的基石。网络安全等级保护作为保障网络安全的重要举措&#xff0c;企业必须高度重视并积极落实。以下要点&#xff0c;企业在完善网络安全等级保护工作中需格外关注&#xff1a; 一、准确开展定级备案 企业首先要依据相…

Trae 04.22版本深度解析:Agent能力升级与MCP市场对复杂任务执行的革新

我正在参加Trae「超级体验官」创意实践征文&#xff0c;本文所使用的 Trae 免费下载链接&#xff1a;Trae - AI 原生 IDE 目录 引言 一、Trae 04.22版本概览 二、统一对话体验的深度整合 2.1 Chat与Builder面板合并 2.2 统一对话的优势 三、上下文能力的显著增强 3.1 W…

OceanBase 开发者大会:详解 Data × AI 战略,数据库一体化架构再升级

OceanBase 2025 开发者大会与5月17日在广州举行。这是继 4 月底 OceanBase CEO 杨冰宣布公司全面进入AI 时代后的首场技术盛会。会上&#xff0c;OceanBase CTO 杨传辉系统性地阐述了公司的 DataAI 战略&#xff0c;并发布了三大产品&#xff1a;PowerRAG、共享存储&#xff0c…

ubuntu环境下 基于Python 打包的 批量命令行可视化操作工具 GUI

文章目录 一.需求&#xff1a;二.原理支撑&#xff1a;三.简单Demo四.封装成GUI1.依赖库2.代码 五.打包成可执行文件六.命令行的配置七.运行效果 一.需求&#xff1a; 作为测试工程师&#xff0c;为了到现场高效的调试&#xff0c;部署工作&#xff0c;需要一个可视化的工具&a…

谷歌宣布推出 Android 的新安全功能,以防止诈骗和盗窃

在上周二的 Android Show 上&#xff0c;也就是Google I/O 开发者大会之前&#xff0c;谷歌宣布了 Android 的全新安全和隐私功能。这些新功能包括对通话、屏幕共享、消息、设备访问和系统级权限的全新保护。谷歌希望通过这些功能保护用户免遭诈骗&#xff0c;在设备被盗或被攻…

Qt/C++编写音视频实时通话程序/画中画/设备热插拔/支持本地摄像头和桌面

一、前言 近期有客户提需求&#xff0c;需要在嵌入式板子上和电脑之间音视频通话&#xff0c;要求用Qt开发&#xff0c;可以用第三方的编解码组件&#xff0c;能少用就尽量少用&#xff0c;以便后期移植起来方便。如果换成5年前的知识储备&#xff0c;估计会采用纯网络通信收发…

Android trace presentFence屏幕显示的帧

Android trace presentFence屏幕显示的帧 presentFence &#xff1a;当帧成功显示到屏幕时&#xff0c;present fence就会signal。 FrameMissed/GpuFrameMissed/HwcFrameMissed表示上一次合成的结果&#xff0c;当SurfaceFlinger合成后显示到屏幕上&#xff0c;present fence就…

c++ 类的语法4

测试析构函数、虚函数、纯虚函数&#xff1a; void testClass5() {class Parent {public:Parent(int x) { cout << "Parent构造: " << x << endl; }~Parent() {cout << "调用Parent析构函数" << endl;}virtual string toSt…

NMOS和PMOS的区别

1 区分NMOS和PMOS&#xff1a;衬底箭头指向G级的是NMOS&#xff0c;衬底箭头背向G级的是PMOS 2 区分D和S级&#xff1a;针对NMOS&#xff0c;体二极管的正方向为S级&#xff1b;针对PMOS&#xff0c;体二极管正的方向为D级 3 区分电流方向&#xff1a;针对NMOS&#xff0c;电…

java云原生实战之graalvm 环境安装

windows环境安装 在Windows环境下安装GraalVM并启用原生镜像功能时&#xff0c;需要Visual Studio的组件支持。具体要点如下&#xff1a; 核心依赖&#xff1a; 需要安装Visual Studio 2022或更新版本&#xff0c;并确保勾选以下组件&#xff1a; "使用C的桌面开发"…

2025年电工杯新规发布-近三年题目以及命题趋势

电工杯将于2025.5.23 周五早八正式开赛&#xff0c;该竞赛作为上半年度竞赛规模最大的竞赛&#xff0c;因免报名费、一级学会承办等因素&#xff0c;被众多高校认可。本文将在从2025年竞赛新规、历史赛题选题分析、近年优秀论文分享、竞赛模板分析等进行电工杯备赛&#xff0c;…

替换word中的excel

PostMapping("/make/report/target/performance/first") public AjaxResult makeTargetReportFirst(RequestBody MakeReportDTO makeReportDTO) {Map<String, String> textReplaceMap new HashMap<>();// 替换日期LocalDateTime nowData LocalDateTime…

大模型服务如何实现高并发与低延迟

写在前面 大型语言模型(LLM)正以前所未有的速度渗透到各行各业,从智能客服、内容创作到代码生成、企业知识库,其应用场景日益丰富。然而,将这些强大的 AI 能力转化为稳定、高效、可大规模应用的服务,却面临着巨大的挑战,其中高并发处理能力和低响应延迟是衡量服务质量的…

OBS Studio:windows免费开源的直播与录屏软件

OBS Studio是一款免费、开源且跨平台的直播与录屏软件。其支持 Windows、macOS 和 Linux。OBS适用于&#xff0c;有直播需求的人群或录屏需求的人群。 Stars 数64,323Forks 数8413 主要特点 推流&#xff1a;OBS Studio 支持将视频实时推流至多个平台&#xff0c;如 YouTube、…

经典面试题:TCP 三次握手、四次挥手详解

在网络通信的复杂架构里&#xff0c;“三次握手”与“四次挥手”仿若一座无形的桥梁&#xff0c;它们是连接客户端与服务器的关键纽带。这座“桥梁”不仅确保了连接的稳固建立&#xff0c;还保障了连接的有序结束&#xff0c;使得网络世界中的信息能够顺畅、准确地流动。 在面…

高光谱数据处理技术相关

一、Savitzky-Golay(SG)平滑 1. 基本概念 Savitzky-Golay(SG)平滑是一种基于局部多项式拟合的卷积算法,主要用于信号处理(如光谱、色谱数据)的去噪和平滑。其核心思想是通过滑动窗口内的多项式拟合来保留信号的原始特征(如峰形、宽度),同时抑制高频噪声。 2. 技术原…

机器视觉的PVC卷对卷丝印应用

在现代工业制造领域&#xff0c;PVC卷对卷丝印工艺凭借其高效、灵活的特点&#xff0c;广泛应用于广告制作、包装印刷、电子产品装饰等多个行业。然而&#xff0c;在高速连续的丝印过程中&#xff0c;如何确保印刷图案的精准定位、色彩一致性以及质量稳定性&#xff0c;一直是困…

LabVIEW数据库使用说明

介绍LabVIEW如何在数据库中插入记录以及执行 SQL 查询&#xff0c;适用于对数据库进行数据管理和操作的场景。借助 Database Connectivity Toolkit&#xff0c;可便捷地与指定数据库交互。 各 VI 功能详述 左侧 VI 功能概述&#xff1a;实现向数据库表中插入数据的操作。当输入…

25考研经验贴(11408)

声明&#xff1a;以下内容都仅代表个人观点 数学一&#xff08;130&#xff09; 25考研数学一难度介绍&#xff1a;今年数学一整体不难&#xff0c;尤其是选填部分&#xff0c;大题的二型线面和概率论大题个人感觉比较奇怪&#xff0c;其他大题还是比较容易的。.26如何准备&a…