C++容器之unordered_map、unordered_set的底层剖析

news2024/11/24 3:38:09

在这里插入图片描述

文中源码以上传至Gitee

目录

  • 序列式容器和关联式容器
    • unordered_set和unordered_map
  • 哈希表
    • 概念
  • 哈希函数与哈希冲突
  • 常用的哈希函数
    • 直接定址法
    • 除留余数法
  • 哈希冲突处理方案
    • 开放定址法
    • 链地址法
    • 开放定地址法和链地址法对比
  • 开放定址法实现
  • 链地址法实现
  • unordered_map和unordered_set的实现
  • 总结

序列式容器和关联式容器

序列式容器和关联式容器是C++标准库中的两种不同类型的容器。序列式容器是按照元素在容器中的线性顺序进行存储和访问的容器。它们可以包含不同类型的元素,并提供了按顺序插入、删除和访问元素的操作。常见的序列式容器包括:

  1. vector:动态数组,支持快速的随机访问。
  2. list:双向链表,支持高效的插入和删除操作。
  3. deque:双端队列,支持在两端进行插入和删除操作。

关联式容器是根据元素的键值进行存储和访问的容器。它们使用一种特定的数据结构(通常是二叉搜索树或哈希表)来实现元素的快速查找。关联式容器中的元素通常是按照键值的排序顺序进行存储。常见的关联式容器包括:

  1. set:集合,存储唯一的元素,并按照键值进行排序。
  2. map:映射,存储键值对,并按照键值进行排序。
  3. multiset:多重集合,存储允许重复的元素,并按照键值进行排序。
  4. multimap:多重映射,存储允许重复的键值对,并按照键值进行排序。

unordered_set和unordered_map

unordered_set 和 unordered_map 都是C++标准库中的关联式容器,它们的底层实现都基于哈希表,主要区别在于它们存储的内容和用途,unordered_set 是一种用于存储唯一值的容器。它类似于一个集合,其中每个值都是唯一的,重复值将被忽略,主要用途是在不需要关联键值对的情况下,仅存储一组唯一的值,并支持高效的插入、查找和删除操作。unordered_map 是用于存储键值对的容器,其中每个键都对应一个唯一的值。它提供了一种通过键来查找值的机制,主要用途是将键与数据进行关联,支持通过键高效地查找、插入和删除值。

哈希表

unordered_map和unordered_set与map和set的用法基本相似,在熟悉了map和set的用法之后,在上手这两个就不是什么难事了,但是已经有了map和set之后,为什么C++11还要加入这两个呢?那必然是有它的独到之处的,就是他那在平均情况下实现常数时间复杂度的插入、查找和删除操作。如此高的效率,它的底层实现也是很值得让人去查探一番。

概念

哈希表是一种常用的数据结构,用于实现关联数组或映射。它通过将键映射到值来实现快速的数据查找和插入操作。哈希表的基本思想是利用哈希函数将键映射到一个固定大小的数组(通常称为哈希表或散列表)的索引位置上。哈希函数将键转换为一个整数,然后使用该整数作为数组的索引,将值存储在对应的位置上。

哈希函数与哈希冲突

哈希函数的设计很重要,它应该能够将键均匀地映射到数组的不同位置上,以避免冲突。哈希冲突也叫哈希碰撞,即不同关键字通过相同哈希哈数计算出相同的哈希地址。又将具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

在这里插入图片描述
当我们在插入3或者5时,根据哈希函数的映射规则,会将要新插入的数映射到一个已经存在有数据的位置,这就会导致哈希冲突。引起哈希冲突的一个极其重要的原因可能是:哈希函数设计不够合理。哈希函数的设计是哈希表的关键部分,一个好的哈希函数应该具备以下设计原则:

  • 确定性:相同的输入应该始终映射到相同的哈希值。这是哈希函数的基本要求,确保在相同的键上进行查找或插入操作时能够得到一致的结果。

  • 均匀性:好的哈希函数应该能够将不同的输入均匀地分布在哈希表的各个位置上,以减少冲突的发生。这可以提高哈希表的性能,因为冲突会导致查找、插入和删除操作的时间复杂度增加。

  • 高效性:哈希函数应该能够快速计算出哈希值,以确保操作的效率。理想情况下,哈希函数的计算时间应该是常数时间。

  • 低碰撞率:碰撞是指多个不同的键映射到相同的哈希值的情况。好的哈希函数应该尽量减少碰撞的发生,但完全消除碰撞是不可能的。

  • 最小冲突影响:当发生碰撞时,哈希函数应该能够将冲突的影响最小化,例如通过开放寻址法或链表法等方法来处理碰撞。

  • 适应性:哈希函数应该适应不同大小的哈希表,而不仅仅适用于特定大小的表格。

