【C++】哈希表特性总结及unordered_map和unordered_set的模拟实现

news2024/9/21 10:50:27

作者阿润菜菜
📖专栏C++


文章目录

  • 前言
  • 一、哈希表的特性 - 哈希函数和哈希冲突
    • 1 哈希函数
    • 2. 哈希冲突
  • 二、闭散列的实现 -- 开放地址法
    • 1. 定义数据结构
    • 2.insert()
    • 3.Find()
    • 4. Erase()
    • 5.仿函数处理key值不能取模无法映射 --- BKDRHash
  • 三、开散列的实现 --- 链地址法(哈希桶)
    • 1. 定义框架结构
    • 2.insert()
    • 3.Find()
    • 4.Erase()
  • 四、封装实现unordered系列容器
    • 1.迭代器设计


前言

  • unordered系列关联式容器是C++11中新增的一类容器,包括unordered_mapunordered_setunordered_multimapunordered_multiset
  • 它们的底层实现是哈希表,可以快速地查找和插入元素,时间复杂度为O(1)。
  • 它们的元素是无序的,因此遍历时元素的顺序是不确定的。
  • 它们的使用方式和红黑树结构的关联式容器(如map和set)基本类似,只是需要包含不同的头文件(<unordered_map>或<unordered_set>)。
  • 它们支持直接访问操作符(operator[]),可以使用key作为参数直接访问value。
  • 哈希最大的作用就是查找(效率很高的),哈希并不具有排序的功能,unordered_map和unordered_set仅仅只有去重的功能而已

一、哈希表的特性 - 哈希函数和哈希冲突

  • 哈希表是一种数据结构,它提供了快速的插入操作和查找操作,无论哈希表中有多少条数据,插入和查找的时间复杂度都是为O(1)
  • 哈希表是通过把关键码值映射到表中一个位置来访问记录,这个映射函数叫做散列函数哈希函数
  • 哈希表的元素是无序的,因为散列函数的映射结果是随机的。
  • 哈希表可能会产生碰撞,也叫哈希冲突,就是不同的关键码值映射到同一个位置,这时就需要采用一些方法来解决碰撞,比如开放地址法链表法,同时

1 哈希函数

  1. 直接定址法–(常用)
    在这里插入图片描述
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B,常用的A是1,B是0。
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况,若分布较广,则空间消耗比较高。
    使用场景:适合查找比较小且连续的情况
  2. 除留余数法–(常用)
    在这里插入图片描述
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
    按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址,一般不这么干,最常用的就是拿vector.size()作为除数,每次扩容将vector.size()扩容二倍。但后面开散列的解决方式那里,我们会仿照库,用质数的集合作为vector.size(),然后用其作为除数。

2. 哈希冲突

当多个关键码key在通过哈希函数映射之后,得到了相同的哈希地址,也就是多个key映射到同一个位置上时,这种现象称为哈希冲突或哈希碰撞解决哈希冲突的办法一般为两种,一种是闭散列的方式解决,即用线性探测或二次探测的方式向后寻找空的哈希位置,一种是开散列的方式解决,即将哈希冲突的元素通过单链表链接,逻辑上像哈希表挂了一个个的桶,所以这样的解决方式也可称为链地址法,或哈希桶方式
在这里插入图片描述
区别概念介绍一下闭散列和开散列

  • 开散列和闭散列都是解决哈希冲突的方法,也就是当不同的关键码值映射到同一个位置时,如何处理的问题
  • 开散列方法又叫链地址法,它是把发生冲突的关键码存储在散列表主表之外,每个位置对应一个链表,链表的头节点存储在主表中。这样,当查找一个关键码时,先通过散列函数得到其位置,然后在对应的链表中进行查找。
  • 闭散列方法又叫开放地址法,它是把发生冲突的关键码存储在主表中另一个槽内。这样,当查找一个关键码时,如果发现其位置已经被占用,就按照一定的规则寻找下一个空闲的位置,直到找到或者遍历整个表。
  • 开散列和闭散列的区别是:
    • 开散列需要额外的空间来存储链表节点,而闭散列不需要;
    • 开散列可以容纳任意多的元素,而闭散列的容量有限;
    • 开散列的查找效率取决于链表的长度,而闭散列的查找效率取决于探测规则;
    • 开散列更适合关键码值分布不均匀的情况,而闭散列更适合关键码值分布均匀且空间紧张的情况。

