【哈希的模拟实现】

news2024/11/19 17:38:53

文章目录

  • 1 哈希概念
  • 2 哈希冲突
    • 2.1 直接定址法 (常用)
    • 2.2 除留余数法 (常用)
    • 2.3 平方取中法
    • 2.4 折叠法
    • 2.5 随机数法
    • 2.6 数学分析法
  • 3 闭散列
    • 3.1 线性探测
    • 3.2 二次探测
  • 4 开散列
    • 4.1 开散列概念
    • 4.2哈希桶的模拟实现
    • 4.3 开散列与闭散列的比较


1 哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

  • 插入元素
    根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。

  • 搜索元素
    对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
在这里插入图片描述
但是我们再插入一个数,例如14 ,会出现什么问题?
是不是就和之前的4给冲突了,那我们应该怎样解决哈希冲突呢?


2 哈希冲突

对于两个数据元素的关键字 k i k_i ki k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) == Hash( k j k_j kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值 域必须在0到m-1之间.

  • 哈希函数计算出来的地址能均匀分布在整个空间中 哈希函数应该比较简单.

常见哈希函数

2.1 直接定址法 (常用)

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况

2.2 除留余数法 (常用)

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

2.3 平方取中法

假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况.

2.4 折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

2.5 随机数法

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。

2.6 数学分析法

设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如:假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等方法。数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况.

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

解决哈希冲突两种常见的方法是:闭散列和开散列


3 闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

3.1 线性探测

比如2上面中的场景,现在需要插入元素14,先通过哈希函数计算哈希地址,hashAddr为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。那我们就将14插入到hashAddr为4后面没有被使用的地方(如果超出了表的长度就需要重新从头开始查找)。

但是这样做了还有一个小问题:我们删除了一个数再次查找时应该怎样查找呢?
如果只是查找为空位置为止的话那么删除元素后面的数据是不是都找不到了,所以为了方便区分各个元素的状态,我们不妨将元素的状态分为:空,存在,删除

那我们就可以先把基本框架先搭好:

namespace CloseHash
{
	enum State
	{
		DELETE,
		EMPTY,
		EXIST
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _st = EMPTY;
	};

	template<class K>
	struct HashTranfor
	{
		size_t operator()(const K& k)
		{
			return (size_t)k;
		}
	};

	//特化一个版本出来
	template<>
	struct HashTranfor<string>
	{
		// BKDR
		size_t operator()(const string& key)
		{
			size_t val = 0;
			for (auto ch : key)
			{
				val *= 131;
				val += ch;
			}

			return val;
		}
	};


	template<class K,class V,class Hash=HashTranfor<K>>
	class HashTable
	{
	private:
		vector<HashData<K,V>> _tables;
		size_t _size=0;
	public:
	};

大家发现了没有,除了模板参数我们除了给出了K,V关系外,我们还给了一个仿函数,为什么我们要给出仿函数呢?
其实也很好理解,因为我们插入查询的关键值可能不仅仅是能够直接取模的,比如常见的我们还可能插入字符串等等,所以我们的要有一个能够将关键值转化为整形的仿函数,因为字符串是比较常见的,所以我们特化了一个版本出来,字符串哈希的方式有很多种,想要进一步了解的老哥可以移步下面的文章:【字符串哈希算法】

插入:通过之前的分析我们知道:当表中空间中有大量位置已经有数据时再次插入数据发生冲突的可能性也会更大,那我们可以通过负载因子来控制表中数据量,一般负载因子给出在0.7~0.8之间比较合适,但是最大的问题是扩容咋办?这也是闭散列的劣势所在,扩容了后原来位置的映射关系可能发生了改变,所以之前旧表的元素得重新映射一遍,而扩容是闭散列效率低下的主要原因。
那应该如何扩容呢?比较常见的做法是我们在开一个空间更大的vector,然后在重复一遍之前插入的动作,但是我们可以采用一下更为巧妙的做法:

bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;
			//扩容
			if (_tables.size() == 0 || (_size * 10 / _tables.size() >= 7))
			{
				//负载因子满了就扩容,一般负载因子给到0.7
				size_t newCapacity = _tables.size() == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V> newHash;//这里也可以考虑直接用一个vector,
				//但是没必要,创建一个新的哈希表直接调用Insert即可
				newHash._tables.resize(newCapacity);
				for (auto& e : _tables)
				{
					if (e._st == EXIST)
						newHash.Insert(e._kv);
				}
				newHash._tables.swap(_tables);
			}
			Hash hash;

			//1 线性探测
			size_t hashi =hash(kv.first) % _tables.size();
			while (_tables[hashi]._st == EXIST)
			{
				++hashi;
				hashi %= _tables.size();
			}

			_tables[hashi]._kv = kv;
			_tables[hashi]._st = EXIST;
			++_size;
			return true;

注意

  • 代码中我们直接创建了一张新表,通过新表直接复用Insert即可,然后再交换旧表与新表的vector即可。
  • 这里面还有一个小细节是每次模表的长度时我们模的都是size(),而不是capacity,这是由于我们要用的空间必须是已经开好了可以直接通过下标访问的。

查询

HashData<K, V>* Find(const K& k)
		{
			if (_tables.size() == 0)//出错控制
				return nullptr;
			Hash hash;
			//size_t hashi = k % _tables.size();//负数也可以处理,会将k先转换成无符号数再取模

			//注意这里用二次探测的时候也是用的是一个一个向后查找
			size_t hashi = hash(k) % _tables.size();
			size_t start = hashi;
			while (_tables[hashi]._st != EMPTY)
			{
				if (_tables[hashi]._st == EXIST && _tables[hashi]._kv.first == k)
					return &_tables[hashi];
				++hashi;
				hashi %= _tables.size();
				if (hashi == start)//防止表中全部剩的是删除的元素或者大部分是删除状态和存在状态而导致死循环
					break;
			}
			return nullptr;
		}

查询时有一个特别容易让人忽略的地方:当表中元素全部都是删除状态和存在状态时上面代码也能够正常处理,我们记录了最开始的位置当我们重新回到了原位置时就退出循环。

删除

bool erase(const K& k)
		{
			if (Find(k) == nullptr)
				return false;

			auto ptr=Find(k);
			ptr->_st = EMPTY;
			--_size;
			return true;
		}

线性探测优点:实现非常简单;
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。如何缓解呢?
我们可以用二次探测来缓解线性探测踩踏问题。

3.2 二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H_i Hi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者: H i H_i Hi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m。其中:i = 1,2,3…, H 0 H_0 H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

二次探测的实现也很简单,但是无论是线性探测还是二次探测都不能解决闭散列扩容时的代价。
二次探测插入的实现:

bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;
			//扩容
			if (_tables.size() == 0 || (_size * 10 / _tables.size() >= 7))
			{
				//负载因子满了就扩容,一般负载因子给到0.7
				size_t newCapacity = _tables.size() == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V> newHash;//这里也可以考虑直接用一个vector,
				//但是没必要,创建一个新的哈希表直接调用Insert即可
				newHash._tables.resize(newCapacity);
				for (auto& e : _tables)
				{
					if (e._st == EXIST)
						newHash.Insert(e._kv);
				}
				newHash._tables.swap(_tables);
			}
			Hash hash;

			//2 二次探测
			size_t hashi = hash(kv.first) % _tables.size();
			int i = 0;
			while (_tables[hashi]._st == EXIST)
			{
				++i;
				hashi = i * i;
				hashi %= _tables.size();
			}

			_tables[hashi]._kv = kv;
			_tables[hashi]._st = EXIST;
			++_size;
			return true;
		}

查找的代码大家可以参考线性探测的代码再修改修改,这里我就不在多说了,很简单。


4 开散列

4.1 开散列概念

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

我们可以将结点像桶似的一个一个挂起来:
在这里插入图片描述这样冲突的元素就在同一条链子上。

4.2哈希桶的模拟实现

开散列的模拟实现:

namespace BucketHash
{
	template<class K,class V>
	struct BucketHashNode
	{
		BucketHashNode* _next;
		pair<K, V> _kv;

		BucketHashNode(const pair<K,V>& kv)
			:_next(nullptr)
			,_kv(kv)
		{}
	};

	template<class K>
	struct HashTranfor
	{
		size_t operator()(const K& k)
		{
			return (size_t)k;
		}
	};

	//特化一个版本出来
	template<>
	struct HashTranfor<string>
	{
		// BKDR
		size_t operator()(const string& key)
		{
			size_t val = 0;
			for (auto ch : key)
			{
				val *= 131;
				val += ch;
			}

			return val;
		}
	};

	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
	};

	template<class K, class V,class Hash=HashTranfor<K>>
	class HashTable
	{
	private:
		typedef BucketHashNode<K, V> Node;
		vector<Node*> _tables;
		size_t _size = 0;

		size_t GetCapacity(size_t sz)
		{
			int i = 0;
			for (; i < __stl_num_primes; ++i)
			{
				if (sz < __stl_prime_list[i]) break;
			}
			return __stl_prime_list[i];
		}

	public:
		~HashTable()
		{
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;
			Hash hash;
			//扩容,负载因子为1
			if (_size == _tables.size())
			{
				//int newcapacity = _tables.size() == 0 ? 10 : _tables.size() * 2;
				int newcapacity =GetCapacity(_tables.size());

				vector<Node*> newTables;//这里直接用vector的目的是可以直接用原表的结点直接链接即可
										//不必多拷贝结点
				newTables.resize(newcapacity);
				for (int i = 0; i < _tables.size(); ++i)
				{
					Node* cur = _tables[i];
					//头插
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hash(cur->_kv.first) % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;
						cur=next;
					}

					_tables[i] = nullptr;
				}

				_tables.swap(newTables);
			}

			size_t hashi = hash(kv.first) % _tables.size();
			Node* newNode = new Node(kv);
			newNode->_next = _tables[hashi];
			_tables[hashi] = newNode;
			++_size;

			return true;
		}

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

		bool Erase(const K& k)
		{
			if (Find(k) == nullptr)
				return false;

			Hash hash;
			size_t hashi = hash(k) % _tables.size();
			if (_tables[hashi]->_kv.first == k)
			{
				Node* del = _tables[hashi];
				_tables[hashi] = del->_next;
				delete del;
				return true;
			}
			Node* cur = _tables[hashi];
			while (cur->_next && cur->_next->_kv.first!=k)
			{
				cur = cur->_next;
			}
			Node* del = cur->_next;
			cur->_next = del->_next;
			delete del;
			return true;
		}

		size_t Size()const
		{
			return _tables.size();
		}

		size_t BucketSize()const
		{
			int cnt = 0;
			for (int i = 0; i < Size(); ++i)
			{
				if (_tables[i])
					++cnt;
			}
			return cnt;
		}

		size_t MaxBucketSize()const
		{
			int maxLen = 0;
			for (int i = 0; i < Size(); ++i)
			{
				Node* cur = _tables[i];
				int len = 0;
				while (cur)
				{
					++len;
					cur = cur->_next;
				}
				maxLen = max(maxLen, len);
			}
			return maxLen;
		}
	};

