【C++进阶】二叉树进阶之二叉搜索树

news2024/12/26 23:40:24

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


目录

  • 一、什么是二叉搜索树
  • 二、二叉搜索树的实现(迭代版)
      • 2.1 定义二叉树的结点
      • 2.2 插入操作
      • 2.3 查找操作
      • 3.4 删除操作
      • 3.5 二叉搜索树的遍历
        • 3.5.1 中序遍历(重点)
        • 3.5.2 前序遍历
        • 3.5.3 后序遍历
  • 四、二叉搜索树的实现 (递归版)
      • 4.1 查找操作
      • 4.2 插入操作
      • 4.3 删除操作
  • 五、代码补充
      • 5.1 释放结点
      • 5.2 拷贝构造
      • 5.3 赋值运算符重载
  • 六、性能分析
  • 七、完整代码
        • 7.1 非递归
      • 7.2 递归

一、什么是二叉搜索树

二叉搜索树(Binary Search Tree)是基于二叉树的一种改进版本。因为普通二叉树没有实际价值,无法进行插入、删除等操作,但二叉搜索树就不一样了。

二叉搜索树又称二叉排序树,它或者是一棵空树。如果不为空则满足一下性质:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也要分别为二叉搜索树

总之,左节点比根小,右节点比根大。因此二叉搜索树的查找效率极高,具有一定的实际价值。

下图展示了二叉搜索树

在这里插入图片描述

搜索二叉树还有一个非常重要的特性:中序遍历(左子树 根 右子树)的结果为升序。

二、二叉搜索树的实现(迭代版)

2.1 定义二叉树的结点

一般来说,二叉树使用链表来定义,不同的是,由于二叉树每个结点都存在两条出边,因此指针域变为两个,分别指向左子树和右子树的根结点地址,因此又把这种链表叫做二叉链表。

template<class K>
struct BSTreeNode // 结点
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

	// 默认构造
	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};

2.2 插入操作

  1. 当插入一个值时,必须先找到满足二叉搜索性质的位置
  2. 如果当前位置不为空或者插入的值和已有的值重复,则插入失败。
  3. 如果找到满足条件的位置并且为空,则结束循环,进行插入。步骤:创建新节点、判断需要插在左边还是右边、链接新节点
template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	BSTree()
		:_root(nullptr)
	{}
	
	// 插入操作
	bool Insert(const K& key)
	{
		// 一开始树为空,直接插入
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		// cur用来查找合适的位置
		Node* cur = _root;
		// parent用来找cur的父亲结点
		Node* parent = nullptr;
		while (cur)
		{
			// 插入的结点大于当前结点
			// 往右走
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			// 插入的结点小于当前结点
			// 往左走
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			// 最后一种情况就是出现冗余
			// 直接返回
			else
			{
				return false;
			}
		}
		// 当循环来到此处说明已经找到合适的位置了
		// 直接开始插入操作
		
		// 1. 创建新的结点
		cur = new Node(key);
		// 2. 判断是插在父亲的左边还是右边
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		
		return true;
	}
private:
	Node* _root;
};

【插入成功】

在这里插入图片描述

【插入失败】

在这里插入图片描述

2.3 查找操作

  1. 从根结点开始比较,比根大的则往右查找;比根小的往左查找。
  2. 走到空还没找到,说明值不存在。否则就是存在
bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else
			{
				return true;
			}
		}

		return false;
	}

【查找成功】

在这里插入图片描述

【查找失败】

在这里插入图片描述

3.4 删除操作

二叉搜索树的删除是个麻烦事,需要考虑很多情况,因此大多数面试都会考察搜索二叉树的删除操作

删除逻辑:

首先查找元素是否在二叉搜索树中,如果不存在,则直接返回;否在要删除的结点就要分以下四种情况

1、 要删除的结点只有左孩子

只需要将被删除结点的左子树与父节点进行链接即可。前提是要判断删除的结点是父结点的左还是右。

在这里插入图片描述

2、 要删除的结点只有右孩子

