binary_search_tree的介绍与实现(二叉搜索树精美图示详解哦)

二叉搜搜索树

  • 引言
  • 二叉搜索树的介绍
  • 二叉搜索树的实现
    • 框架
    • 默认成员函数
      • 构造
      • 析构
      • 赋值重载
    • InsertR(插入)
    • EraseR(删除)
    • SearchR(查找)
  • 源码概览
  • 总结

引言

在C语言部分,我们已经认识了树与二叉树的结构:
戳我看树与二叉树介绍
并且了解了二叉树顺序结构带来的一些应用,即在中的应用:
戳我看堆详解哦
戳我看优先级队列详解

但是二叉树的链式结构仿佛在数据处理方面并没有什么突出的表现,在对一个没有什么特征的二叉树进行增删查改是没有什么意义的。但是如果链式的二叉树具有了一些特性,就会使它在数据处理上的效率出现质的提升。

二叉搜索树,顾名思义,在搜索方面的表现很突出:
之前我们了解到的效率很高的搜索算法就是二分查找算法,在二分查找算法下,搜索的时间复杂度可以达到 O(logN)。但是二分查找有一个很大的限制条件,即它必须要求数据有序
但是对于二叉搜索树而言,在建立二叉搜索树结构后,就可以实现查找效率为 O(logN)

二叉搜索树的介绍

二叉搜索树即在二叉树的基础上满足:
对于任何一个根结点,若左子树不为空,它的左子树的值全部小于根节点;若右子树不为空,右子树的值全部大于根节点。这也就意味着,如果我们中序遍历一个二叉搜索树,得到的一定是一个递增序列:

在这里插入图片描述
在这样的搜索二叉树中,如果我们想要查找某个值target,我们只需要从根结点开始,若当前结点的值大于target向左找,小于target向右找。就可以以很高的效率找到target
在这里插入图片描述

但是,同样是这样的10个数据,根据插入的顺序不同,就会有不同形状的二叉搜索树。在最坏的情况下,二叉搜索树的形状就与单链表类似:
在这里插入图片描述
在这种情况中,搜索的时间复杂度就变成了O(N)(在下一篇文章中介绍到的平衡二叉树,就可以解决这样的问题)

二叉搜索树的实现

在了解了二叉搜索树后,就来详细介绍其实现:

框架

在二叉树的链式结构中,父结点有指向左右孩子结点的指针,依次向下结构相同。即树形结构具有很好的递归特性,所以在此次实现中选择使用递归的方式(函数名中的R表示递归版本实现);
当然,为防止命名冲突,将实现放在我们的命名空间qqq中。

二叉搜索树BSTreeR是一个类模板,这样就可以支持二叉搜索书中存储各种数据。它的成员变量为根结点的指针:

	template<class K>
	class BSTreeR
	{
	protected:
		Node* _root; //Node 即 BSTreeNodeR<K> 的重命名
	};

