【C++】:想知道如何实现互译字典吗?来看二叉搜索树

news2024/9/22 19:46:33

二叉搜索树好文!

文章目录

  • 前言
  • 一、实现搜索二叉树
  • 二、二叉搜索树的应用
    • 1.K模型
    • 2.KV模型
  • 总结


前言

二叉搜索树概念

二叉搜索树又称二叉排序树,它或者是一棵空树 ,或者是具有以下性质的二叉树 :
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
二叉搜索树也被叫做二叉排序树或二插查找树。
二叉搜索树:一颗二叉树,可以为空,如果不为空,则满足一下性质:
1.非空左子树的所有键值小于其根节点的键值
2.非空右子树的所有键值大于其根节点的键值
3.左,右子树都是二叉搜索树
如下图所示:

一、实现搜索二叉树

我们首先要构建搜索二叉树的框架,先用一个命名空间将我们要写的二叉树放进去,我们需要用struct结构体实现一个节点:

template <class K>
	struct TreeNode
	{
		TreeNode<K>* left;
		TreeNode<K>* right;
		K _key;
		TreeNode(const K& key)
			:left(nullptr)
			, right(nullptr)
			, _key(key)
		{

		}
	};

 一个数的节点分为左子树和右子树和节点值,节点值用模板变量使用的时候就可以随意存储任意变量的节点了。同时我们要实现节点的构造函数,构造函数体内不需要实现,只需要在初始化列表把需要初始化的变量初始化即可,当然我们也可以给变量一个缺省参数:

template <class K>
	struct TreeNode
	{
		TreeNode<K>* left;
		TreeNode<K>* right;
		K _key;
		TreeNode(const K& key = K())
			:left(nullptr)
			, right(nullptr)
			, _key(key)
		{

		}
	};

 然后我们实现搜索二叉树的主体,其实很简单我们只需要有一个私有成员变量根节点即可,当然为了使用节点方便我们将节点名重命名一下:

template <class K>
	class BSTree
	{
		typedef TreeNode<K> Node;
	public:

	private:
		Node* _root = nullptr;
	};

 明白了二叉搜索树的概念我们就实现一个二插搜索树,首先我们实现插入功能,因为二叉搜索树的规律是如果插入的节点比当前节点小我就去当前节点的左树寻找,如果插入的节点比当前节点大我就去当前节点的右树查找,直到找到空指针我就将要插入的节点放在此位置,下面我们直接写代码:

bool insert(const K& val)
		{
			if (_root == nullptr)
			{
				_root = new Node(val);
				return true;
			}
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (cur->_key < val)
				{
					parent = cur;
					cur = cur->right;
				}
				else if (cur->_key > val)
				{
					parent = cur;
					cur = cur->left;
				}
				else
				{
					return false;
				}
			}
			if (parent->_key < val)
			{
				parent->right = new Node(val);
			}
			else
			{
				parent->left = new Node(val);
			}
			return true;
		}

首先我们要判断这棵树是否是一颗空树,对一颗空树插入节点只需要给根节点new一个节点即可。当然要记得插入后要返回true,因为我们一次只插入一个节点。接下来我们用一个节点来遍历,遇到插入节点大于当前节点就往当前节点的右子树走,否则就往左子树走,如果发现要插入的节点在数中已经存在那么我们就不进行插入了直接返回false即可。当我们用于遍历的那个节点为空时说明找到要插入的位置了,但是这个时候节点已经从循环中出来了并且为空我们是不能直接连接的,我们需要一个节点来记录遍历节点的前一个位置,然后当遍历节点为空找到合适的位置我们还不能直接插入,因为我们不知道要插入的是在父节点的左子树还是右子树,如下图:

 所以我们在循环完后要先判断要插入的节点是在父节点的左子树还是右子树,然后我们在进行插入,插入成功后返回true即可。

第二个功能我们实现一个中序遍历,这样就可以先测试我们写的插入功能对不对。
 