同理,左子树为空时,将其右子树与父节点进行判断链接,链接完成后删除目标节点

3、要删除的结点有两个孩子

当左右都不为空时,就有点麻烦了,需要找到一个合适的值(即>左子树所有节点的值,又<右子树所有节点的值),确保符合二叉搜索树的基本特点

符合条件的值有:左子树的最右节点(左子树中最大的)或者右子树的最左节点(右子树中最小的),将这两个值中的任意一个覆盖待删除节点的值,都能确保符合要求

以左子树的最右节点(左子树中最大的)为例:

在这里插入图片描述

4、要删除的结点没有孩子

这种情况可以包含在1或者2中

下面是代码实现(详细的代码解释)

bool Erase(const K& key)
{
	// parent - 用来记录删除结点的父亲
	Node* parent = nullptr;
	// cur - 记录被删除的结点
	Node* cur = _root;
	
	// 查找key(被删除的结点)
	while (cur)
	{
		// 如果key比当前元素要大
		// 就往右找
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		// key比当前元素要小
		// 就往左找
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		// 最后一种情况就是找到了
		// 对于找到要分情况讨论	
		else 
		{
			// 1. 如果删除的结点的左孩子为空
			// 右孩子可能为空(包含删除叶子结点的情况)
			// 也可能不为空
			if (cur->_left == nullptr)
			{
				// 可能删除的结点是根结点
				// 也就是以下这种情况
				// 1 --> root cur
				//   2
				//     3
				// 没有考虑这个问题会引发parent野指针问题
				if (cur == _root)
				{
					_root = cur->_right;
				}
				else
				{
					// 如果删除的对象是父亲的右孩子
					// 直接将被删除的结点的孩子接上去
					// (被删除结点右孩子是存在的)
					if (parent->_right == cur)
					{
						parent->_right = cur->_right;
					}
					// 否则就删是删除父亲的左孩子
					else
					{
						parent->_left = cur->_right;
					}
				}
			}	
			// 以下代码逻辑和上面类似
			// 如果删除的结点,右孩子为空
			else if (cur->_right == nullptr)
			{
				if (cur == _root)
				{
					_root = cur->_left;
				}
				else
				{
					if (parent->_right == cur)
					{
						parent->_right = cur->_left;
					}
					else
					{
						parent->_left = cur->_left;
					}
				}
			} 
			// 左右都不为空 
			// 左右子树都不为空的场景中
			// parent 要初始化为cur,避免后面的野指针问题
			else
			{
				// 找替代节点
				// 以左子树的最右节点(左子树中最大的)为例
				Node* parent = cur;
				Node* leftMax = cur->_left;
				while (leftMax->_right)
				{
					parent = leftMax;
					leftMax = leftMax->_right;
				}
				// 替换
				swap(cur->_key, leftMax->_key);

				if (parent->_left == leftMax)
				{
					parent->_left = leftMax->_left;
				}
				else
				{
					parent->_right = leftMax->_left;
				}
				cur = leftMax;
			}

			delete cur;
			return true;
		}
	}

	return false;
}

3.5 二叉搜索树的遍历

二叉搜索树的遍历操作和二叉树一模一样的,因此可以有前序遍历、中序遍历以及后序遍历。

3.5.1 中序遍历(重点)

搜索二叉树的 中序遍历(左子树 根 右子树)的结果为升序

void InOrder(Node* root)
{
	if (root == NULL)
	{
		return;
	}

	InOrder(root->_left);
	cout << root->_key << " ";
	InOrder(root->_right);
}

但因为这里是一个被封装的类,所以面临着一个尴尬的问题:二叉搜索树的根root是私有,外部无法直接获取

解决办法:

  1. 公有化(破坏类的封装性,不推荐)
  2. 使用静态成员函数可以直接被类外使用。(也是ok,但是不推荐)
  3. 将这种需要用到根的函数再封装。(推荐)