二叉搜索树的结点类BSTreeNodeR当然也是一个类模板:
在结点类中有当前结点的数据左右孩子结点的指针。在构造函数中对数据进行初始化,并且将左右孩子结点指针置空即可(由于在对树进行操作时,需要经常访问左右结点与数据,所以这个结点类直接使用struct来定义):

	template<class K>
	struct BSTreeNodeR 
	{
		BSTreeNodeR<K>* _left;
		BSTreeNodeR<K>* _right;
		K _key;

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

另外,要想递归的实现二叉搜索树的增删查改,那么这个接口的参数中一定是有根结点指针的。但是根结点指针是一个私有的成员变量,所以我们在类外是不能调用这些递归的函数进行增删查改的。
所以我们选择将递归调用的函数(瓤)用另一个对外的接口(壳)包起来,我们在类外调用壳接口时,在壳中调用递归函数就可以解决不能访问私有成员变量的问题了(以InsertR为例):

	template<class K>
	class BSTreeR
	{
	public:
		typedef BSTreeNodeR<K> Node;
	public:
		//壳接口
		bool InsertR(const K& key)
		{
			return _InsertR(_root, key);
		}
	protected:
		//递归函数
		bool _InsertR(Node*& root, const K& key);
        //树根节点指针
		Node* _root;
	};

默认成员函数

构造

对于构造函数,我们实现无参构造拷贝构造
无参构造没有什么说的,在构造函数**初始化列表中将根结点指针初始化为nullptr**即可:

	BSTreeR()
		: _root(nullptr)
	{}

拷贝构造就需要逐一使用原树中的结点去对新树的结点进行递归赋值
在壳接口BSTreeR()中去调用递归函数_constructor():

	BSTreeR(const BSTreeR<K>& ctree)
	{
		_constructor(_root, ctree._root);
	}

_constructor()有两个参数,即原树的根结点指针croot新树的根节点指针root的引用

  • 在函数中:首先判断原树的指针croot是否为空,若为空说明这给条分支已经拷贝完毕,返回给上一层去另外的分支拷贝;
  • croot不为空,就使用原树结点中的数据来new一个结点;
  • 然后使root指向新创建出的结点(由于root是根结点的引用,在该栈帧中进行修改同时也修改了上层调用的数据,即在这一步中就实现了将这个新结点与其父节点进行链接);
  • 然后两次递归调用,拷贝当前结点地两个子结点:

在这里插入图片描述

	void _constructor(Node*& root, Node* croot)
	{
		if (croot == nullptr)
			return;

		Node* newnode = new Node(croot->_key);
		root = newnode;
		_constructor(root->_left, croot->_left);
		_constructor(root->_right, croot->_right);
	}

析构

析构函数中,我们需要逐一释放每一个结点
使用壳接口~BSTreeR()调用递归函数_destructor()

	~BSTreeR()
	{
		_destructor(_root);
	}

_destructor的参数为根结点指针的引用。在析构时,如果先析构了当前结点,就不能再访问其左右结点,也就不能再继续析构下去了,所以这里使用的是后序遍历地析构方式,先析构其左右结点,再析构根结点:

  • 在函数中:首先判断root是否为空,为空说明这条分支已经走到底,向上一层返回;
  • 这里执行的是后序遍历,当不为空时,继续向下访问去析构左子树;
  • 左子树析构完后向上返回,再继续向下访问去析构右子树;
  • 右子树析构完后向上返回,才可以析构当前结点,即要析构当前结点,要么当前结点左右子树都为空,要么当前结点左右子树都已经被析构完了

在这里插入图片描述

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

		_destructor(root->_left);
		_destructor(root->_right);
		delete root;
		root = nullptr;
	}

赋值重载

赋值重载,直接使用现代写法

  • 使用值传递,传参时生成一个临时对象ctree
  • 然后在函数中将当前对象的根结点指针与临时对象的根结点指针互换;
  • 然后直接返回*this即可(被换值的临时对象在函数栈帧销毁时会自动析构,不用我们处理):
	BSTreeR<K>& operator=(BSTreeR<K> ctree)
	{
		swap(_root, ctree._root);
		return *this;
	}

InsertR(插入)

在树中插入元素时,首先需要递归找到插入位置,然后进行链接。
当然,我们使用一个壳接口InsertR,其中调用递归函数_InsertR

	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}

_InsertR有两个参数,即根结点指针的引用要插入的元素。返回值为bool

在向下递归找正确位置的过程中:

  • 若新数据小于当前结点的数据,向左边递归;
  • 若新数据大于当前结点的数据,向右边递归;
  • 若新数据等于当前结点的数据,说明这个数据已经存在,不能再插入,返回false

当向下遍历到树底时,即root == nullptr时,说明当前的位置就是插入位置,将新结点与上层结点链接即可。
但是这里存在着一个问题:即当前结点并不清楚自己是上层结点的左结点还是右结点。如果要在递归遍历时同时记录父结点,在不确定时与父节点重新比较一遍,这样就太麻烦了。
根节点指针的引用传参就完美的解决了这个问题:我们只需要让这个引用指向新结点即可。如果上层传参的是左孩子指针的引用,就能直接使左孩子指针指向新结点,传参右孩子指针的引用亦然:
在这里插入图片描述

	bool _InsertR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			Node* newnode = new Node(key);
			root = newnode;
			return true;
		}

		if (key < root->_key)
		{
			return _InsertR(root->_left, key);
		}
		else if (key > root->_key)
		{
			return _InsertR(root->_right, key);
		}
		else
		{
			cout << "The key has existed" << endl;
			return false;
		}
	}