void Inorder()
		{
			_Inorder(_root);
		}

void _Inorder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			_Inorder(root->left);
			cout << root->_key << " ";
			_Inorder(root->right);
		}

这里我们为什么要用一个函数调用另一个函数呢?因为我们要遍历一棵树是需要根节点的,但是我们要让用户调用的时候应该用对象调用,不再传参了如下图:

 所以我们需要一个无参的函数封装,中序遍历很简单,遇到空树就返回,然后先遍历左子树打印其节点值再遍历右子树,然后在封装的函数中传入根节点即可。

3.删除节点

对于搜索二叉树来讲,删除节点应该是最复杂的,因为要分多种情况,我们一个一个来看:

第一种情况:删除叶子节点

要删除上图中的叶子节点只需要通过搜索二叉树的规律找到其节点,然后将其释放掉,同时要让删除节点的父节点的某个子树为空。

第二种情况:

如果要删像6和14这样的节点该怎么办呢?这样的节点的规律是只有一个子树,所以我们需要将节点删除后将他们的子树托付给他的父节点,比如删除6就需要将7托付给3的右子树,比如说要删14就要将14的左子树托付给10的右子树,因为我们不能确定要删除的节点有左子树还是右子树,并且也不能确定他是他的父节点的左右哪一颗子树,所以需要我们单独判断。

第三种情况:

第三种情况是最复杂的,要删除的节点既有左子树,又有右子树,在这种情况下我们就需要找一个合适的节点来代替要删除的节点的位置,而了解二叉搜索树的规律后我们发现,这个合适的节点只有两个,一个是要删除节点的左子树中的最大值,第二个是要删除节点的右子树中的最小值。比如上图中要删除8可以用左树中的7替换,也可以用右子树中的10替换。再比如要删除3,那就用3的左子树的1替换或者右树中的4替换,而简单的替换也是不行的,我们发现要帮被删除节点带孩子的节点也有可能有自己的孩子,比如说8如果用10来替换,10也有自己的14这个孩子,所以这个时候我们还需要将10的孩子托付给被替换后的节点如下图:

 了解了以上几点我们就可以写代码了:

bool erase(const K& val)
	{
		if (_root == nullptr)
		{
			return false;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_key < val)
			{
				parent = cur;
				cur = cur->right;
			}
			else if (cur->_key > val)
			{
				parent = cur;
				cur = cur->left;
			}
			else
			{
				//找到了,有三种情况,1.要删的是叶子节点 2.要删的节点有一个子树  
                //3.要删的节点有2个子树  叶子节点和要删除的节点有一个子树可以一起判断
				//叶子节点
				//要删除的节点只有一个子树
				if (cur->left == nullptr || cur->right == nullptr)
				{
					if (cur->left)
					{
						//要防止歪脖子数,歪脖子树要删除根节点parent为空
						if (parent == nullptr)
						{
							_root = cur->left;
						}
						else if (parent->left == cur)
						{
							parent->left = cur->left;
						}
						else
						{
							parent->right = cur->left;
						}
					}
					else
					{
						if (parent == nullptr)
						{
							_root = cur->right;
						}
						else if (parent->left == cur)
						{
							parent->left = cur->right;
						}
						else
						{
							parent->right = cur->right;
						}
					}
					delete cur;
					return true;
				}
				else
				{
					//要删除的节点有两个子树,这个时候需要左子树中的最大值或者右子树中的最小 
                    值来托管
					//假如让右子树的最小值托管,右子树的最小值一定没有左子树但是可能有右子 
                    //树,所以需要记录右子树最小值的父亲来托管右子树最小值的右子树
					Node* prevparent = cur;
					Node* tmp = cur->right;
					while (tmp->left)
					{
						prevparent = tmp;
						tmp = tmp->left;
					}
					if (prevparent->left == tmp)
					{
						prevparent->left = tmp->right;
					}
					else
					{
						prevparent->right = tmp->right;
					}
					cur->_key = tmp->_key;
					delete tmp;
					return true;
				}
			}
		}
		return false;
	}

 首先如果这棵树本来就为空那么是无法删除的,返回false即可,然后我们用一个节点遍历二叉树,同时还要用一个节点记录遍历节点走之前的前一个位置也就是找父节点,先去找要删除的节点,找到后有三种情况但是我们发现要删除的节点只有一个节点和叶子节点刚好可以一起判断,所以当左树为空或者右树为空的时候就进循环,进入循环后还要判断要删除的节点到底是有左子树还是有右子树,然后再判断要删除节点是其父节点的左右哪颗子树,只有知道了这个我们才能将要删除节点的孩子正确的托管给父节点。从代码中的注释我们也可以看到,还有一种情况是当删除的节点是根节点并且这棵树要不只有左树要不只有右树,这种树在一开始遍历的时候parent节点就为空,所以当发现是这种情况直接就让根节点变成这个数的左子树或右子树即可。判断完前两种情况后我们就判断当要删除的节点左右子树都有的情况,我们以用右子树的最小值为例,这种情况有个特例:

 当要删除8这个节点的时候,本来找右子树的最小值托管是找到其右子树然后一直往这棵树的左子树递归即可,然后还要有一个节点记录递归的最小子树的父节点,找到父节点让其托管右子树最小节点的右子树,但是如上图所示会有无左子树的情况,这种情况下prevparent如果初始化为空指针就会对空指针进行解引用,所以要将这个节点初始化为要删除的节点,这样才能解决上图中删8的情况。最后在托管的时候一定要判断右子树的最小值是其父节点的左子树还是右子树,这些条件都判断完后把要删除的节点释放即可。下面还有另一种实现删除的代码,大家容易理解哪个就写哪个:

bool erase(const K& val)
		{
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (cur->_key < val)
				{
					parent = cur;
					cur = cur->right;
				}
				else if (cur->_key > val)
				{
					parent = cur;
					cur = cur->left;
				}
				else
				{
					//找到要删除的节点
					if (cur->left == nullptr)
					{
						if (parent == nullptr)
						{
							_root = cur->right;
							return true;
						}
						if (parent->left == cur)
						{
							parent->left = cur->right;
						}
						else
						{
							parent->right = cur->right;
						}
						delete cur;
						return true;
					}
					else if (cur->right == nullptr)
					{
						if (parent == nullptr)
						{
							_root = cur->left;
							return true;
						}
						if (parent->left == cur)
						{
							parent->left = cur->left;
						}
						else
						{
							parent->right = cur->left;
						}
						delete cur;
						return true;
					}
					else
					{
						//要删除的节点有两个子树,需要右树的最小节点或者左树的最大节点托管
						Node* minRight = cur->right;
						Node* PrevParent = cur;
						while (minRight->left)
						{
							PrevParent = minRight;
							minRight = minRight->left;
						}
						cur->_key = minRight->_key;
						if (PrevParent->left == minRight)
						{
							PrevParent->left = minRight->right;
						}
						else
						{
							PrevParent->right = minRight->right;
						}
						delete minRight;
						return true;
					}
				}
			}
			return false;
		}

4.查找

下面我们实现查找函数:

Node* find(const K& val)
		{
			Node* tmp = _find(_root, val);
			return tmp;
		}
Node* _find(Node* root, const K& val)
		{
			Node* cur = root;
			while (cur)
			{
				if (cur->_key < val)
				{
					cur = cur->right;
				}
				else if (cur->_key > val)
				{
					cur = cur->left;
				}
				else
				{
					return cur;
				}
			}
			return  nullptr;
		}

查找同样需要封装一下在使用,在实现查找函数的时候需要注意当根节点为空时返回nullptr即可。

5.用递归实现查找函数

Node* findR(const K& val)
		{
			Node* tmp = _findR(_root, val);
			return tmp;
		}