常用的哈希函数

我们常用的哈希函数主要有两个,分别是直接定址法和除留余数法。

直接定址法

直接定址法使用键的某个线性函数(通常是哈希表大小的乘法因子)来计算哈希值。具体步骤如下:

  1. 对于给定的键,使用一个线性函数计算哈希值:hash(key) = a * key + b,其中a和b是常数。
  2. 将哈希值作为数组的索引,将键存储在该位置。

在这里插入图片描述

直接定址法的优点是简单快速,不需要处理冲突,但它要求哈希表的大小足够大,以避免冲突的发生。如果哈希表的大小不够大,可能会导致较高的冲突率。

除留余数法

除留余数法使用键除以哈希表的大小,然后取余数作为哈希值。具体步骤如下:

  1. 对于给定的键,计算哈希值:hash(key) = key % table_size,其中table_size是哈希表的大小。
  2. 将哈希值作为数组的索引,将键存储在该位置。

在这里插入图片描述

除留余数法的优点是简单易实现,适用于哈希表大小不变的情况。然而,如果哈希表的大小改变,可能会导致哈希值的分布不均匀,增加冲突的概率。

哈希冲突处理方案

哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。理论上只要存放数据的数组足够大,能够保证每个数据都有自己独立的空间,这样就不会出现冲突。先不说这种设想的可能性,单是空间的利用率就已经让人望而生畏。因此就还需要一个能够让冲突的概率和空间利用率处于一种相对平衡状态的方案,于是负载因子应运而生。负载因子通常表示为λ(lambda),是哈希表中已存储元素的数量与哈希表总容量之间的比值。它用来衡量哈希表的填充程度,即已存储元素占哈希表容量的百分比。一般来说,常见的负载因子阈值为0.7或0.8,即当哈希表中已存储元素的数量占总容量的70%或80%时,触发动态扩展操作。这样可以在保持较好性能的同时,避免过度消耗内存。

开放定址法

开放定址法也叫闭散列,当发生冲突时,继续探测数组中的下一个位置,直到找到一个空闲的位置来存储值。 寻找下一个空闲位置的方法有线性探测和二次探测,当发生冲突时,线性探测会依次检查下一个哈希表位置,直到找到一个空的位置为止。这意味着如果位置i被占用,线性探测会尝试位置i+1,然后i+2,以此类推。

在这里插入图片描述

二次探测使用二次函数来确定下一个探测位置,例如,如果位置i被占用,二次探测会尝试位置(i+k^2)%size(k=1,2,3…),以此类推。这有助于减少线性探测可能出现的聚集问题。

在这里插入图片描述

链地址法

链地址法也叫开散列,在链地址法中,每个散列表的槽都会链接一个链表,当发生冲突时,新元素会被添加到相应槽的链表中,而不是覆盖已存在的元素。这种方法允许多个元素共享同一个槽,并通过链表将它们串联在一起。当需要查找特定元素时,散列表首先计算元素的散列值,然后定位到相应的槽,最后在链表中搜索。

在这里插入图片描述

开放定地址法和链地址法对比

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

开放定址法实现

如果使用线性探测,在开放定址法的实现中,首先需要一个顺序表用来存储映射的数据,由于线性探测的特性,如果在删除操作时直接将数据进行删除,那么就会影响其他元素的查找,例如:
在这里插入图片描述
在上述的查找中,如果直接将3删除后,在查找2的时候,在经过哈希函数的计算从下标为2的地方开始查找,不等于2则向后走,走到下标为3的时候发现该位置没有数据,则认为表中不存在元素2,查找失败。因此线性探测应该采用标记的伪删除法来删除一个元素,就可以这样定义顺序表中存储的数据,如下代码:

enum State { EMPTY, EXIST, DELETE };
template<class K, class V>
struct Elem
{
	pair<K, V> _val;
	State _state;
};

对于哈希表,除了定义一个顺序表之外,还需要又一个size来记录有效元素个数以及一个totalsize来记录顺序表中的总元素个数,如下代码:

vector<Elem> _ht;
size_t _size;  
size_t _totalSize; 

除此之外还需要一个哈希函数和一个检查扩容的函数,哈希函数采用除留余数法,由于key的类型不确定,因此还需要提供一个仿函数来将不能直接进行取模运算的类型转换成为可以进行取模运算的类型,就有如下代码:

size_t HashFunc(const K& key)
{
	return HF()(key) % _ht.size();
}

检查扩容函数就是检查负载因子是否超过阈值,如果超过阈值就需要对顺序表进行扩容并且对其中的有效数据进行重新映射和更新totalsize。如下代码:

void CheckCapacity()
{
	if (_totalSize * 10 / _ht.size() >= 6)
	{
		HashTable<K, V> ht(_ht.size() * 2);
		for (auto& e : _ht)
		{
			if (e._state == EXIST)
			{
				ht.Insert(e._val);
			}
		}
		Swap(ht);
	}
}

然后就可以对哈希表来进行各种操作了,如插入,删除,查找等。如下代码:

// 插入
		bool Insert(const pair<K, V>& val)
		{
			CheckCapacity();
			size_t ret = Find(val.first);
			if (ret != -1) return false;
			int hashi = HashFunc(val.first);
			while (1)
			{
				if (_ht[hashi % _ht.size()]._state == EMPTY)
				{
					_ht[hashi % _ht.size()] = { val, EXIST };
					_size++;
					_totalSize++;
					break;
				}
				hashi++;
			}
			return true;
		}

再插入操作中,首先对负载因子进行检查,然后在对要插入元素的key值进行查找,看看是否存在相同key值,如果存在则插入失败,否则就计算该key值的映射位置,对该位置进行线性探测,插入完成后跳出循环返回true。

代码中的循环不会造成死循环,因为负载因子的控制保证了顺序表中必然存在空的位置来进行插入,二次线性探测也是类似,只需要修改hashi的步长逻辑即可。

// 查找
size_t Find(const K& key)
{
	int hashi = HashFunc(key);
	while (hashi < _ht.size() && _ht[hashi]._state != EMPTY)
	{
		if (_ht[hashi]._state != DELETE && _ht[hashi]._val.first == key)
		{
			return hashi;
		}
		hashi++;
	}			
	return -1;
}

// 删除
bool Erase(const K& key)
{
	size_t ret = Find(key);
	if (ret == -1)
	{
		return false;
	}
	_ht[ret]._state = DELETE;
	_size--;
	return true;
}

链地址法实现

要链地址法首先还是需要一个顺序表,里面存放指向数据元素的指针,数据元素里面除了存放数据也需要有一个指针将冲突元素链接起来,因此就可以定义一个结构体对象,如下代码:

template<class T>
struct HashBucketNode
{
	HashBucketNode(const T& data)
		: _pNext(nullptr)
		, _data(data)
	{}
	HashBucketNode<T>* _pNext;
	T _data;
};

哈希表的定义除了一个顺序表外还需要一个size用来记录表中的元素个数,如下代码:

vector<Node*> _table;
size_t _size;

与开放定址法类似,由于节点中data的类型不确定,因此对data的哈希映射时不能直接进行取模,还需要提供一个仿函数对key值进行转换,这样就可以通过仿函数将key值过滤一遍在进行取模运算。如下代码:

size_t HashFunc(const K& key)
{
	return HF()(key) % _table.size();
}

链地址法也需要维护负载因子来减少冲突的概率,因此也需要一个检查扩容的函数,又因为date的类型不确定,因此还需要提供一个仿函数用来提取元素的key值,如下代码:

void CheckCapacity()
{
	if (_size * 10 / _table.size() >= 7)
	{
		_table.resize(_table.size() * 2);
		for (int i = 0; i < _table.size(); i++)
		{
			while (_table[i] != nullptr)
			{
				int hashi = HashFunc(KeyOfT()(_table[i]->_data));
				if (hashi == i) break;
				Node* p = _table[i];
				_table[i] = p->_pNext;						
				p->_pNext = _table[hashi];
				_table[hashi] = p;
			}
		}
	}
}

最后可以对哈希表进行插入、删除和查找了,如下代码:

pair<iterator, bool> Insert(const T& data)
{
	KeyOfT keyoft;
	int hashi = HashFunc(keyoft(data));
	Node* p = _table[hashi];
	while (p)
	{
		if (keyoft(p->_data) == keyoft(data)) 
			return make_pair(iterator(nullptr, this), false);
		p = p->_pNext;
	}
	CheckCapacity();
	hashi = HashFunc(keyoft(data));
	Node* node = new Node(data);
	node->_pNext = _table[hashi];
	_table[hashi] = node;
	_size++;
	return make_pair(iterator(node, this), true);
}