public:
void InOrder()
{
	_InOrder(root);
}
private:
void _InOrder(Node* root)
{
	if (root == NULL)
	{
		return;
	}

	_InOrder(root->_left);
	cout << root->_key << " ";
	_InOrder(root->_right);
}

实际调用时,只能调到InOrder,因为真正的函数_InOrder为是私有,除了类之外,其他地方不可访问。

3.5.2 前序遍历

public:
void PreOrder()
{
	_PreOrder(_root);
}

private:

	void _PreOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		cout << root->_key << " ";
		_PreOrder(root->_left);
		_PreOrder(root->_right);
	}

3.5.3 后序遍历

public:
void PostOrder()
{
	_PostOrder(_root);
}
private:

void _PostOrder(Node* root)
{
	if (root == nullptr)
	{
		return;
	}
	_PostOrder(root->_left);
	_PostOrder(root->_right);
	cout << root->_key << ' ';
}

四、二叉搜索树的实现 (递归版)

4.1 查找操作

递归查找逻辑:

  1. 如果当前根的值<查找值,递归至右树查找
  2. 如果当前根的值>查找值,递归至左树查找

二叉树的递归常常是通过将问题分解为更小规模的子问题来解决的。通过传递根节点作为参数,我们可以在每次递归中处理当前节点,并将问题分解为处理左子树和右子树的子问题。因此,还需要对函数进行封装。

public:
bool Find(const K& key)
{
	return _Find(_root, key);
}
private:
bool _Find(Node* root, const k& key)
{
	// 如果根为空,说明查找失败(递归出口)
	if (root == nullptr)
		return false;

	// 查找的值比当前结点大,往右找
	if (root->_key < key)
	{
		return _Find(root->_right);
	}
	// 查找的值比当前结点小,往左找
	else if (root->_key > key)
	{
		return _Find(root->_left);
	}
	// 最后一种情况就是找到了
	else
		return true;
}

4.2 插入操作

这里和其它不同的是,在传参的时候使用了引用传引用是为了在递归查找过程中能够修改目标节点的指针值。搜索二叉树的递归查找算法通常会通过比较当前节点的值与目标值来决定向左子树还是右子树继续查找。如果不传引用,每次递归调用时都会创建一个新的节点指针副本,这样就无法修改原始节点的指针值,从而导致无法正确返回目标节点的指针。

这么说有点抽象,可以先看代码理解;过会画完递归展开图大家就能够理解了。

public:
bool Insert(const K& key)
{
	return _Insert(_root, key);
}

private:
bool _Insert(Node*& root, const K& key)
{
	// 当走到空,说明找到了合适的位置
	if (root == nullptr)
	{
		// 还得找到父亲,为什么加个引用就搞定了?
		root = new Node(key);
		return true;
	}

	// 插入的值比当前结点大,往右找
	if (root->_key < key)
	{
		return _Insert(root->_right);
	}
	// 插入的值比当前结点小,往左找
	else if (root->_key > key)
	{
		return _Insert(root->_left);
	}
	// 当插入的值和树发生冗余,直接返回false
	else
	{
		return false;
	}
}

【递归展开图】

在这里插入图片描述

4.3 删除操作

递归删除时也使用了引用,其想法和插入一样,不需要找删除结点的父亲,直接修改目标节点的指针值

public:
bool Earse(const K& key)
{
	return _Erase(root, key);
}

private:
bool _Erase(Node* root, const K& key)
{
	// 如果是空结点,说明删除失败
	if (root == nullptr)
		return false;
	
	if (root->_key < key)
	{
		return _Erase(root->_right, key);
	}
	else if (root->_key > key)
	{
		return _Erase(root->_left, key);
	}
	// 找到key
	else
	{
		Node* del = root;
		// 还是分三种情况
		
		// 1. 删除结点的左孩子为空
		if (root->_left == nullptr)
		{
			root = root->_right;
		}
		// 2. 删除结点的右孩子为空
		else if (root->_right == nullptr)
		{
			root = root->_left;
		}
		// 3. 删除结点孩子都不为空
		else
		{
			//递归为小问题去处理
			Node* maxLeft = root->_left;
			while (maxLeft->_right)
			{
				maxLeft = maxLeft->_right;
			}

			//注意:需要交换
			std::swap(root->_key, maxLeft->_key);

			//注意:当前找的是左子树的最右节点,所以递归从左子树开始
			return _Erase(root->_left, key);
		}
		delete del;	//释放节点
		return true;
	}
}	