Node* _findR(Node* root, const K& val)
		{
			if (root == nullptr)
			{
				return nullptr;
			}
			if (root->_key == val)
			{
				return root;
			}
			if (root->_key < val)
			{
				return _findR(root->right, val);
			}
			else
			{
				return _findR(root->left, val);
			}
		}

递归实现起来其实代码会更简洁,当遇到空就返回空,找到了就返回节点,如果当前节点小于要查找的节点就递归当前节点的右子树查找,否则就去左子树查找。

6.用递归实现插入函数

bool insertR(const K& val)
		{
			bool result = _insertR(_root, val);
			return result;
		}
bool _insertR(Node*& root, const K& val)
		{
			if (root == nullptr)
			{
				root = new Node(val);
				return true;
			}
			if (root->_key < val)
			{
				return _insertR(root->right, val);
			}
			else if (root->_key > val)
			{
				return _insertR(root->left, val);
			}
			else
			{
				return false;
			}
		}

插入函数就是当要插入的节点大于当前节点就递归当前节点的右子树,否则就是左子树,当节点相等我们不插入返回false,等到当前节点为空我们就给当前节点new一个节点,插入成功后返回true即可,下面我们画个递归图解释为什么要用引用:

 7.用递归实现删除函数

bool eraseR(const K& val)
		{
			bool result = _eraseR(_root, val);
			return result;
		}
bool _eraseR(Node*& root, const K& val)
		{
			if (root == nullptr)
			{
				return false;
			}
			if (root->_key < val)
			{
				return _eraseR(root->right, val);
			}
			else if (root->_key > val)
			{
				return _eraseR(root->left, val);
			}
			else
			{
				Node* del = root;
				if (root->left == nullptr)
				{
					root = root->right;
				}
				else if (root->right == nullptr)
				{
					root = root->left;
				}
				else
				{
					Node* maxLeft = root->left;
					while (maxLeft->right)
					{
						maxLeft = maxLeft->right;
					}
					std::swap(root->_key, maxLeft->_key);
					return _eraseR(root->left, val);
				}
				delete del;
				return true;
			}
		}

递归版的删除函数大致思想还是与我们刚刚的一样,只不过当我们要删除的时候情况变了,当要删除的节点左子树为空我们就将当前节点变成当前节点的右子树(因为等会会将这个要删除的节点释放),当要删除的节点的右子树为空我们就将当前节点变成当前节点的左子树。当要删除的节点两个子树都不为空时,我们以用左树的最大值托管为例,先创建一个节点为当前节点的左子树节点,然后让这个节点去遍历右子树,因为左子树的最大值一定在左子树的右子树中,遍历完直接将左子树的最大值和要删除的节点的值交换,因为已经交换了如下图:

所以我们直接递归7的左子树删除这个为8的节点即可,在递归版需要注意的是节点一定要用引用,下面我们画个递归图解释:

 8.析构函数:

~BSTree()
		{
			Destruct(_root);
		}

void Destruct(Node*& root)
		{
			if (root == nullptr)
			{
				return;
			}
			Destruct(root->left);
			Destruct(root->right);
			delete root;
			root = nullptr;
		}

 析构函数的实现就很简单了,当遇到空就返回,否则就先递归左子树再递归右子树,最后是否root节点。也就是说整体就是一个后序遍历从最后一个节点开始删除,这里用引用的作用是当我们最后将root节点置为nullptr那么_root根节点也会被置为空。因为root是_root的别名。

9.构造函数

BSTree()
			:_root(nullptr)
		{

		}

构造函数只需要将根节点置为空即可。这里必须要实现的原因是等会要实现拷贝构造函数,如果我们不写构造函数当我们实现拷贝构造就不会生成默认的构造函数了。

10.拷贝构造函数

BSTree(const BSTree<K,V>& t)
		{
			_root = copy(t._root);
		}