bool Erase(const K& key)
{
	KeyOfT keyoft;
	int hashi = HashFunc(key);
	if (_table[hashi] == nullptr) return false;
	Node* cur = _table[hashi];
	if (keyoft(cur->_data) == key)
	{
		_table[hashi] = cur->_pNext;
		delete cur;
		_size--;
		return true;
	}
	else
	{
		Node* prev = cur;
		cur = cur->_pNext;
		while (cur)
		{
			if (keyoft(cur->_data) == key)
			{
				prev->_pNext = cur->_pNext;
				delete cur;
				_size--;
				return true;
			}
			prev = prev->_pNext;
			cur = cur->_pNext;
		}	
		return false;
	}
}

iterator Find(const K& key)
{
	KeyOfT keyoft;
	int hashi = HashFunc(key);
	Node* p = _table[hashi];
	while (p)
	{
		if (keyoft(p->_data) == key) return iterator(p, this);
		p = p->_pNext;
	}
	return end();
}

除此之外还可以给哈希表加上迭代器,迭代器需要有指向元素的指针,为了方便对迭代器进行++操作,还需要一个哈希表,如下代码:

template<class K, class T, class KeyOfT, class Ref, class Ptr, class HF>
class HBIterator
{
	typedef HashBucket<K, T, KeyOfT, HF> HashBucket;
	typedef HashBucketNode<T> Node;
	typedef HBIterator<K, T, KeyOfT, Ref, Ptr, HF> self;
public:
	HBIterator(Node* node, const HashBucket* ht)
		:_pnode(node), _ht(ht)
	{}
	HBIterator(const self& it)
		:_pnode(it._pnode)
		,_ht(it._ht)
	{}

	Ref operator*()
	{
		return _pnode->_data;
	}
	Ptr operator->()
	{
		return &(_pnode->_data);
	}
	bool operator==(const self& it)
	{
		return _pnode == it._pnode;
	}
	bool operator!=(const self& it)
	{
		return _pnode != it._pnode;
	}
	self operator++()
	{
		if (_pnode->_pNext)
		{
			_pnode = _pnode->_pNext;
		}
		else
		{
			int hashi = HF()(KeyOfT()(_pnode->_data)) % _ht->_table.size();
			hashi++;
			while (hashi < _ht->_table.size())
			{
				if (_ht->_table[hashi])
				{
					_pnode = _ht->_table[hashi];
					break;
				}
				else
				{
					++hashi;
				}
			}
			if (hashi == _ht->_table.size())
			{
				_pnode = nullptr;
			}
		}
		return *this;
	}
private:
	Node* _pnode;
	const HashBucket* _ht;
};

unordered_map和unordered_set的实现

在实现了链地址法的哈希表后,对于unordered_map和unordered_set的实现就可以用这个哈希表做底层容器,直接复用代码即可简单完成unordered_map和unordered_set。如下代码:

template<class K, class V, class HF = HashFunc<K>>
class unordered_map
{
public:
	struct KeyOfT
	{
		const K& operator()(const std::pair<K, V>& data)
		{
			return data.first;
		}
	};
	typedef HashBucket<K, std::pair<K, V>, KeyOfT, HF> HT;
	typedef typename HashBucket<K, std::pair<K, V>, KeyOfT, HF>::iterator iterator;
public:
	unordered_map(size_t num = 10)
		:_map(num)
	{}
	unordered_map(const HT& map)
		:_map(map)
	{}
	~unordered_map()
	{}
	iterator begin()
	{
		return _map.begin();
	}
	iterator end()
	{
		return _map.end();
	}
	pair<iterator, bool> insert(const std::pair<K, V>& data)
	{
		return _map.Insert(data);
	}
	iterator find(const K& key)
	{
		return _map.Find(key);
	}
	bool erase(const K& key)
	{
		return _map.Erase(key);
	}
	size_t size()
	{
		return _map.Size();
	}
	size_t count()
	{
		return _map.BucketCount();
	}
private:
	HT _map;
};
template<class K, class HF = HashFunc<K>>
class unordered_set
{
	struct KeyOfT
	{
		const K& operator()(const K& data)
		{
			return data;
		}
	};
	typedef HashBucket<K, K, KeyOfT, HF> HT;
	typedef HBIterator<K, K, KeyOfT, K&, K*, HF> iterator;
public:
	unordered_set(size_t num = 10)
		:_set(num)
	{}
	unordered_set(const HT& set)
		:_set(set)
	{}
	~unordered_set()
	{}
	iterator begin()
	{
		return _set.begin();
	}
	iterator end()
	{
		return _set.end();
	}
	bool insert(const K& data)
	{
		return _set.Insert(data);
	}
	iterator find(const K& data)
	{
		return _set.Find(data);
	}
	bool erase(const K& data)
	{
		return _set.Erase(data);
	}
	size_t size()
	{
		return _set.Size();
	}
	size_t count()
	{
		return _set.BucketCount();
	}
private:
	HT _set;
};