五、代码补充

5.1 释放结点

创建节点时,使用了new申请堆空间,根据动态内存管理原则,就需要使用delete释放申请的堆空间,但二叉搜索树是一棵树,不能直接释放,需要递归式的遍历每一个节点

释放思路:后序遍历思想

public:
~BSTree()
{
	_destory(_root);
}

private:
void _destory(Node*& root)
{
	if (root == nullptr)
		return;

	//后序遍历销毁
	destory(root->_left);
	destory(root->_right);

	delete root;
	root = nullptr;
}

5.2 拷贝构造

销毁问题考虑完以后,就要想是否会有浅拷贝问题,因为浅拷贝问题会导致一个结点析构两次,程序就崩了。而我们在类和对象中说过,如果类中有动态分配内存的指针变量,则需要手动编写深拷贝的拷贝构造函数。

public:
BSTree(BSTree<K>& tree)
	:_root(nullptr)
{
	_root = _Copy(tree._root);
}

private:
Node* _Copy(Node* root)
{
	//递归拷贝
	if (root == nullptr)
		return nullptr;

	Node* new_root = new Node(root->_key);	
	new_root->_left = _Copy(root->_left);
	new_root->_right = _Copy(root->_right);

	return new_root;
}

5.3 赋值运算符重载

如果没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝(浅拷贝)。其默认成员函数和拷贝构造的默认函数类似:内置类型成员变量是直接赋值的,而自定义类型成员会去调用它的默认函数。因此赋值运算符也需要自己实现深拷贝

直接写现代写法

BSTree<K> operator=(BSTree<K> tree)
{
	std::swap(_root, tree._root);
	return *this;
}

六、性能分析

从名字上来看,二叉树的特性就是查找快。

搜索二叉树的时间复杂度取决于树的高度。因此当是一颗平衡二叉搜索树时,时间复杂度是O(log n)。因为每次搜索都可以通过比较目标值与当前节点的值来确定向左子树还是右子树进行搜索,这样每次都可以将搜索范围减半。
是不是有点类似于二分查找,但二分查找不是一个很实用的算法。因为对比二叉搜索树来说,二分查找(底层是一个数组)的删除和插入的效率低0(n)(特别是中间插入和删除)

在这里插入图片描述

二叉搜索树看起来这么完美,但下限没有保障。在最坏的情况下,当搜索二叉树是一个不平衡的树时,时间复杂度为O(n),其中n是树中节点的数量。这是因为在最坏情况下,每次搜索都要遍历树的所有节点。

在这里插入图片描述

因此,为了解决这个问题,引入了AVL树和红黑树(后续会讲解)

七、完整代码

7.1 非递归

#pragma once

// 迭代版本
template<class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	BSTree()
		:_root(nullptr)
	{}

	bool Insert(const K& key)
	{
		// 如果一开始树为空,直接插入
		if (_root == nullptr)
		{
			// 对自定义类型的new,会自动调用其默认构造函数
			_root = new Node(key);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(key);
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		return true;
	}

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

		return false;
	}

	bool Erase(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;

		while (cur)
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else // 找到了
			{
				// 左为空
				if (cur->_left == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						if (parent->_right == cur)
						{
							parent->_right = cur->_right;
						}
						else
						{
							parent->_left = cur->_right;
						}
					}
				}// 右为空
				else if (cur->_right == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (parent->_right == cur)
						{
							parent->_right = cur->_left;
						}
						else
						{
							parent->_left = cur->_left;
						}
					}
				} // 左右都不为空 
				else
				{
					// 找替代节点
					Node* parent = cur;
					Node* leftMax = cur->_left;
					while (leftMax->_right)
					{
						parent = leftMax;
						leftMax = leftMax->_right;
					}

					swap(cur->_key, leftMax->_key);

					if (parent->_left == leftMax)
					{
						parent->_left = leftMax->_left;
					}
					else
					{
						parent->_right = leftMax->_left;
					}

					cur = leftMax;
				}

				delete cur;
				return true;
			}
		}

		return false;
	}

	void InOrder()
	{
		_InOrder(_root);
	}

	void PreOrder()
	{
		_PreOrder(_root);
	}

	void PostOrder()
	{
		_PostOrder(_root);
	}