EraseR(删除)

删除的实现在二叉搜索树中是最复杂的一部分,其中有各种繁琐的情况,接下来就来一一分析解决:

先来简单的介绍一下删除的思路

  • 要在二叉搜索树中删除一个数据,我们首先要找到该数据的结点,然后才能将这个目标结点删除;
  • 对于只有一个子树的目标结点而言,我们只需要将它的那个子树托管给它的父节点(若该数据小于其父结点,则其左右子树数据都小于其父结点,反之则都大于其父结点),然后直接释放它即可(这类似于单链表的删除);
  • 但是,如果这个目标结点有左右子树,那么就不能直接进行托管了,解决方案是:
    先将目标结点的数据于其左子树中的最大结点的数据进行交换(右树最小也可行,这里不进行介绍),这个左子树的最大结点一定只有一个左子树,或者没有孩子结点;
    然后按照上面的只有单孩子的思路进行删除即可:
    (这里将各个情况都进行图示)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    不可能有第四种情况了!

在写代码时,递归的方式其实要比非递归的方式要简便一些,虽然思路是一模一样的。

不必多说,在壳接口EraseR中调用递归函数_EraseR

	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}

_EraseR有两个参数,即根结点指针的引用要删除的数据,返回值类型为bool

_EraseR函数中,首先递归寻找要删除的数据(若目标数据小于当前结点,向子树递归,反之向右递归),如果root == nullptr 就说明找到底都没有要删除的那个数据,返回false即可:

  •   //递归找到要删的结点
      if (root == nullptr)
      {
      	cout << "The key does not exist" << endl;
      	return false;
      }
      	
      if (key < root->_key)
      {
      	return _EraseR(root->_left, key);
      }
      else if (key > root->_key)
      {
      	return _EraseR(root->_right, key);
      }
      else //找到
      {}
    

当找到要删除的元素后,判断这个目标结点的子树情况:

  • 目标结点只有左子树,将root->_right赋值给root后(前面提到过,这里的root是上层子结点指针的引用,所以这样的赋值可以直接完成链接),deleteroot即可。但是如果在赋值后delete,释放掉的就是本来的root->right,如果在赋值前delete,之后就无法进行赋值,所以这里先用临时变量deltemp来存原来的root,在链接完成后释放deltemp即可(目标结点只有右子树情况与之相反):
  •     if (root->_left == nullptr) //左为空
      	{
      		Node* deltemp = root;
      		root = root->_right;
      		delete deltemp;
    
      		return true;
      	}
      	else if (root->_right == nullptr) //右为空
      	{
      		Node* deltemp = root;
      		root = root->_left;
      		delete deltemp;
    
      		return true;
      	}
    
  • 目标结点有左右子树,首先循环寻找左子树种的最大结点;
  • 然后将最大结点的值与目标结点交换,这样要删除的目标结点就满足前面的情况了;
  • 最后再递归调用一下_EraseR,参数就传交换后的目标结点,即之前的左树最大结点即可。这样就可以直接完成删除:
  •   	else//有两个个孩子时,找左树中的最大元素,交换后再调该函数
      	{
      		Node*& leftMax = root->_left;
      		while (leftMax->_right) //循环找左最大
      		{
      			leftMax = leftMax->_right;
      		}
      		std::swap(leftMax->_key, root->_key);//交换值
    
      		return _EraseR(leftMax, key);
      	}
    

总体代码:

	bool _EraseR(Node*& root, const K& key)
	{
		//递归找到要删的结点
		if (root == nullptr)
		{
			cout << "The key does not exist" << endl;
			return false;
		}
			
		if (key < root->_key)
		{
			return _EraseR(root->_left, key);
		}
		else if (key > root->_key)
		{
			return _EraseR(root->_right, key);
		}
		else //找到
		{
			if (root->_left == nullptr) //左为空
			{
				Node* deltemp = root;
				root = root->_right;
				delete deltemp;

				return true;
			}
			else if (root->_right == nullptr) //右为空
			{
				Node* deltemp = root;
				root = root->_left;
				delete deltemp;

				return true;
			}
			else//有两个个孩子时,找左树中的最大元素,交换后再调该函数
			{
				Node*& leftMax = root->_left;
				while (leftMax->_right)
				{
					leftMax = leftMax->_right;
				}
				std::swap(leftMax->_key, root->_key);//交换值

				return _EraseR(leftMax, key);
			}
		}
	}