总结

文章对C++unordered_map和unordered_set的底层哈希表进行了详细介绍,但是对unordered_map和unordered_set的用法并没有进行介绍,因为这两者和map、set的用法并没有太大差异,如果能够使用map和set那么使用unordered系列的容器也就没多大难度,反而是底层的不同更值得让我们进行研究。因为代码篇幅太长,所以文章中只是对代码截取,如果需要查看源码的话可以跳转到gitees上查看,gitee链接已放置在文章顶部。码文不易,如果觉得文章对你有帮助的话,你的👍就是对作者最大的鼓励。

在这里插入图片描述

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

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

相关文章

什么是SQL注入(SQL Injection)?如何预防它

什么是 SQL 注入&#xff08;SQL Injection&#xff09;&#xff1f;如何预防它&#xff1f; SQL注入&#xff08;SQL Injection&#xff09;是一种常见的网络安全漏洞&#xff0c;攻击者通过在应用程序的输入中插入恶意SQL代码来执行未经授权的数据库操作。SQL注入攻击可能导…

CSP-J第二轮试题-2020年-4题

文章目录 参考&#xff1a;总结 [CSP-J2020] 方格取数题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 样例 #2样例输入 #2样例输出 #2 提示样例 1 解释 样例 2 解释 数据规模与约定 答案1 现场真题注意事项 参考&#xff1a; P7074 [CSP-J2020] 方格取数 总结 本系…

Docker 自动化部署(实践)

常用命令 docker search jenkins查看需要的jenkins镜像源 docker pull jenkins/jenkins 拉取jenkins镜像 docker images查看下载的镜像源 docker ps 查看包含启动以及未启动的容器 docker ps -a查看启动的容器 docker rm 容器id/容器名称 删除容器 docker rm -f 容器id/容器名…

算法基础课第一部分