二、闭散列的实现 – 开放地址法

1. 定义数据结构

闭散列的实现,我们以键值作为存储元素来讲解。
我们采用vector作为底层容器,用vector来存储哈希结点,哈希结点是一个结构体,其中存储键值对和状态值,_state用于标定哈希映射位置为空、存在、删除三种状态。
在这里插入图片描述
同时为了判断什么时候进行哈希表的扩容,在hashTable类中多增加了一个无符号整型的_n变量,表示当前哈希表中存储数据的个数,方便我们用数据个数和vector.size()作除法,看结果是否大于负载因子,如果大于则扩容,如果不大于则继续插入。

enum state
{
	EMPTY,
	EXIST,
	DELETE
};
template <class K, class V>
struct HashNode
{
	HashNode()
		: _state(EMPTY)
	{}
	HashNode(const pair<K, V>& kv)
		:_kv(kv)
		, _state(EMPTY)
	{}
	pair<K, V> _kv; //数据
	enum state _state;  //状态
};
//......
};

2.insert()

负载因子
哈希表冲突越多,效率越低
若表中位置都满了,就需要扩容 ,我们利用负载因子进行判断何时扩容

负载因子的概念
负载因子 = 填入表的元素个数 / 表的长度
表示 表储存数量的百分比

填入表的元素个数 越大,表示冲突的可能性越大,
填入表的元素个数 越小,表示冲突的可能性越小
所以在开放定址法时,应该控制在0.7-0.8以下,超过就会扩容

线性探测
哈希表的线性探测原理是一种解决哈希冲突的方法,它的基本思想是:当发生哈希冲突时,就从当前位置开始,顺序查找下一个空闲的位置,然后将数据插入到该位置。

例如,如果我们要将数据 88 插入到哈希表中,经过哈希函数计算得到的数组下标是 16 ,但是在数组下标为 16 的位置已经有其他元素了,那么就继续查找 17 , 18 ,直到找到一个空闲的位置,然后将 88 插入到该位置。


在实现扩容时,我们进行代码复用,我们不再新建立vector,而是新建立一个哈希表,对新哈希表中的vector进行扩容,然后调用哈希表的Insert函数,将原vector中的键值对的关键码插入到新哈希表当中,这样就不需要自己在写代码,进行代码复用即可。最后将新哈希表中的vector和原哈希表的vector进行swap即可,这样就完成了原有数据到新表中的挪动,然后再插入要插入的kv即可。

bool Insert(const pair<K, V>& kv)
{
	if (Find(kv.first))
			return false;
	
	//大于标定的负载因子,进行扩容,降低哈希冲突的概率
	if (_n * 10 / _tables.size() > 7)//可能会出现除0错误
	{
		//旧表数据,重新计算,映射到新表
		/*vector<Node> newtables;
		newtables.resize(2 * _tables.size());	*/

		HashTable<K, V, BKDRHash<K>> newHT;
		newHT._tables.resize(2 * _tables.size());
		for (auto& e : _tables)
		{
			if (e._state == EXIST)
			{
				newHT.Insert(e._kv);
				//取原表中的数据插入到新表的vector里面,键值对之间发生赋值重载。因为newHT是新开的初始化好的哈希表
				//递归通常是自己调用自己,这里不是递归,仅仅是代码复用而已。
			}
		}

		_tables.swap(newHT._tables);
	}

	size_t hashi = Hash()(kv.first) % _tables.size();//这里不能%capacity,某些位置不是可用的,vector[]会对下标检查
	while (_tables[hashi]._state == EXIST)
	{
		//线性探测
		++hashi;
		//二次探测
		//hashi = hashi + i * i;//降低冲突概率,但还是有可能会冲突,占其他位置

		hashi %= _tables.size();
	}

	/*_tables[hashi] = Node(kv);
	_tables[hashi]._state = EXIST;*/
	//在构造新表对象时,默认构造已经初始化好哈希表里面的结点空间了,你再开空间拷贝数据浪费。
	_tables[hashi]._kv = kv;
	_tables[hashi]._state = EXIST;
	++_n;

	return true;
}