注意:

  • 代码中扩容时我们并没有直接从10然后二倍二倍的扩容,而是直接打表了一组数据(质数)扩容,这样出的好处时能够尽少的减低哈希冲突的概率。
  • 扩容时我们并没有像之前闭散列那样直接创建一张新表然后复用insert,我们创建的一个新的vector,然后将原表的结点一个一个取下来头插即可,这样做节约了开辟结点的开销,但是一定要注意,新插入的节点不能够直接将对应链表直接连接过来,而是所有结点要重新映射关系,因为空间容量变了,必须重新映射数据。

至于其他地方我相信大家学了链表的增删查改后都是小意思了。
至于验证的话大家可以找一组随机数来测测哈希冲突的概率:

void test1()
	{
		srand(time(0));
		HashTable<int, int> sh;
		int N = 100;
		for (int i = 0; i < N; ++i)
		{
			int x = rand();
			sh.Insert(make_pair(x,x));
		}

		cout << sh.BucketSize() << endl;
		cout << sh.MaxBucketSize() << endl;
		cout << sh.Size() << endl;
		cout << "负载因子" << (double)sh.BucketSize() / (double)sh.Size() << endl;
	}

4.3 开散列与闭散列的比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。


好了,今天的内容就到这里了,如果该文章对你有帮助的话能不能一件三连博主呢?💗💗💗

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

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