算法基础课 第一讲 基础算法快速排序归并排序二分整数二分模板AcWing 789. 数的范围(整数二分法)AcWing 1236.递增三元组AcWing 730. 机器人跳跃问题AcWing 1227. 分巧克力AcWing 1221. 四平方和(二分法/哈希)蓝桥杯-扫地机器人 (二分贪心)AcWing 790. 数的三次方根(浮点二分法…

NSSCTF做题(6)

[HCTF 2018]Warmup 查看源代码得到 开始代码审计 <?php highlight_file(__FILE__); class emmm { public static function checkFile(&$page) { $whitelist ["source">"source.php","hint"…

Java-API简析_java.util.Objects类(基于 Latest JDK)(浅析源码)

【版权声明】未经博主同意&#xff0c;谢绝转载&#xff01;&#xff08;请尊重原创&#xff0c;博主保留追究权&#xff09; https://blog.csdn.net/m0_69908381/article/details/133463511 出自【进步*于辰的博客】 因为我发现目前&#xff0c;我对Java-API的学习意识比较薄弱…

人工智能的学习算法

1956年&#xff0c;几个计算机科学家相聚在达特茅斯会议&#xff0c;提出了 “人工智能” 的概念&#xff0c;梦想着用当时刚刚出现的计算机来构造复杂的、拥有与人类智慧同样本质特性的机器。其后&#xff0c;人工智能就一直萦绕于人们的脑海之中&#xff0c;并在科研实验室中…

K折交叉验证——cross_val_score函数使用说明

在机器学习中&#xff0c;许多算法中多个超参数&#xff0c;超参数的取值不同会导致结果差异很大&#xff0c;如何确定最优的超参数&#xff1f;此时就需要进行交叉验证的方法&#xff0c;sklearn给我们提供了相应的cross_val_score函数&#xff0c;可对数据集进行交叉验证划分…

小程序是一种伪需求技术吗?

点击下方“JavaEdge”&#xff0c;选择“设为星标” 第一时间关注技术干货&#xff01; 免责声明~ 任何文章不要过度深思&#xff01; 万事万物都经不起审视&#xff0c;因为世上没有同样的成长环境&#xff0c;也没有同样的认知水平&#xff0c;更「没有适用于所有人的解决方案…

[NOIP2012 提高组] 开车旅行

[NOIP2012 提高组] 开车旅行 题目描述 小 A \text{A} A 和小 B \text{B} B 决定利用假期外出旅行&#xff0c;他们将想去的城市从 $1 $ 到 n n n 编号&#xff0c;且编号较小的城市在编号较大的城市的西边&#xff0c;已知各个城市的海拔高度互不相同&#xff0c;记城市 …

零基础一站式精通安卓逆向2023最新版(第一天):Android Studio的安装与配置

目录 一、Android Studio 开发环境的下载二、Android Studio 的安装与配置2.1 安装2.2 Android SDK 的管理 三、创建 Android 应用程序补充&#xff1a;安装完 Android Studio 后 SDK 目录下没有 tools 目录 一、Android Studio 开发环境的下载 通常情况下&#xff0c;为了提高…

对pyside6中的textedit进行自定义,实现按回车可以触发事件。

我的实现方法是&#xff0c;先用qt designer写好界面&#xff0c;如下图&#xff1a; 接着将其生成的ui文件编译成为py文件。 找到里面这几行代码&#xff1a; self.textEdit QTextEdit(self.centralwidget)self.textEdit.setObjectName(u"textEdit")self.textEdit…

Vue城市选择器示例(省市区三级)

Vue城市选择器&#xff08;省市区&#xff09; 读者可以参考下面的省市区三级联动代码思路&#xff0c;切记要仔细研究透彻&#xff0c;学习交流才是我们的本意&#xff0c;而非一成不变。切记切记&#xff01; 最近又重读苏子的词&#xff0c;颇为感慨&#xff0c;愿与诸君共…

2022年中国征信行业覆盖人群、参与者数量及征信业务查询量统计[图]

征信是指依法收集、整理、保存、加工自然人、法人及其他组织的信用信息&#xff0c;并对外提供信用报告、信用评估、信用信息咨询等服务&#xff0c;帮助客户判断、控制信用风险&#xff0c;进行信用管理的活动。 征信业主要范畴 资料来源&#xff1a;共研产业咨询&#xff08…

B. Comparison String

题目&#xff1a; 样例&#xff1a; 输入 4 4 <<>> 4 >><< 5 >>>>> 7 <><><><输出 3 3 6 2 思路&#xff1a; 由题意&#xff0c;条件是 又因为要使用尽可能少的数字&#xff0c;这是一道贪心题&#xff0c;所以…

初识多线程

一、多任务 现实中太多这样同时做多件事的例子了&#xff0c;例如一边吃饭一遍刷视频&#xff0c;看起来是多个任务都在做&#xff0c;其实本质上我们的大脑在同一时间依旧只做了一件事情。 二、普通方法调用和多线程 普通方法调用只有主线程一条执行路径 多线程多条执行路径…

uni-app_消息推送_华为厂商_unipush离线消息推送

文章目录 一、创建项目二、生成签名证书三、开通 unipush 推送服务四、客户端集成四、制作自定义调试基座五、开发者中心后台Web页面推送&#xff08;仅支持在线推送&#xff09;六、离线消息推送1、创建华为开发者账号2、开通推送服务3、创建项目4、添加应用5、添加SHA256证书…

【Linux】详解线程第三篇——线程同步和生产消费者模型

线程同步和生消模型 前言正式开始再次用黄牛抢票来讲解线程同步的思想通过条件变量来实现线程同步条件变量接口介绍初始化和销毁pthread_cond_waitsignal和broadcast 生产消费者模型三种关系用基本工程师思维再次理解基于生产消费者模型的阻塞队列版本一版本二多生多消 利用RAI…

2022年全球一次能源消费量:石油消耗量持续增加达190.69百亿亿焦耳,亚太地区消费量居首位[图]

一次性能源是指从自然界取得未经改变或转变而直接利用的能源。如原煤、原油、天然气、水能、风能、太阳能、海洋能、潮汐能、地热能、天然铀矿等。一次性能源又分为可再生能源和不可再生能源&#xff0c;前者指能够重复产生的天然能源&#xff0c;包括太阳能、风能、潮汐能、地…

响应式设计的实现方式

一. 什么是响应式 响应式网站设计是一种网络页面设计布局。页面的设计与开发应当根据用户行为以及设备环境&#xff08;系统平台&#xff0c;屏幕尺寸&#xff0c;屏幕定向等&#xff09;进行相应的响应和调整。 响应式网站常见特点&#xff1a; 1. 同时适配PC平板手机。 2…