在这里简单介绍一下非递归的实现方式复杂在哪里了
首先在找目标结点时,就需要一个辅助指针parentnode,来描述当前结点的父结点;
然后在只有一个子树或无子树的情况种,要判断目标结点与其父节点的大小,来决定要托管给父结点的哪个子树;
另外在寻找左子树的最大结点时,需要三个变量同时移动:一个变量leftmax描述左子树最大结点、一个变量maxparent描述这个最大结点的父节点、一个变量temp做临时变量;
最后对于要删除的目标结点究竟是无子节树点还是只有左子树的情况要进行判断:
(这里放一段非递归实现的代码,供大家参考)

		//非递归实现的erase
		bool Erase(const K& key)
		{
			Node* parentnode = _root;
			Node* cur = _root;

			while (cur)//找cur
			{
				if (key > cur->_key)
				{
					parentnode = cur;
					cur = parentnode->_right;
				}
				else if (key < cur->_key)
				{
					parentnode = cur;
					cur = parentnode->_left;
				}
				else
				{
					break;
				}
			}
			if (cur == nullptr)
			{
				cout << "The key does not exist" << endl;
				return false;
			}

			//要删除的结点只有一个孩子,将其孩子直接给其父亲
			//要删除的结点有左右孩子,用左子树中最大的元素替代它后,将左子树的元素删除。
			//若替代后,原左子树的最大结点存在左孩子,将其左孩子给其父亲
			if (cur->_left == nullptr && cur->_right == nullptr)
			{
				if (parentnode == cur)
				{
					_root = nullptr;
				}
				else if (cur->_key > parentnode->_key)
				{
					parentnode->_right = nullptr;
					delete cur;
				}
				else
				{
					parentnode->_left = nullptr;
					delete cur;
				}
			}
			else if (cur->_left == nullptr && cur->_right != nullptr)
			{
				if (parentnode == cur)
				{
					_root = cur->_right;
				}
				else if (cur->_key > parentnode->_key)
				{
					parentnode->_right = cur->_right;
					delete cur;
				}
				else
				{
					parentnode->_left = cur->_right;
					delete cur;
				}
			}
			else if (cur->_left != nullptr && cur->_right == nullptr)
			{
				if (parentnode == cur)
				{
					_root = cur->_left;
				}
				else if (cur->_key > parentnode->_key)
				{
					parentnode->_right = cur->_left;
					delete cur;
				}
				else
				{
					parentnode->_left = cur->_left;
					delete cur;
				}
			}
			else //左右孩子均不为空
			{
				//找左子树中的最大元素
				Node* maxparent = cur;
				Node* leftmax = maxparent->_left;
				Node* temp = leftmax->_right;
				while (temp)
				{
					maxparent = leftmax;
					leftmax = temp;
					temp = temp->_right;
				}

				std::swap(cur->_key, leftmax->_key);

				if (leftmax == maxparent->_left) //只有在这种情况下,才需要将最大值的左树(只可能是左)给maxparent的左
				{
					maxparent->_left = leftmax->_left;
					delete leftmax;
				}
				else
				{
					maxparent->_right = leftmax->_left;
					delete leftmax;
				}
			}
			return true;
		}

SearchR(查找)

其实在介绍完删除操作之后,查找操作就很简单了:

壳接口SearchR中调用递归函数_SearchR

	Node* SearchR(const K& key)
	{
		return _SearchR(_root, key);
	}

函数_SearchR的参数为根结点的指针要查找的数据(这里不需要指针的引用传参了,是因为我们不需要建立与上层的链接了):