Node* copy(Node* root)
		{
			if (root == nullptr)
			{
				return nullptr;
			}
			Node* newroot = new Node(root->_key);
			newroot->left = copy(root->left);
			newroot->right = copy(root->right);
			return newroot;
		}

拷贝构造的实现我们只需要用一个函数去用前序遍历依次创建节点即可,(默认的拷贝构造是浅拷贝)当节点为空就返回空指针,然后创建与一个newroot节点,此节点用传来的root节点的值初始化,然后分别递归建立左子树和右子树,最后返回新树即可。

11.赋值重载函数

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

赋值重载函数我们直接用现代写法,传参用传值传参,这样会拷贝构造一个树,然后我们直接交换这个被拷贝构造的数的根节点和我们的this指针指向的节点,然后返回即可,这里记得返回要引用返回否则还会多调用一次拷贝构造。

以上就是二叉搜索树的全部函数接口实现,下面我们讲讲二插搜索树的应用。

二、二叉搜索树的应用

1.K模型

. K 模型: K 模型即只有 key 作为关键码,结构中只需要存储 Key 即可,关键码即为需要搜索到
的值
比如: 给一个单词 word ,判断该单词是否拼写正确 ,具体方式如下:
以词库中所有单词集合中的每个单词作为 key ,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

2.KV模型

KV 模型:每一个关键码 key ,都有与之对应的值 Value ,即 <Key, Value> 的键值对 。该种方
式在现实生活中非常常见:
比如 英汉词典就是英文与中文的对应关系 ,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文 <word, chinese> 就构成一种键值对;
再比如 统计单词次数 ,统计成功后,给定单词就可快速找到其出现的次数, 单词与其出
现次数就是 <word, count> 就构成一种键值对。
对于KV模型我们给出以下两个例子:
1.输入单词,查找单词对应的中文翻译:
namespace key_value
{
	template <class K,class V>
	struct TreeNode
	{
		TreeNode<K,V>* left;
		TreeNode<K,V>* right;
		K _key;
		V _value;
		TreeNode(const K& key,const V& value)
			:left(nullptr)
			, right(nullptr)
			, _key(key)
			,_value(value)
		{

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

		}
		BSTree(const BSTree<K,V>& t)
		{
			_root = copy(t._root);
		}
		BSTree<K,V>& operator=(BSTree<K,V> t)
		{
			std::swap(_root, t._root);
			return *this;
		}
		~BSTree()
		{
			Destruct(_root);
		}
		bool insert(const K& val, const V& value)
		{
			if (_root == nullptr)
			{
				_root = new Node(val,value);
				return true;
			}
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (cur->_key < val)
				{
					parent = cur;
					cur = cur->right;
				}
				else if (cur->_key > val)
				{
					parent = cur;
					cur = cur->left;
				}
				else
				{
					return false;
				}
			}
			if (parent->_key < val)
			{
				parent->right = new Node(val,value);
			}
			else
			{
				parent->left = new Node(val,value);
			}
			return true;
		}
		bool erase(const K& val)
		{
			assert(_root != nullptr);
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (cur->_key < val)
				{
					parent = cur;
					cur = cur->right;
				}
				else if (cur->_key > val)
				{
					parent = cur;
					cur = cur->left;
				}
				else
				{
					//找到要删除的节点
					if (cur->left == nullptr)
					{
						if (parent == nullptr)
						{
							_root = cur->right;
							return true;
						}
						if (parent->left == cur)
						{
							parent->left = cur->right;
						}
						else
						{
							parent->right = cur->right;
						}
						delete cur;
						return true;
					}
					else if (cur->right == nullptr)
					{
						if (parent == nullptr)
						{
							_root = cur->left;
							return true;
						}
						if (parent->left == cur)
						{
							parent->left = cur->left;
						}
						else
						{
							parent->right = cur->left;
						}
						delete cur;
						return true;
					}
					else
					{
						//要删除的节点有两个子树,需要右树的最小节点或者左树的最大节点托管
						Node* minRight = cur->right;
						Node* PrevParent = cur;
						while (minRight->left)
						{
							PrevParent = minRight;
							minRight = minRight->left;
						}
						cur->_key = minRight->_key;
						if (PrevParent->left == minRight)
						{
							PrevParent->left = minRight->right;
						}
						else
						{
							PrevParent->right = minRight->right;
						}
						delete minRight;
						return true;
					}
				}
			}
			return false;
		}
		Node* find(const K& val)
		{
			Node* tmp = _find(_root, val);
			return tmp;
		}
		void Inorder()
		{
			_Inorder(_root);
		}
	protected:
		Node* copy(Node* root)
		{
			if (root == nullptr)
			{
				return nullptr;
			}
			Node* newroot = new Node(root->_key);
			newroot->left = copy(root->left);
			newroot->right = copy(root->right);
			return newroot;
		}
		void Destruct(Node*& root)
		{
			if (root == nullptr)
			{
				return;
			}
			Destruct(root->left);
			Destruct(root->right);
			delete root;
			root = nullptr;
		}
		Node* _find(Node* root, const K& val)
		{
			Node* cur = root;
			while (cur)
			{
				if (cur->_key < val)
				{
					cur = cur->right;
				}
				else if (cur->_key > val)
				{
					cur = cur->left;
				}
				else
				{
					return cur;
				}
			}
			return  nullptr;
		}
		void _Inorder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			_Inorder(root->left);
			cout << root->_key << ":"<<root->_value<<" ";
			_Inorder(root->right);
		}

	private:
		Node* _root = nullptr;
	};
}
void test5()
{
	key_value::BSTree<string, string> dict;
	dict.insert("sort", "排序");
	dict.insert("insert", "插入");
	dict.insert("left", "左边");
	dict.insert("right", "右边");
	dict.insert("erase", "删除");
	string str;
	while (cin >> str)
	{
		auto ret = dict.find(str);
		if (ret)
		{
			cout << ":"<<ret->_value << endl;
		}
		else
		{
			cout << "找不到此单词" << endl;
		}
	}
}