private:

	void _PostOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_PostOrder(root->_left);
		_PostOrder(root->_right);
		cout << root->_key << ' ';
	}

	void _PreOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		cout << root->_key << " ";
		_PreOrder(root->_left);
		_PreOrder(root->_right);
	}

	void _InOrder(Node* root)
	{
		if (root == NULL)
		{
			return;
		}

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

private:
	Node* _root;
};

7.2 递归

#pragma once
template<class K>
struct BSTNode
{
	BSTNode<K> _left;
	BSTNode<K> _right;
	K _key;

	BSTNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, key(key)
	{}
};

template<class K>
class BSTree
{
	typedef BSTNode<K> Node;
public:
	BSTree()
		:_root(nullptr)
	{}

	~BSTree()
	{
		_destory(_root);
	}

	bool Find(const K& key)
	{
		return _Find(_root, key);
	}

	bool Insert(const K& key)
	{
		return _Insert(_root, key);
	}

	bool Earse(const K& key)
	{
		return _Erase(root, key);
	}

	BSTree(BSTree<K>& tree)
		:_root(nullptr)
	{
		_root = _Copy(tree._root);
	}

	BSTree<K> operator=(BSTree<K> tree)
	{
		std::swap(_root, tree._root);
		return *this;
	}
private:

	Node* _Copy(Node* root)
	{
		//递归拷贝
		if (root == nullptr)
			return nullptr;

		Node* new_root = new Node(root->_key);	
		new_root->_left = _Copy(root->_left);
		new_root->_right = _Copy(root->_right);

		return new_root;
	}


	void _destory(Node*& root)
	{
		if (root == nullptr)
			return;

		//后序遍历销毁
		destory(root->_left);
		destory(root->_right);

		delete root;
		root = nullptr;
	}

	bool _Find(Node* root, const k& key)
	{
		if (root == nullptr)
			return false;

		// 查找的值比当前结点大,往右找
		if (root->_key < key)
		{
			return _Find(root->_right);
		}
		// 查找的值比当前结点小,往左找
		else if (root->_key > key)
		{
			return _Find(root->_left);
		}
		// 最后一种情况就是找到了
		else
			return true;
	}

	bool _Insert(Node*& root, const K& key)
	{
		// 当走到空,说明找到了合适的位置
		if (root == nullptr)
		{
			// 还得找到父亲,为什么加个引用就搞定了?
			root = new Node(key);
			return true;
		}

		// 插入的值比当前结点大,往右找
		if (root->_key < key)
		{
			return _Insert(root->_right);
		}
		// 插入的值比当前结点小,往左找
		else if (root->_key > key)
		{
			return _Insert(root->_left);
		}
		// 当插入的值和树发生冗余,直接返回false
		else
		{
			return false;
		}
	}