在查找过程中

  • 若目标结点小于当前结点,在左子树中递归寻找;
  • 若目标结点大于当前结点,在右子树中递归寻找;
  • 若等于则返回这个目标结点的指针。

如果root == nullptr 说明找到底也没有目标结点,返回nullptr即可:

	Node* _SearchR(Node* root, const K& key)
	{
		if (root == nullptr)
			return nullptr;

		if (key < root->_key)
		{
			return _SearchR(root->_left, key);
		}
		else if(key > root->_key)
		{
			return _SearchR(root->_right, key);
		}
		else
		{
			return root;
		}
	}

这里提一下为什么在之前的插入与删除操作中没有复用这个查找函数,因为插入与删除的操作在查找到目标结点后,都需要修改与上一层调用中结点的链接,所以这里的查找函数还是不要凑热闹。

源码概览

namespace qqq
{
	//递归
	template<class K>
	struct BSTreeNodeR
	{
		BSTreeNodeR<K>* _left;
		BSTreeNodeR<K>* _right;
		K _key;

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

	template<class K>
	class BSTreeR
	{
	public:
		typedef BSTreeNodeR<K> Node;
	public:
		BSTreeR()
			: _root(nullptr)
		{}
		BSTreeR(const BSTreeR<K>& ctree)
		{
			_constructor(_root, ctree._root);
		}
		BSTreeR<K>& operator=(BSTreeR<K> ctree)
		{
			swap(_root, ctree._root);
			return *this;
		}
		~BSTreeR()
		{
			_destructor(_root);
		}

		bool InsertR(const K& key)
		{
			return _InsertR(_root, key);
		}

		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

		Node* SearchR(const K& key)
		{
			return _SearchR(_root, key);
		}

		void InOrderR()  //测试代码
		{
			_InOrderR(_root);
			cout << endl;
		}
	protected:
		void _constructor(Node*& root, Node* croot)
		{
			if (croot == nullptr)
				return;

			Node* newnode = new Node(croot->_key);
			root = newnode;
			_constructor(root->_left, croot->_left);
			_constructor(root->_right, croot->_right);
		}

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

			_destructor(root->_left);
			_destructor(root->_right);
			delete root;
			root = nullptr;
		}

		bool _InsertR(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				Node* newnode = new Node(key);
				root = newnode;
				return true;
			}

			if (key < root->_key)
			{
				return _InsertR(root->_left, key);
			}
			else if (key > root->_key)
			{
				return _InsertR(root->_right, key);
			}
			else
			{
				cout << "The key has existed" << endl;
				return false;
			}
		}
		bool _EraseR(Node*& root, const K& key)
		{
			//递归找到要删的结点
			if (root == nullptr)
			{
				cout << "The key does not exist" << endl;
				return false;
			}
				
			if (key < root->_key)
			{
				return _EraseR(root->_left, key);
			}
			else if (key > root->_key)
			{
				return _EraseR(root->_right, key);
			}
			else //找到
			{
				if (root->_left == nullptr) //左为空
				{
					Node* deltemp = root;
					root = root->_right;
					delete deltemp;

					return true;
				}
				else if (root->_right == nullptr) //右为空
				{
					Node* deltemp = root;
					root = root->_left;
					delete deltemp;

					return true;
				}
				else//有两个个孩子时,找左树中的最大元素,交换后再调该函数
				{
					Node*& leftMax = root->_left;
					while (leftMax->_right)
					{
						leftMax = leftMax->_right;
					}
					std::swap(leftMax->_key, root->_key);//交换值

					return _EraseR(leftMax, key);
				}
			}
		}
		Node* _SearchR(Node* root, const K& key)
		{
			if (root == nullptr)
				return nullptr;

			if (key < root->_key)
			{
				return _SearchR(root->_left, key);
			}
			else if(key > root->_key)
			{
				return _SearchR(root->_right, key);
			}
			else
			{
				return root;
			}
		}
		void _InOrderR(Node* root) //测试代码
		{
			if (root == nullptr)
				return;

			_InOrderR(root->_left);
			cout << root->_key << " ";
			_InOrderR(root->_right);
		}
		
        //树根节点指针
		Node* _root;
	};

}

总结