相关文章

Science|改变微生物群落可以增强树木对气候变化的耐受性

改变微生物群落可以增强树木对气候变化的耐受性 Shifting microbial communities can enhance tree tolerance to changing climates Research Article&#xff0c;2023-5-25&#xff0c;Science&#xff0c; [IF 63.714] DOI&#xff1a;10.1126/science.adf202 第一作者&…

Java常用快捷键

在编写java代码时&#xff0c;要提升自己的编写代码的速度&#xff0c;光是靠鼠标来完成各种的操作是不行的&#xff0c;还会显得十分不成熟&#xff0c;所以这是就需要我们使用一定的快捷键&#xff0c;在键盘点击之间&#xff0c;完成代码的操作。接下来我就为大家介绍常用的…

7.2DIY可视化后台表格自定义列显示类型

后台表格自定义列显示类型 本教程均在第一节中项目启动下操作 后台表格自定义列显示类型前言一、图片格式内容显示:二. 效果展示: http://localhost:9999/#/hdp三:自定义列使用:1.添加字段:自定义列,取值自幻灯片title2. 设置自定义信息: 三.效果展示本文章原自bilibli作者视频…

6月6日复盘总结 11H10min-|23:15~23:25*

​​​​​​​ 7:20-8:20 背书翻译单词 【1h】 8:30-9:00 乐词 【30min】 9:00-10:00 TPO66 L1 精听 【1h】 10:00-10:30 TPO66 L1 错题分析 【30min】 10:30-11:30 L2 Fish Movement 精听 【1h】 11:40-12:40 午饭+水果🍑+玩手机…