3.Find()

查找的思想非常简单,我们首先利用要查找的key值求出映射的哈希地址,如果当前位置的状态为存在或者删除,则继续找,若在循环中找到了,则返回对应位置的地址,若没找到则返回nullptr,遇见空则结束查找。

在线性探测中,如果查找到尾部了,则让hashi%=vector的size即可,让hashi回到开头的位置。但有一种极端特殊情况,就是边插入边删除,这样整个哈希表中的结点状态有可能都是delete或exist,则在线性探测中不会遇到empty,while会陷入死循环,所以在while里面多加一层判断,如果start等于hashi,说明在哈希表中已经线性探测一圈了,那此时就返回,因为找了一圈都没找到key,那就说明key不在哈希表里面。

Node* Find(const K& key)
{
	size_t hashi = Hash()(key) % _tables.size();
	size_t start = hashi;
	while (_tables[hashi]._state != EMPTY)
	{
		if (_tables[hashi]._kv.first == key && _tables[hashi]._state == EXIST)
		{
			return &_tables[hashi];
		}

		++hashi;
		hashi %= _tables.size();//防止越界
		if (start == hashi)
			break;
	}
	return nullptr;
}

4. Erase()

大部分数据结构容器的删除其实都是伪删除或者叫做惰性删除,因为我们无法做到释放一大块空间的某一部分空间,所以在数据结构这里的删除基本都是用标记的伪删除 ,哈希表的删除也一样,我们在每个结点里面增加一个状态标记,用状态来标记当前结点是否被删除。如果删除结点不存在,则返回false。
在这里插入图片描述

bool Erase(const K& key)
{
	Node* ret = Find(key);
	if (ret == nullptr)
		return false;

	ret->_state = DELETE;
	--_n;
	return true;
}

5.仿函数处理key值不能取模无法映射 — BKDRHash

上面代码中,对于整型数据可以完成key值取模映射,那如果我们的数据是string类型,怎么解决?string如何对vector的size取模呢?此时就需要仿函数来完成自定义类型转换为整型的操作了,只有转换为整型,我们才能取模,进而才能完成哈希映射的工作。
对于其他类型,比如int,char,short,double等,我们直接强转为size_t,这样就可以完成哈希映射。

字符串转换为整型的场景还是比较常见的,网上有很多关于字符串哈希的算法,我们取最优的算法,思路就是将每一个字符对应的ascll码分别拆下来,每次的hash值都为上一次的hash值×131后再加上字符的ascll码值,遍历完字符串后,最后的hash为字符串转成整型的结果,这样每个字符串转换后的整型是极大概率不重复的,是一个非常不错的哈希算法,被人们称为BKDRHash。

template <class K>
struct BKDRHash
{
	size_t operator()(const K& key)
	{
		return (size_t)key;//只要这个地方能转成整型,那就可以映射,指针浮点数负数都可以,但string不行
	}
};
template <>
struct BKDRHash<string>
{
	size_t operator()(const string& key)
	{
		//return key[0];//字符串第一个字符是整型,那就可以整型提升,只要是个整型能进行%模运算,完成映射即可。

		size_t hash = 0;
		for (auto ch : key)
		{
			hash = hash * 131 + ch;
		}
		return hash;
	}
};

三、开散列的实现 — 链地址法(哈希桶)

开散列的哈希表是最常用的方式,库里面的unordered_map和unordered_set用的也是哈希桶的方式实现的,我们模拟实现的哈希桶也仿照库实现,哈希结点node里面存储键值对和下一个结点指针。

1. 定义框架结构

在哈希表的模板参数中,也多加了一个缺省仿函数类的参数,也就是Hash,因为我们需要Hash的仿函数对象或匿名构造,将key转成整型。