到此,关于二叉搜索树的介绍与实现就结束了
在本文刚开始的时候,我们也提到了一些二叉搜索树的弊端,它最坏的时间复杂度为O(N)。在接下来的文章中将会介绍一些平衡二叉树。相比于二叉搜索树又增加了一些特征,以解决这种漏洞

如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出

如果本文对你有帮助,希望一键三连哦

希望与大家共同进步哦

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何进行正确的 CodeReview

软件开发生命周期中至关重要的一步是代码审查。它使开发人员能够显著提升代码质量。它类似于书籍的创作过程。首先&#xff0c;作者写故事&#xff0c;然后经过编辑以确保不会出现诸如混淆“you’re”和“yours”之类的错误。在这个语境中&#xff0c;代码审查指的是检查和评估…

GPT应用程序的行业应用

GPT&#xff08;Generative Pre-trained Transformer&#xff09;应用程序在各个行业都有广泛的应用潜力&#xff0c;其自然语言生成的能力使其适用于多种场景。以下是一些行业中常见的GPT应用&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件…

【明道云】学习笔记1-了解APaaS

【背景】 APaaS (Application Platform As A Service) &#xff0c;即应用程序平台即服务&#xff0c;这是基于PaaS&#xff08;平台即服务&#xff09;的一种解决方案&#xff0c;支持应用程序在云端的开发、部署和运行&#xff0c;提供软件开发中的基础工具给用户&#xff0…

React Hooks 源码解析:useEffect

React Hooks 源码解析&#xff08;4&#xff09;&#xff1a;useEffect React 源码版本: v16.11.0源码注释笔记&#xff1a;airingursb/react 1. useEffect 简介 1.1 为什么要有 useEffect 我们在前文中说到 React Hooks 使得 Functional Component 拥有 Class Component 的…

三维重建(4)--三维重建与极几何

目录 一、三维重建基础 1、线性解法 2、非线性解法 3、多视图几何的关键问题 二、极几何 1、极几何关系 2、极几何特例 3、本质矩阵 4、本质矩阵的性质 5、基础矩阵 6、基础矩阵的性质 7、基础矩阵的作用 三、基础矩阵估计 1、八点法 2、归一化的八点法 四…

138.随机链表的复制(附带源码)

目录 一、思路分析 二、如何操作 三、源码 深拷贝&#xff1a;原封不动的拷贝一份 一、思路分析&#xff1a; 这一题&#xff0c;偏向于技巧性。如果是按照工科思维硬推&#xff0c;那会非常头大&#xff0c;脑袋瓜疼。 这一题目的核心难点在于&#xff1a;处理random指针…

BP蓝图映射到C++笔记1

教程链接&#xff1a;示例1&#xff1a;CompleteQuest - 将蓝图转换为C (epicgames.com) 1.常用的引用需要记住&#xff0c;如图所示。 2.蓝图中可以调用C函数&#xff0c;也可以实现C函数 BlueprintImplementableEvent:C只创建&#xff0c;不实现&#xff0c;在蓝图中实现 B…

日常常见应用组件升级记录

一、前言 因近期安全扫描&#xff0c;发现java后端应用涉及多个引用组件版本过低&#xff0c;涉及潜在漏洞利用风险&#xff0c;特记录相关处理升级处理过程&#xff0c;以备后续确认&#xff1b; 二、升级处理过程 2.1、Java类应用内置Spring Boot版本升级 Spring Boot是一…

python实现图片式PDF转可搜索word文档[OCR](已打包exe文件)

目录 1、介绍 1.1、痛点 1.2、程序介绍 2、安装方式 2.1、&#x1f53a;必要环节 2.2、脚本安装 2.2.1、不太推荐的方式 2.2.2、节约内存的方式 2.3、⭐完整版安装 3、使用 3.1、最终文件目录 3.2、主程序 3.2.1、绝对路径 3.2.2、是否为书籍 3.2.3、⭐截取区域 …

SpringCloudConfig+SpringCloudBus+Actuator+Git实现Eureka关键配置属性热更新(全程不重启服务)