【华为OD统一考试B卷 | 100分】经典屏保(C++ Java JavaScript Python)

题目描述 DVD机在视频输出时,为了保护电视显像管,在待机状态会显示“屏保动画”,如下图所示,DVD Logo在屏幕内来回运动,碰到边缘会反弹。 请根据如下要求,实现屏保Logo坐标的计算算法。 屏幕是一个800*600像素的矩形,规定屏幕的左上角点坐标原点,沿横边向右方向为X轴…

jeecg-boot权限篇

前言 本文档是对jeecg-boot官方文档的一个分析和补充&#xff0c;具体的可以同时互相参照来看&#xff0c;难免个人理解有出路&#xff0c;如有错误&#xff0c;还望补充和指正&#xff5e; 文章目录 角色、用户、部门普通权限控制特殊定制化权限按钮控制文本输入禁用 列表控制…

碳中和城市建筑能源系统(2):网络篇(龙惟定)2022

碳中和城市建筑能源系统(2):网络篇 摘要 本文是碳中和城市建筑能源系统系列文章的第二篇,分别概要介绍了城市能源系统中的电网、热网和燃气网在碳中和背景下的新概念、新技术及新方法。强调未来的能源网以电网为主,三网融通,构成城区的能源互联网。综述了智能电网的灵活性、热…

我们不会很快有GPT-5;让 ChatGPT 帮我们总结 Hacker News

&#x1f989; AI新闻 &#x1f680; OpenAI联合创始人Sam Altman&#xff1a;我们不会很快有GPT-5 摘要&#xff1a;在2023北京智源大会的“AI安全与对齐”主题论坛上&#xff0c;OpenAI联合创始人Sam Altman表示&#xff0c;目前他们没有答案&#xff0c;不会很快有GPT-5&a…

OpenCV 项目开发实战--对图像种的斑点进行检测(Python、C++代码实现)

什么是斑点? Blob 是图像中一组连接的像素,它们共享一些共同的属性(例如,灰度值)。在上图中,暗连接区域是斑点,斑点检测旨在识别和标记这些区域。 文末附相关测试代码的下载链接 SimpleBlobDetector 示例 OpenCV 提供了一种基于不同特征检测和过滤斑点的便捷方法。让…

es相关的知识点