template <class K, class V>
struct hashNode
{
	hashNode(const pair<K,V>& kv)
		:_kv(kv)
		,_next(nullptr)
	{}

	pair<K, V> _kv;
	hashNode<K, V>* _next;
};
template <class K, class V, class Hash = BKDRHash<K>>
class hashTable
{
public:
	typedef hashNode<K, V> Node;

	…………省略
private:
	vector<Node*> _table;
	size_t _n;
};

对于哈希桶,我们必须写出析构函数,因为编译器默认生成的析构函数会调用vector的析构,而vector的析构仅仅只能将自己的空间还给操作系统,如果某些节点指针指向了具体的节点,则只归还vector的空间是不够的,还需要归还那些申请的节点空间。
所以需要遍历每一个哈希桶,将每一个桶里面的节点都还给操作系统,这里就用到单链表的节点删除的知识了,在删除前需要保留下一个位置,要不然delete归还空间之后就找不到下一个节点的位置了。

2.insert()

为什么进行头插
对单链表进行尾插,因为尾插还需要找尾,那就需要遍历桶,这样的效率太低,并且桶中也不要求次序什么的,所以我们直接进行头插即可,头插的效率很高,因为映射找到哈希地址之后即可进行头插。

  1. 哈希桶的负载因子,官方默认值为1.0,那就是_n和vector.size()相等的时候进行扩容,扩容的目的还是重新建立映射关系,缓解哈希冲突,因为如果某一个哈希桶的结点个数过多,在哈希映射之后还需要遍历哈希桶寻找结点,会降低哈希查找的效率,所以扩容就是多增加哈希桶的个数,减少平均哈希桶中结点的个数,提高哈希查找的效率。
    2.注意我们遍历原表的每个结点指针,将每个指针指向结点的key重新计算哈希映射关系,头插到新的vector里面,在每完成一个桶的重新映射关系后,将原vector中的桶位置的指针置为空,否则析构的时候,结点会被析构两遍。等到原表的所有结点遍历完之后,将新的vector和原来的vector一交换即可,临时对象_newtable在离开函数栈帧时会被销毁,调用vector的默认析构完成空间的归还即可

研究表明,每次除留余数法最好模一个素数,这会大概率降低哈希冲突的可能性。所以我们下面的扩容大小每次挑选小于2倍的最大素数作为扩容后的vector大小,这里复用了一下stl库里面的素数表。

inline unsigned long __stl_next_prime(unsigned long n)
{
	static const int __stl_num_primes = 28;
	static const unsigned long __stl_prime_list[__stl_num_primes] =
	{
	  53,         97,         193,       389,       769,
	  1543,       3079,       6151,      12289,     24593,
	  49157,      98317,      196613,    393241,    786433,
	  1572869,    3145739,    6291469,   12582917,  25165843,
	  50331653,   100663319,  201326611, 402653189, 805306457,
	  1610612741, 3221225473, 4294967291
	};

	for (size_t i = 0; i < __stl_num_primes; i++)
	{
		if (__stl_prime_list[i] > n)
		{
			return __stl_prime_list[i];
		}
	}

	return __stl_prime_list[__stl_num_primes - 1];
}
bool Insert(const pair<K, V>& kv)
{
	if (Find(kv.first))//不允许重复元素
		return false;

	//负载因子控制在1,超过就扩容
	if (_n == _table.size())
	{
		vector<Node*> _newtable;
		_newtable.resize(__stl_next_prime(_table.size()), nullptr);//resize开空间后,默认值为Node*()的构造,我们也可以自己写
		for (int i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			while (cur)
			{
				Node* next = cur->_next;
				size_t hashi = Hash()(cur->_kv.first) % _newtable.size();
				cur->_next = _newtable[hashi];
				_newtable[hashi] = cur;

				cur = next;
			}

			_table[i] = nullptr;
		}
		_table.swap(_newtable);
	}

	size_t hashi = Hash()(kv.first) % _table.size();
	Node* newnode = new Node(kv);
	newnode->_next = _table[hashi];//newnode的next指向当前表哈希映射位置的结点地址
	_table[hashi] = newnode;//让newnode做头

	++_n;
	return true;
}