我们看看上述代码的结果:

KV的实现只需要添加一个模板参数,修改遍历中序打印函数和插入函数,插入的时候我们需要将新参数也插入,我们在节点中初始化的时候记得初始化新的参数即可。

简单的中文互译我们就搞定了,再看看统计次数的例子 。

2.统计水果出现的次数:
void test6()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
	key_value::BSTree<string, int> ct;
	for (auto& ch : arr)
	{
		auto ret = ct.find(ch);
		if (ret)
		{
			ret->_value++;
		}
		else
		{
			ct.insert(ch, 1);
		}
	}
	ct.Inorder();
}

统计次数很简单,当数组中的水果在树中存在我们就让水果的次数++,如果不存在我们就将这个水果插入数中,最后打印出来的顺序是按照ascll码进行比较的,因为中文的底层是ascll。 


总结

二叉搜索树的性能分析:

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有 n 个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二
叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树。
最优情况下,二叉搜索树为完全二叉树 ( 或者接近完全二叉树 ) ,其平均比较次数为:O(log N)
最差情况下,二叉搜索树退化为单支树 ( 或者类似单支 ) ,其平均比较次数为:O(N)

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

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

相关文章

Orcale中OCILogon和OCIServerAttach登录方式的区别分析

参考文档《Oracle Call Interface Programmers Guide》 在Orcale和DM数据库提供的API中&#xff0c;通过OCI方式接口连接数据库的方法有多个&#xff0c;这里只讨论OCILogon和OCIServerAttach的比较。 1、官方描述 根据文档里的描述&#xff1a; OCILogon():This function is…

DJ4-5 路由和选路