文章目录 前言1.痛点2.解决方案3.具体实现3.1搭建热配置服务3.2编写配置文件3.3搭建版本控制仓库3.4Eureka-Client引入以下依赖3.5Eureka-Client微服务编写以下配置bootstrap.yml提前加载3.6分别编写测试Controller3.7测试效果3.8下线场景压测 4.SpringCloudBus优化 前言 在上…

【Kafka】Kafka介绍、架构和概念

目录 Kafka介绍Kafka优势Kafka应用场景Kafka基本架构和概念ProducerConsumer/Consumer GroupBrokerZooKeeperTopicPartitionReplicasOffsetsegment Kafka介绍 Kafka是是一个优秀的分布式消息中间件&#xff0c;关于常用的消息中间件对比可参考文章&#xff1a;消息中间件概述。…

Linux用户空间和内核空间所有15种内存分配方法

在Linux操作系统中&#xff0c;内存管理是一个关键的系统功能。用户空间和内核空间分别使用不同的函数来申请内存。以下是用户空间和内核空间内存申请函数的详细列表&#xff1a; Linux用户空间内存申请函数 1. malloc() 函数&#xff1a; void* malloc(size_t size); 用于…

Android OpenGL EGL使用——自定义相机

如果要使用OpenGl来自定义相机&#xff0c;EGL还是需要了解下的。 可能大多数开发者使用过OpengGL但是不知道EGL是什么&#xff1f;EGL的作用是什么&#xff1f;这其实一点都不奇怪&#xff0c;因为Android中的GlSurfaceView已经将EGL环境都给配置好了&#xff0c;你一直在使用…

100天精通Python(实用脚本篇)——第113天:基于Tesseract-OCR实现OCR图片文字识别实战

文章目录 专栏导读1. OCR技术介绍2. 模块介绍3. 模块安装4. 代码实战4.1 英文图片测试4.2 数字图片测试4.3 中文图片识别 书籍分享 专栏导读 &#x1f525;&#x1f525;本文已收录于《100天精通Python从入门到就业》&#xff1a;本专栏专门针对零基础和需要进阶提升的同学所准…

Flowable 生成流程图

/*** 生成流程图** param processId 任务ID*/ RequestMapping("/diagram/{processId}") public void genProcessDiagram(HttpServletResponse response,PathVariable("processId") String processId) {InputStream inputStream flowTaskService.diagram(p…

redis优化系列(六)

本期分享redis内存过期策略&#xff1a;过期key的处理 Redis之所以性能强&#xff0c;最主要的原因就是基于内存存储。然而单节点的Redis其内存大小不宜过大&#xff0c;会影响持久化或主从同步性能。 可以通过修改配置文件来设置Redis的最大内存&#xff1a; maxmemory 1gb …

[翻译] Vulkan-Sample-MSAA (Multisample anti-aliasing)

原文 Aliasing是以低于原始信号采样率的采样率进行采样导致的。在图形学中&#xff0c;这个过程可以描述为&#xff1a;基于一个会产生artifacts的分辨率去计算像素值&#xff0c;从而在模型边缘产生锯齿。多重采样抗锯齿&#xff08;Multisample anti-aliasing&#xff0c;MS…

vivado 接口、端口映射

接口 重要&#xff01;接口只能在“fpga”类型的&#xff1c;component&#xff1e;中定义。接口部分提供了<component>上所有可用物理接口的列表。<interfaces>部分包含嵌套在其中的一个或多个<interface>标记。一个接口是通过使用<port_map>标记由多…

[pytorch入门] 3. torchvision中的transforms

torchvision中的transforms 是transforms.py工具箱&#xff0c;含有totensor、resize等工具 用于将特定格式的图片转换为想要的图片的结果&#xff0c;即用于图片变换 用法 在transforms中选择一个类创建对象&#xff0c;使用这个对象选择相应方法进行处理 能够选择的类 列…

股东出资透明度提升:企业股东出资信息API的应用

前言 在当今商业环境中&#xff0c;股东出资信息的透明度对于投资者、监管机构以及企业自身的健康发展至关重要。随着企业信息公开化的推进&#xff0c;企业股东出资信息API应运而生&#xff0c;为各方提供了一个便捷、高效的信息获取渠道。本文将探讨企业股东出资信息API如何…