3.Find()

哈希桶的查找和闭散列的哈希表很相似,先通过key找到映射的哈希桶,然后去对应的哈希桶里面找查找的结点即可,找到返回结点地址,未找到返回nullptr即可。

Node* Find(const K& key)
{
	size_t hashi = Hash()(key) % _table.size();
	Node* cur = _table[hashi];
	while (cur)
	{
		if (cur->_kv.first == key)
			return cur;
		cur = cur->_next;
	}
	return nullptr;
}

4.Erase()

哈希桶的erase其实就是单链表结点的删除,如果是头删,那就是下一个指针作头,如果是中间删除,则记录前一个结点位置,让前一个结点的next指向删除结点的next。然后归还结点空间的使用权,即为delete结点指针。

bool Erase(const K& key)
{
	Node* ret = Find(key);
	if (!ret)
		return false;

	size_t hashi = Hash()(key) % _table.size();
	Node* cur = _table[hashi];
	if (cur->_kv.first == key)//头删
	{
		_table[hashi] = cur->_next;
		delete cur;
		cur = nullptr;
	}
	else//中间删除
	{
		while (cur)
		{
			Node* prev = cur;
			cur = cur->_next;
			if (cur->_kv.first == key)
			{
				prev->_next = cur->_next;
				delete cur;
				cur = nullptr;
			}
		}
	}
	--_n;
	return true;
}

四、封装实现unordered系列容器

封装实现unordered系列容器所需硬件的哈希表结构以及哈希函数、插入、查找、删除这些接口我们直接复用开散列哈希桶的接口即可,重点在于我们实现容器的迭代器操作,只要实现了迭代器的操作,那我们自己封装的unordered系列容器基本上就能跑起来了。

1.迭代器设计

  • 迭代器需要定义一些模板参数,包括键值类型、元素类型、哈希函数类、键值获取类等。其中,元素类型对于unordered_set来说就是键值类型,对于unordered_map来说就是pair<const key, value>类型。哈希函数类用于将元素类型转换为整数类型,键值获取类用于从元素类型中提取键值。
  • 迭代器需要封装两个指针,一个是节点指针,用于指向当前遍历的元素,另一个是哈希表指针,用于在遍历完一个链表后找到下一个不为空的链表。
  • 迭代器需要重载一些运算符,包括*和->运算符,用于访问当前元素的数据域;++运算符,用于移动到下一个元素;==和!=运算符,用于比较两个迭代器是否指向同一个元素。
  • 迭代器需要提供一些构造函数和析构函数,用于创建和销毁迭代器对象。
//前置声明
template <class K, class T, class Hash, class KeyOfT>
class hashTable;

template <class K, class T, class Hash, class KeyOfT>
struct __HTIterator
{
	typedef hashNode<T> Node;
	typedef hashTable<K, T, Hash, KeyOfT> HT;
	typedef __HTIterator<K, T, Hash, KeyOfT> Self;
	Node* _node;
	HT* _ht;

	__HTIterator(Node* node, HT* ht)
		:_node(node)
		,_ht(ht)
	{}

	Self& operator++()
	{
		if (_node->_next)
		{
			_node = _node->_next;
		}
		else
		{
			//当前桶走完了,要去哈希表里面找下一个桶
			size_t hashi = Hash()(KeyOfT()(_node->_data)) % _ht->_table.size();
			hashi++;

			while (hashi != _ht->_table.size() && _ht->_table[hashi] == nullptr)
			{
				hashi++;
			}

			if (hashi == _ht->_table.size())
				_node = nullptr;
			else
				_node = _ht->_table[hashi];
		}

		return *this;
	}

	T& operator->()
	{
		return &_node->_data;
	}
	T* operator*()
	{
		return _node->_data;
	}

	bool operator!=(const Self& it)const
	{
		return _node != it._node;
	}
	bool operator==(const Self& it)const
	{
		return _node == it._node;
	}
};