目录 一、路由与转发的相互作用 二、路由的基本概念 1. 默认路由器 2. 路由算法 三、网络的抽象模型 1. 节点图 2. 费用 Cost 四、路由算法分类 1. 静态路由算法 2. 动态路由算法 3. 全局路由算法 4. 分布式路由算法 一、路由与转发的相互作用 二、路由的基本概念 …

美团赴抖音之“约”:让本地生活补贴大战来得更猛烈些?

面对抖音在本地生活领域的强势挑战&#xff0c;美团似乎准备好了正面迎战。 近期&#xff0c;美团动作频频。最开始&#xff0c;美团在美团App美食页面下的“特价团购”打出“限时补贴&#xff0c;全网低价”的口号。对此&#xff0c;一位行业人士分析称&#xff0c;“之前美团…

java commons-io 工具类的使用

commons-io是第三方程序员编写的工具类&#xff0c;并不是java本身带的方法。是在java提供的工具类基础上&#xff0c;开发的工具类。简化了代码的用法&#xff0c;可以提升开发效率。 用法 1.下载jar包 2.在程序中新建lib目录&#xff0c;把jar包放进去 3.在jar包上右键&…

learn_C_deep_6 (布尔类型、布尔与“零值“、浮点型与“零值“、指针与“零值“的比较)

目录 语句和表达式的概念 if语句的多种语法结构 注释的便捷方法&#xff08;环境vs&#xff09; if语句执行的过程 逻辑与&& 逻辑或|| 运算关系的顺序 else的匹配原则 C语言有没有布尔类型 C99标准 sizeof(bool)的值为多少&#xff1f; _Bool原码 BOOL…

音视频八股文(6)-- ffmpeg大体介绍和内存模型

播放器框架 常用音视频术语 • 容器&#xff0f;文件&#xff08;Conainer/File&#xff09;&#xff1a;即特定格式的多媒体文件&#xff0c; 比如mp4、flv、mkv等。 • 媒体流&#xff08;Stream&#xff09;&#xff1a;表示时间轴上的一段连续数据&#xff0c;如一 段声音…

dubbogo如何实现路由规则功能

dubbo-go中如何实现路由规则功能 路由规则&#xff08; routing rule &#xff09;是为了改变网络流量所经过的途径而修改路由信息的技术&#xff0c;主要通过改变路由属性&#xff08;包括可达性&#xff09;来实现。在发起一次 RPC 调用前起到过滤目标服务器地址的作用&…

Node第三方包 【node-xlsx】

文章目录 &#x1f31f;前言&#x1f31f;node-xlsx&#x1f31f;安装&#x1f31f;导出xlsx文件&#x1f31f;解析xlsx文件&#x1f31f;另外&#xff1a;其他支持读写Excel的Node.js模块有&#xff1a;&#x1f31f;直接导出excel文件 &#x1f31f;写在最后 &#x1f31f;前…

麒麟信安联合主办 | openEuler Developer Day 2023召开 openEuler全场景走向深入

【中国&#xff0c;上海&#xff0c;2023年4月21日】openEuler Developer Day 2023于4月20-21日在线上和线下同步举办。本次大会由开放原子开源基金会、中国软件行业协会、openEuler社区、边缘计算产业联盟共同主办&#xff0c;以“万涓汇流&#xff0c;奔涌向前”为主题&#…

【FTP工具】- Win10下免费的FTP服务器搭建 - FileZilla 的下载、安装、使用

目录 一、概述二、下载、安装2.1 下载2.2 安装 三、FileZilla服务器的使用3.1 连接服务器3.2 配置用户权限 四、在windows访问该Ftp服务器4.1 查看Ftp服务器IP4.2 访问Ftp服务器 一、概述 FileZilla服务器是一个免费的开源FTP和FTPS服务器&#xff0c;是根据GNU通用公共许可证条…

HTB靶机03-Shocker-WP