海量数据下如何提升es的操作性能 .filesystemcache os cache操作系统缓存 es中的数据,实际上写入磁盘,磁盘文件的操作系统,实际上会将数据写入到oscache中 es的搜索引擎严重依赖于底层的filesystemcache 如果filesystemcache的内存足够大,可以容纳所有的index segmentfile索引…

Mac环境下在vs code中配置copilot

1、下载vs code编辑器 2、在GitHub个人设置里&#xff0c;把copilot设为allow&#xff0c;这里涉及要开通服务就不再详述。 3、在vs code插件市场里下载GitHub copilot 安装好了以后根据指示输入你的GitHub账号即可 编译器底部出现copilot图标即表示安装成功 使用方法就是你先…

GD32F4单片机实现接收超时中断+DMA实现串口的不定长接收和DMA发送

1、通常的实现方式介绍 环形缓冲区定时器超时中断的方式 优点 环形缓冲区可以接收多帧数据数据帧超时间隔可以设置 缺点 设备任务比较繁重时&#xff0c;使用中断接收可能会丢失数据。尤其是在长时间关闭中断或者串口中断优先级不高时频繁进出中断。在使用RTOS的系统中&#x…

给第一行单元格赋值 + WPS JS获取工作表的总行数 + WPS JS获取工作表的总行数

戳我&#xff0c;了解更多相关办公的小技巧 给第一行单元格赋值 1、在计算机中有一种ASCII编码&#xff0c;其中A在计算机中的表示的数字是65&#xff0c;a的ascii码是97&#xff0c;b的ascii码是98。 2、从A1到F1可以看到第一个字母在变化&#xff0c;第2个数字始终是1&#x…

过电流保护原理

过电流保护是指当电流超过预定最⼤值时&#xff0c;使保护装置动作的⼀种保护⽅式。当流过被保护原件中的电流超过预先整定 的某个数值时&#xff0c;保护装置启动&#xff0c;并⽤时限保证动作的选择性&#xff0c;使断路器跳闸或给出报警信号。 过电流保护主要包括短路保护和…

北京通信展的精华内容,都在这里!(上篇)

友情提醒&#xff1a;本文图片较多&#xff0c;请大家注意手机流量。 大家好&#xff0c;我是小枣君。 昨天&#xff0c;中国国际信息通信展览会&#xff08;PT展&#xff09;在北京正式落幕了。 小枣君全程参加了这场行业盛会。按照惯例&#xff0c;我来给大家汇报一下现场的情…

ChatGPT Prompt 提示词设计技巧必知必会

本文内容整理自图灵社区直播《朱立成&#xff1a;ChatGPT Prompt提示词技巧必知必会》。 朱立成&#xff0c;图灵社区《ChatGPT即学即用》视频课程作者&#xff0c;软件工程师&#xff0c;对新事物充满好奇&#xff0c;关注ChatGPT应用。2001年毕业于浙江大学&#xff0c;从事软…

硬件设计电源系列文章-电路电源设计流程

文章目录 概要整体架构流程技术名词解释技术细节小结 概要 本文主要介绍硬件单板电源设计流程。 整体架构流程 提示&#xff1a;这里可以添加技术整体架构 主要分三部&#xff1a; 电源需求&#xff1a; 根据硬件总体方案&#xff0c;确定各芯片所需的供电电压&#xff1…

go-Context详解

Context详解 简介 官网 context go package context-blog Context是一个很特殊的接口&#xff0c;在go里面主要承担的责任是在边界&#xff08;方法&#xff0c;线程等&#xff09;传递上下文&#xff0c;这些上下文包括 取消信号超时时间特殊的参数 需要有几个注意点 …

Tcl常用语法备忘录-字符串篇

TCL语言中的string命令用于对字符串进行操作&#xff0c;常用的有以下几种用法&#xff1a; string length 语法&#xff1a;string length string 参数说明&#xff1a;string为要计算长度的字符串。 示例&#xff1a; set str "Hello TCL" puts [string lengt…

盘点一个Jupyter显示的细节问题

点击上方“Python爬虫与数据挖掘”&#xff0c;进行关注 回复“书籍”即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 弦弦掩抑声声思&#xff0c;似诉平生不得志。 大家好&#xff0c;我是皮皮。 一、前言 前几天在Python白银群【小王子】问了一个Python基础的问题&…