未完待续

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

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

相关文章

【Linux系列P4】Linux需要什么?编辑器?软件包?一文帮你了解掌握 [yum][vim]———基础开发工具篇

前言 大家好&#xff0c;这里是YY的Linux系列part4&#xff1b;本章主要内容面向接触过Linux的老铁&#xff0c;主要内容含【学习yum工具&#xff0c;进行软件安装】【拓展yum源安装】【掌握vim编辑器使用&#xff0c;基本命令】【命令集】【懒人配置文件安装教程】 在下一章节…

Java高并发编程—可见性与有序性原理

原子性、可见性和有序性是并发编程所面临的三大问题。 Java通过CAS操作已解决了并发编程中的原子性问题&#xff0c;本章为大家介绍Java如何解决剩余的另外两个问题——可见性和有序性。 CPU物理缓存结构 由于CPU的运算速度比主存&#xff08;物理内存&#xff09;的存取速度…

Android系统原理性问题分析 - 多路并发情况下的C/S模型

声明 在Android系统中经常会遇到一些系统原理性的问题&#xff0c;在此专栏中集中来讨论下。Android系统中很多地方都采用了I/O多路复用的机制&#xff0c;为了引出I/O多路复用机制&#xff0c;先来分析多路并发情况下的C/S模型。此篇参考一些博客和书籍&#xff0c;代码基于A…

C++条件变量condition_variable

一、问题 假设没有条件变量&#xff0c;对于一个生产者消费者问题&#xff0c;消费线程在得知队列中没有产品时&#xff0c;将阻塞自己。生产者线程可以给队列中放入产品&#xff0c;但是没有办法激活消费者线程&#xff0c;而消费者线程处于阻塞状态也没有办法自己激活自己。…

RocketMQ 领域模型概述

本文为您介绍 Apache RocketMQ 的领域模型。 Apache RocketMQ 是一款典型的分布式架构下的中间件产品&#xff0c;使用异步通信方式和发布订阅的消息传输模型。通信方式和传输模型的具体说明&#xff0c;请参见下文通信方式介绍和消息传输模型介绍。 Apache RocketMQ 产品具备…

IOS开发指南之自定义TableViewCell使用

演示效果: 1.自定义TableViewCell创建 File->new->File... 在iOS模板中选择Empty来创建一个空的XIB文件,然后点击下一步 输入XIB文件名Cell,然后点击Create创建 创建XIB文件成功后如下: 同时按钮Shift+command+L弹出库,然后输入 table筛选,选择Table View Cell 拖到下…

一文通透spring的初始化

简述 今天重点分析ApplicationContext初始化时做的事情&#xff0c;我们都只到spring是个IOC和AOP容器&#xff0c;那再我们new一个ApplicationContext&#xff0c;spring内部都做了什么&#xff1f;怎么实现的IOC和AOP&#xff1f; 比如说下面这段代码 Configuration Compon…

计组 第二章 数据的表示与运算 2.1 数制与编码 知识点整理

2.1 数制与编码 二进制转八进制&#xff1a;3位一组&#xff0c;高位补0 二进制转十六进制&#xff1a;4位一组&#xff0c;高位补0 任意进制转十进制&#xff08;按权展开法&#xff09;&#xff1a;数码与权值相乘&#xff0c;再相加 十进制转化为任意进制数&#xff08;基…

全面接入:ChatGPT杀进10个商业应用,让AI替你打工

ChatGPT狂飙160天&#xff0c;世界已经不是两个月前的样子。 新建了一个网站 https://ai.weoknow.com/ 每天给大家更新可用的国内可用chatGPT资源 ChatGPT API已开放60多天。世界已经不是两个月前的样子了。 微软联合创始人比尔盖茨&#xff08;BillGates&#xff09;将GPT称…

一、预约挂号详情

文章目录 一、预约挂号详情1、需求分析 2、api接口2.1 添加service接口2.2 添加service接口实现2.2.1 在ScheduleServiceImpl类实现接口2.2.2 在获取科室信息 2.3 添加controller方法 3、前端3.1封装api请求3.2 页面展示 二、预约确认1、api接口1.1 添加service接口1.2 添加con…