	bool _Erase(Node* root, const K& key)
	{
		if (root == nullptr)
			return false;
		
		if (root->_key < key)
		{
			return _Erase(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _Erase(root->_left, key);
		}
		else
		{
			Node* del = root;
			// 还是分三种情况
			
			// 1. 删除结点的左孩子为空
			if (root->_left == nullptr)
			{
				root = root->_right;
			}
			// 2. 删除结点的右孩子为空
			else if (root->_right == nullptr)
			{
				root = root->_left;
			}
			// 3. 删除结点孩子都不为空
			else
			{
				//递归为小问题去处理
				Node* maxLeft = root->_left;
				while (maxLeft->_right)
				{
					maxLeft = maxLeft->_right;
				}

				//注意:需要交换
				std::swap(root->_key, maxLeft->_key);

				//注意:当前找的是左子树的最右节点,所以递归从左子树开始
				return _Erase(root->_left, key);
			}
			delete del;	//释放节点
			return true;
		}
	}

private:
	Node* _root;
};

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

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

相关文章

测试面试回顾(1)

硬件测试面试回顾&#xff08;1&#xff09; pn结MOS和CMOS消耗功耗——MOS器件开关时半导体工艺半导体器件SPSS常用的10种统计分析如何用SPSS进行数据分析&#xff1f;Jmeter介绍及测试用例编写JmeterSelenium测试用例测试用例设计方法通用测试用例八要素自动化运维全局变量与…

matlab自带VMD详解,VMD去噪,VMD分解

为了更好的利用MATLAB自带的vmd函数&#xff0c;本期作者将详细讲解一下MATLAB自带的vmd函数如何使用&#xff0c;以及如何画漂亮的模态分解图。 首先给出官方vmd函数的调用格式。 [imf,residual,info] vmd(x) 函数的输入&#xff1a; 这里的x是待分解的信号&#xff0c;一行或…

Kubernetes Up and Running

从整体概念思想到具体组件对象的思想设计说明和实践探索&#xff0c;对分布式系统软件建设的认识更上一层楼。 英文版本下载 链接&#xff1a;https://pan.baidu.com/s/1ZjMqEMc3GGJxDc0ekz4ihA?pwdhjko 提取码&#xff1a;hjko 摘要 《Kubernetes Up and Running》是一本非…

前后端分离,JSON数据如何交互

如何接收&#xff1a; 在配置文件商法加上相应注解 EnableWebMvc 在接收的路径上加上RequestBody注解 注解的作用&#xff1a;在Spring框架中&#xff0c;RequestBody注解用于将HTTP请求的body中的内容转换为Java对象&#xff0c;并将其作为参数传递给控制器方法。它通常用…

【TCPDF】使用TCPDF导出PDF文件

目录 一、安装TCPDF类库 二、安装字体 三、使用TCPDF导出PDF文件 目的&#xff1a;PHP通过TCPDF类库导出文件为PDF。 开发语言及类库&#xff1a;ThinkPHP、TCPDF 效果图如下 一、安装TCPDF类库 在项目根目录使用composer安装TCPDF&#xff0c;安装完成后会在vendor目录下…

JAVAEE初阶相关内容第十一弹--多线程(进阶)

目录 一、常见的锁策略 1乐观锁VS悲观锁 1.1乐观锁 1.2悲观锁 2.轻量级锁VS重量级锁 2.1轻量级锁 2.2重量级锁 3.自旋锁VS挂起等待锁 3.1自旋锁 3.2挂起等待锁 4.互斥锁VS读写锁 4.1互斥锁 4.2读写锁 5.公平锁VS非公平锁 5.1公平锁 5.2非公平锁 6.可重入锁VS不…

记LGSVL Map Annotation(2)导入点云、以及地图

导入点云 内置的点云导入器工具提供了将最流行的点云文件格式&#xff08;PCD、PLY、LAS、LAZ&#xff09;转换为可用于仿真的数据所需的所有功能。 要访问点云导入器窗口&#xff0c;请在 Unity 编辑器中打开模拟器项目&#xff0c;然后导航到 Simulator/Import Point Cloud…

SpringCloud学习笔记(六)OpenFeign 服务接口调用

一、OpenFeign简介 1、OpenFeign是什么 Feign是一个声明式WebService客户端&#xff0c;使用Feign能让编写Web Service客户端更加简单。 它的使用方法是定义一个服务接口然后在上面添加注解&#xff0c;Feign也支持可拔插式的编码器和解码器&#xff0c;Spring Cloud对Feign进…

SolVES4.1学习2——导入数据运行模型

使用样例数据运行模型很容易&#xff0c;运行自己的数据要根据教程先对数据进行预处理之后根据教程导入数据。 首先新建一个solves数据库&#xff0c;之后restore。导入数据大概的流程为&#xff1a; 1、导入数据 首先使用PostGIS导入矢量数据。矢量数据包括点位和范围数据。…

grpc多语言通信之GO和DART

都是一个吗生的,找下例子 上一篇文章说到go实现的grpc方法已经实现了一个grpc的server端, 注意: 这两个项目的.proto文件应当是完全一致的,只是方法用各自的语言实现罢了 报错了: Caught error: gRPC Error (code: 12, codeName: UNIMPLEMENTED, message: grpc: Decompresso…

MySQL——命令行客户端的字符集问题

原因&#xff1a;服务器端认为你的客户端的字符集是utf-8&#xff0c;而实际上你的客户端的字符集是GBK。 查看所有字符集&#xff1a;SHOW VARIABLES LIKE character_set_%; 解决方案&#xff0c;设置当前连接的客户端字符集 “SET NAMES GBK;”

Nacos服务心跳和健康检查源码介绍

服务心跳 Nacos Client会维护一个定时任务通过持续调用服务端的接口更新心跳时间&#xff0c;保证自己处于存活状态&#xff0c;防止服务端将服务剔除&#xff0c;Nacos默认5秒向服务端发送一次&#xff0c;通过请求服务端接口/instance/beat发送心跳。 客户端服务在注册服务的…

论文解读 | 用于3D对象检测的PV-RCNN网络原创

原创 | 文 BFT机器人 01 背景 本文的背景涉及到3D物体检测&#xff0c;这是一个在自动驾驶和机器人等领域应用广泛的重要问题。在这些领域&#xff0c;LiDAR传感器被广泛用于捕捉3D场景信息&#xff0c;生成不规则且稀疏的点云数据。这些点云数据提供了理解和感知3D场景的关键…

QVector 和 QMap

QVector_QMap QVector简介 头文件&#xff1a;#include<QVector> 模块&#xff1a; QT core 功能&#xff1a; QVector类是动态数组的模板类&#xff0c;顺序容器&#xff0c;它将自己的每一个对象存储在连续的内存中&#xff0c;可以使用索引号来快速访问它们 常用…

【数据结构】树和二叉树概念

1.树概念及结构 树概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 有一个特殊的结点&#xff0c;…

Android获取系统读取权限

在Androidifest.xml文件中加上授权语句 <uses-permission android:name"android.permission.WRITE_EXTERNAL_STORAGE"/><uses-permission android:name"android.permission.READ_EXTERNAL_STORAGE"/>

Android12之/proc/pid/status参数含义(一百六十五)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

【LeetCode题目详解】第九章 动态规划part13 300.最长递增子序列 674. 最长连续递增序列 718. 最长重复子数组 (day52补)

本文章代码以c为例&#xff01; 一、力扣第300题&#xff1a;最长递增子序列 题目&#xff1a; 给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数组中的元素而不改…

Scrum敏捷开发工具的基本概念、使用方法、优势以及实际应用案例

​随着软件开发行业的不断发展和进步&#xff0c;Scrum敏捷开发工具逐渐成为了备受关注的话题。 Scrum是一种灵活且高效的项目管理方法&#xff0c;旨在提高团队协作和交付效率&#xff0c;使团队能够更快地响应变化和需求。 本文将深入探讨Scrum敏捷开发工具的基本概念、使用…

分类预测 | MATLAB实现WOA-CNN-BiGRU鲸鱼算法优化卷积双向门控循环单元数据分类预测

分类预测 | MATLAB实现WOA-CNN-BiGRU鲸鱼算法优化卷积双向门控循环单元数据分类预测 目录 分类预测 | MATLAB实现WOA-CNN-BiGRU鲸鱼算法优化卷积双向门控循环单元数据分类预测分类效果基本描述模型描述程序设计参考资料 分类效果 基本描述 1.Matlab实现WOA-CNN-BiGRU多特征分类…