Shocker scan 2023-03-30 23:22 ┌──(xavier㉿xavier)-[~/Desktop/Inbox] └─$ sudo nmap -sSV -T4 -F 10.10.10.56 Starting Nmap 7.91 ( https://nmap.org ) at 2023-03-30 23:22 HKT Nmap scan report for 10.10.10.56 Host is up (0.40s latency). Not shown: 99 clos…

QT-day(2)-(常用类、信号与槽.....)

题目&#xff1a;编写一个登录功能的界面&#xff0c;在登录框项目中&#xff0c;将登陆按钮发射的tclicked信号&#xff0c;连接到自定义的槽函数中&#xff0c;在槽函数中&#xff0c;判断u界面输入的用户名是否为"admin ,密码是否为"123456如果验证成功&#xff0…

数据结构_时间复杂度/空间复杂度

目录 1. 数据结构在学什么 2. 数据结构的基本概念 3. 算法和算法评价 3.1 算法的基本概念 3.2 算法的特征 3.3 算法效率的度量 3.3.1 时间复杂度 3.3.2 空间复杂度 1. 数据结构在学什么 常言道&#xff1a;学以致用&#xff1b;学习完基本C语言程序&#xff0c;我们希…

AB数对 码蹄集

题目来源&#xff1a;码蹄集 题目描述&#xff1a; 大致思路&#xff1a; 遍历输入的n个整数&#xff0c;将每个数存入哈希表中&#xff0c;key为数值&#xff0c;value为该数出现的次数。 再次遍历这n个整数&#xff0c;对于每个数x&#xff0c;计算出x-C和xC的值&#xff0…

Go语言基础----Go语言简介

【原文链接】Go语言基础----Go语言简介 一、Go语言简介 Go语言&#xff0c;又称Golang&#xff0c;是Google公司的Robert Griesemer&#xff0c;Rob Pike 及 Ken Thompson开发的一种静态强类型、编译型的语言。Go语言语法和C语言接近&#xff0c;但是功能上内存安全&#xff…

【Python】lambda匿名函数

文章目录 前言lambda匿名函数的定义lambda匿名函数的使用使用lambda匿名函数写一个计算器总结 前言 在Python中,可以使用def 关键字定义函数,使用def定义的关键字是有名称的,在调用时可以重复使用.还有一种是使用lambda关键字进行函数的定义,这个方式定义的函数是匿名函数,只能…

51单片机(四)静态数码管和动态数码管显示

❤️ 专栏简介&#xff1a;本专栏记录了从零学习单片机的过程&#xff0c;其中包括51单片机和STM32单片机两部分&#xff1b;建议先学习51单片机&#xff0c;其是STM32等高级单片机的基础&#xff1b;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 &#xff1a;适用于想要…

内存优化-比glibc更快的tcmalloc

TCMalloc 是 Google 开发的内存分配器&#xff0c;在不少项目中都有使用&#xff0c;例如在 Golang 中就使用了类似的算法进行内存分配。它具有现代化内存分配器的基本特征&#xff1a;对抗内存碎片、在多核处理器能够 scale。据称&#xff0c;它的内存分配速度是 glibc2.3 中实…

3d网上渲染平台是怎么渲图的_云渲染流程详解!

题主说的看到许多网友对‘’3d网上渲染平台是怎么渲图的‘’进行提问&#xff0c;瑞云渲染小编也提供自己的小小见解。针对3D网上渲染平台是指什么&#xff0c;实际应该是指云渲染农场。几十年来&#xff0c;随着计算机软硬件不断更迭&#xff0c;图形图像渲染的效果更加清晰、…

信号完整性分析基础知识之传输线和反射(二):阻性负载的反射,源端阻抗,弹跳图

传输线的端接需要考虑三种重要的特殊情况&#xff0c;每种情况中&#xff0c;传输线的特性阻抗均为50Ohm。信号将从源端在这条传输线上传播&#xff0c;并以特定的阻抗端接到达远端。 TIP:在时域中&#xff0c;信号对瞬时阻抗十分敏感&#xff0c;第二区域并不一定是一条传输线…