FastRcnn理论合集

FastRcnn理论合集 Rcnn 论文原著 Rich feature hierarchies for accurate object detection and semantic segmentation R-CNN可以说是利用深度学习进行目标检测的开山之作。作者Ross Girshick多次在PASCAL VOC的目标检测竞赛中折桂&#xff0c;曾在2010年带领团队获得终身成就…

【P29】JMeter IF 控制器(If Controller)

文章目录 一、IF 控制器&#xff08;If Controller&#xff09;参数说明二、测试计划设计2.1、groovy 写法2.2、javaScript 写法2.3、jexl3 写法 一、IF 控制器&#xff08;If Controller&#xff09;参数说明 可以控制其下面的子/后代元素是否执行&#xff1b;如果为 true 则…

CSDN问答机器人

文章目录 前言一、背景二、总体流程三、构建知识库四、粗排五、精排六、Prompt总结相关博客 前言 先看结果: 已经连续很多周获得了第二名(万年老二), 上周终于拿了一回第一, 希望继续保持. &#x1f601; 这是今天的榜单, 采纳的数量相对较少, 之前基本上维持在100 重点说明…

数字韧性助力金融科技行稳致远 同创永益亮相2023双态IT武汉樱花论坛

2023年4月7日&#xff0c;由ITSS数据中心运营管理组DCMG指导&#xff0c;双态IT论坛主办的以“分布式架构和云原生时代的运维软件进化”为主题的“双态IT武汉樱花论坛”在武汉成功举办&#xff0c;共有来自银行、保险、证券等行业用户及企业代表近百人参会。 云原生时代下&am…

【Linux】-yum的使用

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树 ❤️‍&#x1fa79;作者宣言&#xff1a;认真写好每一篇博客 &#x1f4a8;作者gitee:gitee &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 如 果 你 喜 欢 作 者 的 文 章 &#xff0c;就 给 作 者 点…

某生存游戏玩家属性值分析

0x01 背景 这是一款丧尸生存的多人沙盒类游戏&#xff0c;角色在废墟之城内不断的搜刮&#xff0c;强化自己的装备和建造设施来抵御丧尸的无休止攻击&#xff0c;记住&#xff0c;这是一款团队合作游戏&#xff0c;你面对的丧尸随时可能夺走你的性命&#xff01; 0x02 玩家结…

【C++ Primer Plus】基础知识

C站的小伙伴们&#xff0c;大家好呀&#xff01;我最近开始阅读学习《C Primer Plus》这本书&#xff0c;在这里和大家一起分享。 下面是本书的第二章《开始学习C》 开始学习C 进入Cmain&#xff08;&#xff09;函数C注释C预处理器和iostream文件头文件名名称空间使用count进行…

【Linux专区】 Linux is not unix | Linux发展史 | Linux应用现状

&#x1f49e;&#x1f49e;欢迎来到 Claffic 的博客&#x1f49e;&#x1f49e; &#x1f449; 专栏&#xff1a;《Linux专区》&#x1f448; 前言&#xff1a; 上次提前带大家搭建了Linux的环境&#xff0c;其实之前应该还有一步的&#xff0c;就是向大家介绍Linux发展史&…

HNU-电路与电子学-小班3

第三次讨论 1 、直接用晶体管而不是逻辑门实现异或门&#xff0c;并解释这个电路是如何工作的。 &#xff08;6个 MOS 管构成&#xff09; 2 、通信双方约定采用 7 位海明码进行数据传输。请为发送方设计海明码校验位 生成电路&#xff0c;采用功能块和逻辑门为接收方设计海…

SCMA基本原理介绍

SCMA: Sparse Code Multiple Access SCMA基本原理 我们考虑一个同步&#xff08;synchronous&#xff09;的SCMA系统&#xff0c; 含1个基站&#xff08;Base Station, BS&#xff09;&#xff1b; J J J个用户&#xff08;so called layers&#xff09;&#xff1b;K个OFDM…