【C++:哈希表】从哈希冲突到负载因子:熟悉哈希表的核心机制

【C++:哈希表】从哈希冲突到负载因子:熟悉哈希表的核心机制

🔥艾莉丝努力练剑:个人主页

专栏传送门:《C语言》《数据结构与算法》C/C++干货分享&学习过程记录Linux操作系统编程详解笔试/面试常见算法:从基础到进阶测试开发要点全知道

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬艾莉丝的简介:


🎬艾莉丝的C++专栏简介:


目录

C++的两个参考文档

前情提示

1  ~>  初始哈希

2  ~>  直接定址法

2.1  概念

2.2  示例:字符串中的第一个唯一字符

3  ~>  哈希的一些概念

3.1  哈希冲突

3.2  负载因子

3.3  将关键字转为整数

4  ~>  哈希函数

4.1  除法散列法 / 除留余数法

4.2  乘法散列法(了解即可)

4.3  全域散列法(了解即可)

4.4  其他方法(了解即可)

5  ~>  处理哈希冲突

5.1  开放定址法  

5.1.1  线性探测(含堆积问题)

5.1.2  二次探测

5.1.3  双重探测

5.2  详解开放定址法代码

5.2.1  哈希表结构

5.2.2  扩容问题

5.2.3  key不能取模的问题

5.3  链地址法

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

5.3.2  哈希桶概念及其示例

5.3.3  扩容

5.3.4  极端场景  

5.4  哈希桶代码实现

5.4.0  在私有定义成员变量

5.4.1  插入

5.4.2  查找 

5.4.3  删除

完整代码示例与实践演示

HashTable.h:

Test.cpp:

运行结果

open_address::TestHT1():

open_address::TestHT2():

Hash_bucket::TestHT1():

Hash_bucket::TestHT2():

博主手记

结尾


C++的两个参考文档

老朋友(非官方文档):cplusplus

准官方文档(同步更新):C++ 官方参考文档
set和multiset的参考文档:setmultiset

map和multimap的参考文档:mapmultimap
unordered_set和unordered_multiset的参考文档:unordered_setunordered_multiset


前情提示


1  ~>  初始哈希

哈希(hash)又称散列,故哈希表又称散列表,是一种组织数据的方式。哈希是音译名,从译名来看,有散乱排列(散列)的意思。哈希的本质就是通过哈希函数把关键字Key跟存储位置建立一个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进行快速查找。


2  ~>  直接定址法

2.1  概念

当关键字的范围比较集中时,直接定址法是非常简单高效的方法:比如一组关键字都在[0,99]之间,那么我们开一个100个数的数组,每个关键字的值直接就是存储位置的下标。再比如一组关键字值都在[a,z]的小写字母,那么我们开一个26个数的数组,每个关键字acsii码-aascii码就是存储位置的下标。也就是说直接定址法本质就是用关键字计算出一个绝对位置或者相对位置。这个方法我们不仅在计数排序部分用过,在string部分的OJ题目那里也用过了。

2.2  示例:字符串中的第一个唯一字符

力扣链接:387. 字符串中的第一个唯一字符

题目描述:

class Solution { public: int firstUniqChar(string s) { // 每个字⺟的ascii码-'a'的ascii码作为下标映射到count数组,数组中存储出现的次数 int count[26] = { 0 }; // 统计次数 for (auto ch : s) { count[ch - 'a']++; } for (size_t i = 0; i < s.size(); ++i) { if (count[s[i] - 'a'] == 1) return i; } return -1; } };

3  ~>  哈希的一些概念

3.1  哈希冲突

直接定址法的缺点也非常明显,当关键字的范围比较分散时,就很浪费内存甚至内存不够用。假设我们只有数据范围是[0,9999]的N个值,我们要映射到一个M个空间的数组中(一般情况下M >= N),那么就要借助哈希函数(hash function)hf,关键字key被放到数组的h(key)位置,这里要注意的是h(key)计算出的值必须在[O , M)之间。

这里存在的一个问题就是,两个不同的key可能会映射到同一个位置去,这种问题我们叫做哈希冲突,或者哈希碰撞。理想情况是找出一个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的,所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的方案。

3.2  负载因子

假设哈希表中已经映射存储了N个值,哈希表的大小为M,那么负载因子 = N / M,负载因子有些地方也翻译为载荷因子/装载因子等,他的英文为loadfactor。负载因子越大,哈希冲突的概率越高,空间利用率越高;负载因子越小,哈希冲突的概率越低,空间利用率越低。

3.3  将关键字转为整数

我们将关键字映射到数组中位置,一般是整数好做映射计算,如果不是整数,我们要想办法转换成整数,这个细节我们后面代码实现中再进行细节展示。下面哈希函数部分我们讨论时,如果关键字不是整数,那么我们讨论的Key是关键字转换成的整数。


4  ~>  哈希函数

4.1  除法散列法 / 除留余数法

1、除法散列法也叫做除留余数法,顾名思义,假设哈希表的大小为M,那么通过key除以M的余数作为映射位置的下标,也就是哈希函数为:h(key) = key % M

2、当使用除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等。如果是2^X,key % 2^X本质相当于保留key的后X位(后X位相同的值),计算出的哈希值都是一样的——就冲突了。比如:[63,31}看起来没有关联的值,如果M是16,也就是2^4,那么计算出的哈希值都是15,因为63的二进制后8位是00111111,31的二进制后8位是00011111。如果是10^x,就更明显了,保留的都是10进值的后X位,如:[112,12312},如果M是100(10^2),计算出的哈希值都是12。

3、当使用除法散列法时,建议M取不太接近2的整数次幂的一个质数(素数),什么是质数?可以参考下面的概念——

下面的内容仅做了解,大家可以选择跳过哦!前面第三点建议M取一个不太接近2的整数次幂的质数,Java中,HashMap采用除法散列法时就是2的整数次幂做哈希表的M,这样一来我们可以直接位运算,相对取模会更加高效些。只是Java不是单纯地取模,比如说M是2^16,本质上是取后16位,那么用key' = key >> 16,然后把key和key'异或的结果作为哈希值,也就是说:我们可以映射的值在[0 , M]范围内,尽可能让key所有的位都参与计算,这样映射出的哈希值更加均匀一些,因此,艾莉丝前面建议“M取不大接近2的整数次幂的一个质数”,大多数数据结构书籍中写的基本是这个理论,我们实践中还是建议灵活运用。

4.2  乘法散列法(了解即可)

乘法散列法对哈希表大小M没有要求,这里介绍一下大思路,第一步:用关键字K乘上常数A(0 < A < 1),并抽取出k*A的小数部分第二步:后再用M乘以k * A的小数部分,再向下取整

h(key) = floor(M * ((A * key) % 1.0)),其中floor表示对表达式进行下取整,A

\in

(0 , 1),%1.0是为了取小数,这里最重要的是A的值应该如何设定,Knuth——这又是一位大佬——他认为A = (5 - 1) / 2 = 0.6180339887...(黄金分割点)比较好。

乘法散列法对哈希表大小M是没有要求的,假设M为1024,key为1234,A = 0.6180339887,A * key = 762.6539420558,取小数部分为0.6539420558,M * ((A * key) % 1.0) = 0.6539420558*1024 = 669.6366651392,那么h(1234) = 669。

4.3  全域散列法(了解即可)

如果存在这样一个恶意的对手,他针对我们提供的散列函数,特意构造出一个发生严重冲突的数据集,比如,让所有关键字全部落入同一个位置中——这种情况是可以存在的,只要散列函数是公开且确定的,就可以实现此攻击。解决方法自然是见招拆招,给散列函数增加随机性,攻击者就无法找出确定可以导致最坏情况的数据。这种方法叫做全域散列

hab(key) = ((a * key + 6) % P) % M,P需要选一个足够大的质数,a可以随机选[1 , P - 1]之间的
任意整数,b可以随机选[0 , P - 1]之间的任意整数,这些函数构成了一个P * (P - 1)组全域散列函数组。假设P = 17,M = 6,a = 3,b = 4,则h34(8) = ((3 * 8 + 4) % 17) % 6 = 5。

需要注意的是每次初始化哈希表时,随机选取全域散列函数组中的一个散列函数使用,后续增删查
改都固定使用这个散列函数,否则每次哈希都是随机选一个散列函数,那么插入是一个散列函数,
查找又是另一个散列函数,就会导致找不到插入的key了。

4.4  其他方法(了解即可)

上面的几种方法是在《算法导论》这本书籍中讲解的方法。

《殷人昆数据结构:用面向对象方法与C++语言描述 (第二版)》和《[数据结构 (C语言版).严蔚
敏,吴伟民》等教材型书籍上面还给出了平方取中法、折叠法、随机数法、数学分析法等,这些方
法相对更适用于一些局限的特定场景,大家如果有兴趣可以去看看这些书籍。


5  ~>  处理哈希冲突

实践中哈希表一般还是选择除法散列法作为哈希函数,当然哈希表无论选择什么哈希函数也避免不了冲突,因为冲突是避免不了的,我们只能减少冲突,那么插入数据时,如何解决冲突呢?主要有两种方法,开放定址法和链地址法

5.1  开放定址法  

在开放定址法中所有的元素都放到哈希表里,当一个关键字key用哈希函数计算出的位置冲突了,则按照某种规则找到一个没有存储数据的位置进行存储,开放定址法中负载因子一定是小于的。这里的规则有三种:线性探测、二次探测、双重探测

 

 

5.1.1  线性探测(含堆积问题)

我们先简单谈谈“堆积 / 群积问题”——

如下图所示——

5.1.2  二次探测

5.1.3  双重探测

5.2  详解开放定址法代码

开放定址法在实践中是不如下面会介绍的链地址法的,因为开放定址法解决冲突不管使用哪种方法,占用的都是哈希表中的空间,始终存在互相影响的问题——正因如此,开放定址法我们简单选择线性探测实现即可。

5.2.1  哈希表结构

enum State { EXIST, EMPTY, DELETE }; template<class K, class V> struct HashData { pair<K, V> _kv; State _state = EMPTY; }; template<class K, class V> class HashTable { private: vector<HashData<K, V>> _tables; size_t _n = 0; // 表中存储数据个数 };

要注意的是这里需要给每个存储值的位置加一个状态标识,否则删除一些值以后,会影响后面冲突的值的查找。如下图所示,我们删除30,会导致查找20失败,当我们给每个位置加一个状态标识
{EXIST,EMPTY,DELETE},删除30就可以不用删除值,而是把状态改为DELETE,那么查找20
时是遇到EMPTY才能,就可以找到20。

h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) = 10,h(12) = 1。

5.2.2  扩容问题

这里我们哈希表负载因子控制在0.7,当负载因子到0.7以后我们就需要扩容了,我们还是按照2倍的方式扩容,但是同时我们要保持哈希表大小是一个质数,第一个是质数,2倍后就不是质数了。如何解决?一种方案就是上面在【除法散列法】中我们介绍过的JavaHashMap的使用2的整数次幂,但是计算时不能直接取模的改进方法;另外一种方案是SGI版本的哈希表使用的方法,给了一个近似2倍的质数表,每次去质数表获取扩容后的大小。

5.2.3  key不能取模的问题

  当key是string / Date等类型时,key不能取模,我们需要给HashTable增加一个仿函数,这个仿函数支持把key转换成一个可以取模的整型,如果key可以转换为整型并且不容易冲突,那么这个仿函数就用默认参数即可,如果这个Key不能转换为整型,我们就需要自己实现一个仿函数传给这个参数,实现这个仿函数的要求就是尽量key的每值都参与到计算中,让不同的key转换出的整型值不同。string做哈希表的key非常常见,所以我们可以考虑把string特化一下——

template<class K> struct HashFunc { size_t operator()(const K& key) { return (size_t)key; } }; // 特化 template<> struct HashFunc<string> { // 字符串转换成整形,可以把字符ascii码相加即可 // 但是直接相加的话,类似"abcd"和"bcad"这样的字符串计算出是相同的 // 这里我们使⽤BKDR哈希的思路,⽤上次的计算结果去乘以一个质数,这个质数一般去31, 131等效果会比较好 size_t operator()(const string& key) { size_t hash = 0; for (auto e : key) { hash *= 131; hash += e; } return hash; } }; template<class K, class V, class Hash = HashFunc<K>> class HashTable { public: private: vector<HashData<K, V>> _tables; size_t _n = 0; // 表中存储数据个数 };

5.3  链地址法

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

对比一下——

5.3.2  哈希桶概念及其示例

开放定址法中所有的元素都放到哈希表里,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储一个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,把这些冲突的数据链接成一个链表,挂在哈希表这个位置下面,像是一个个挂在晾衣杆的水桶,链地址法也叫做拉链法或者哈希桶。

以{19,30,5,36,13,20,21,12,24,96}这一组值为例,映射到M = 11的表中——

h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) = 10,

h(12) = 1,h(24) = 2,h(96) = 88

5.3.3  扩容

开放定址法负载因子必须小于1,链地址法的负载因子就没有限制了,可以大于1。负载因子越大,哈希冲突的概率越高,空间利用率越高;负载因子越小,哈希冲突的概率越低,空间利用率越低。stl中unordered_xxx的最大负载因子基本控制在1(负载因子平均是1,但是这是理想的情况,当然没有那么平均,有的哈希桶不挂,有的挂2~3个),大于1就扩容,艾莉丝之后在代码实现中也使用这个方式进行扩容。

扩容有分散的意思在——平均每个桶的长度变短。

5.3.4  极端场景  

如果在极端场景下,某个桶特别长怎么办?我们其实可以考虑使用全域散列法,这样就不容易被针对了。假设不是被针对了,此时虽然用了全域散列法,但是在偶然情况下,某个桶很长,查找效率很低怎么办呢?这里在Java8的HashMap中,当桶的长度超过一定阀值(8)时就把链表转换成红黑树,我们知道红黑树是近似平衡,时间复杂度很稳定,O(logN),而哈希在插入的时候会“抽风”,而且随着插入次数增加,“抽风”程度会愈演愈烈——

不过,一般情况下,不断扩容,单个桶很长的场景还是比较少的,这个解决极端场景的思路,uu们这里可以了解一下。

换红黑树也是一种解决方案。

5.4  哈希桶代码实现

5.4.0  在私有定义成员变量

5.4.1  插入

5.4.2  查找 

 

 

5.4.3  删除


完整代码示例与实践演示

HashTable.h:

#pragma once #include<vector> 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 }; inline unsigned long __stl_next_prime(unsigned long n) { const unsigned long* first = __stl_prime_list; const unsigned long* last = __stl_prime_list + __stl_num_primes; // >= n const unsigned long* pos = lower_bound(first, last, n); return pos == last ? *(last - 1) : *pos; } template<class K> struct HashFunc { size_t operator()(const K& key) { return (size_t)key; } }; // 特化 template<> struct HashFunc<string> { size_t operator()(const string& key) { size_t hash = 0; for (auto ch : key) { hash += ch; hash *= 131; } return hash; } }; namespace open_address { enum State { EMPTY, EXIST, DELETE }; template<class K, class V> struct HashData { pair<K, V> _kv; State _state = EMPTY; }; template<class K, class V, class Hash = HashFunc<K>> class HashTable { public: HashTable() :_tables(__stl_next_prime(1)) { } bool Insert(const pair<K, V>& kv) { if (Find(kv.first)) return false; // 满了 / 快满了就要扩容,负载因子 >= 0.7就要扩容 if ((double)_n / (double)_tables.size() >= 0.7) // 至少强转一个 { //vector<HashData<K, V>> newTables(__stl_next_prime(_tables.size() * 2)); // //_tables.resize(); // 扩容可以直接resize吗?哈希表的扩容不是那么简单的,要重新分配 // std::vector<HashData> newtables(_tables.size()); // for (size_t i = 0; i < _tables, size(); i++) // { // if (_tables[i]._state == EXIST) // { // // 重新映射到新表 // // ... // } // } // // _tables.swap(newtables); HashTable<K, V, Hash> newht; newht._tables.resize(__stl_next_prime(_tables.size() + 1)); for (size_t i = 0; i < _tables.size(); i++) { // 遍历旧表,旧表数据插入到newht if (_tables[i]._state == EXIST) { newht.Insert(_tables[i]._kv); } } _tables.swap(newht._tables); } Hash hs; // 插入的逻辑 size_t hash0 = hs(kv.first) % _tables.size(); // 只能模size,不能模capacity // []访问必须要在size访问之内,模capacity放不进去(尽可能让capacity和size一致) // 线性探测 size_t i = 1; // 第一次就不加了 size_t hashi = hash0; while (_tables[hashi]._state == EXIST) { hashi = (hash0 + i) % _tables.size(); // 取模,回绕回去 ++i; // 不断加i } _tables[hashi]._kv = kv; _tables[hashi]._state = EXIST; ++_n; return true; } HashData<K, V>* Find(const K& key) { Hash hs; size_t hash0 = hs(key) % _tables.size(); //线性探测 size_t i = 1; size_t hashi = hash0; while (_tables[hashi]._state != EMPTY) { if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key) { return &_tables[hashi]; } hashi = (hash0 + i) % _tables.size(); ++i; } return nullptr; } bool Erase(const K& key) { HashData<K, V>* ret = Find(key); if (ret) { ret->_state = DELETE; --_n; return true; } else { return false; } } private: std::vector<HashData<K, V>> _tables; // 指针数组 size_t _n = 0; // 存储的有效数据个数 }; } namespace Hash_bucket { template<class K,class V> struct HashNode { pair<K, V> _kv; HashNode<K, V>* _next; HashNode(const pair<K,V>& kv) :_kv(kv) ,_next(nullptr) {} }; template<class K, class V,class Hash = HashFunc<K>> class HashTable { typedef HashNode<K, V> Node; public: HashTable() :_tables(__stl_next_prime(1),nullptr) ,_n(0) { } // 析构,要单独实现 ~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; } _n = 0; } bool Insert(const pair<K, V>& kv) { if (Find(kv.first)) return false; Hash hs; // 除余都要套上hs // 负载因子 == 1就开始扩容 if (_n == _tables.size()) { //HashTable<K, V> newht; //newht._tables.resize(_tables.size() * 2); //for (size_t i = 0; i < _tables.size(); i++) //{ // // 遍历旧表,旧表数据插入到newht // Node* cur = _tables[i]; // while (cur) // { // newht.Insert(cur->_kv); // cur = cur->_next; // } //} //_tables.swap(newht._tables); std::vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr); for (size_t i = 0; i < _tables.size(); i++) { // 遍历旧表,旧表节点重新映射,挪动到新表 Node* cur = _tables[i]; while (cur) { Node* next = cur->_next; // 头插 size_t hashi = hs(cur->_kv.first) % newtables.size(); cur->_next = newtables[hashi]; newtables[hashi] = cur; cur = next; } _tables[i] = nullptr; } _tables.swap(newtables); } size_t hashi = hs(kv.first) % _tables.size(); // 头插 Node* newnode = new Node(kv); newnode->_next = _tables[hashi]; _tables[hashi] = newnode; ++_n; // 插入,_n是有效数据个数,要++ return true; } Node* Find(const K& key) { Hash hs; size_t hashi = hs(key) % _tables.size(); Node* cur = _tables[hashi]; while (cur) { if (cur->_kv.first == key) { return cur; } cur = cur->_next; } return nullptr; } bool Erase(const K& key) { Hash hs; size_t hashi = hs(key) % _tables.size(); Node* prev = nullptr; Node* cur = _tables[hashi]; while (cur) { if (cur->_kv.first == key) { // 删除 if (prev == nullptr) { // 哈希桶中的第一个节点 _tables[hashi] = cur->_next; } else { prev->_next = cur->_next; } --_n; // _n是有效数据个数,每次删除之后都要减减 delete cur; return true; } prev = cur; cur = cur->_next; } return false; } private: std::vector<Node*> _tables; // 指针数组 size_t _n; // 存储的有效数据个数 //std::vector<std::list<K, V>> _tables; // 不是实现不了,而是这种实现太绕了,而且比较抽象,现阶段对我们来说还是太难了 }; }

Test.cpp:

#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<unordered_map> using namespace std; #include"HashTable.h" namespace open_address { void TestHT1() { int a[] = { 19,30,5,36,13,20,21,12,58 }; HashTable<int, int> ht; for (auto e : a) { ht.Insert({ e,e }); } ht.Insert({ 2,2 }); ht.Insert({ 22,22 }); cout << ht.Find(5) << endl; cout << ht.Find(58) << endl; ht.Erase(5); cout << ht.Find(5) << endl; cout << ht.Find(58) << endl; //for (size_t i = 0; i < 100; i++) //{ // ht.Insert({ rand(),i }); //} } struct HashFuncString { // BKDR size_t operator()(const string& key) { size_t hash = 0; for (auto ch : key) { hash += ch; hash *= 131; } return hash; } }; void TestHT2() { //HashTable<string, string, HashFuncString> dict; HashTable<string, string> dict; dict.Insert({ "string","字符串" }); // string无法取模 dict.Insert({ "string","字符串1" }); dict.Insert({ "left","左边" }); dict.Insert({ "right","右边" }); cout << dict.Find("string") << endl; cout << dict.Find("left") << endl; cout << dict.Find("left ") << endl; HashFuncString hfs; cout << hfs("abcd") << endl; cout << hfs("acbd") << endl; cout << hfs("aadd") << endl; unordered_map<string, string> dictmap; dictmap.insert({ "string","字符串" }); // 编译报错,需要自己实现Hash的仿函数把key转成整形 //unordered_map<pair<string, int>, string> um; //um.insert({ {"string", 1}, "字符串" }); } } namespace Hash_bucket { void TestHT1() { int a[] = { 19,30,5,36,13,20,21,12,58 }; HashTable<int, int> ht; for (auto e : a) { ht.Insert({ e,e }); } ht.Insert({ 2,2 }); ht.Insert({ 22,22 }); ht.Insert({ 44,44 }); // 这两个过了就说明代码没问题了:先删58再删36 ht.Erase(58); ht.Erase(36); } void TestHT2() { HashTable<string, string> dict; dict.Insert({ "string","字符串" }); // string无法取模 dict.Insert({ "string","字符串1" }); dict.Insert({ "left","左边" }); dict.Insert({ "right","右边" }); cout << dict.Find("string") << endl; cout << dict.Find("left") << endl; cout << dict.Find("left ") << endl; } } int main() { // open_address::TestHT1(); //open_address::TestHT2(); Hash_bucket::TestHT1(); //Hash_bucket::TestHT2(); return 0; }

运行结果

open_address::TestHT1():

open_address::TestHT2():

Hash_bucket::TestHT1():

Hash_bucket::TestHT2():


博主手记

这是博主的学习笔记,大家可以看看——


结尾

uu们,本文的内容到这里就全部结束了,艾莉丝再次感谢您的阅读!

往期回顾:

【C++:哈希表封装】用哈希表封装unordered_map和unordered_set

结语:既然都看到这里啦!请大佬不要忘记给博主来个“一键四连”哦! 

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡

૮₍ ˶ ˊ ᴥ ˋ˶₎ა


Read more

【Linux】Linux 进程通信:System V 共享内存(最快方案)C++ 封装实战 + 通信案例,4 类经典 Bug 快速修复

【Linux】Linux 进程通信:System V 共享内存(最快方案)C++ 封装实战 + 通信案例,4 类经典 Bug 快速修复

前言:欢迎各位光临本博客,这里小编带你直接手撕**,文章并不复杂,愿诸君**耐其心性,忘却杂尘,道有所长!!!! IF’Maxue:个人主页  🔥 个人专栏: 《C语言》 《C++深度学习》 《Linux》 《数据结构》 《数学建模》 ⛺️生活是默默的坚持,毅力是永久的享受。不破不立! 文章目录 * 二、System V共享内存:最快的进程间通信 * 1. System V共享内存核心概念 * 2. System V共享内存原理 * (1)进程虚拟地址空间结构 * (2)共享内存映射过程 * (3)共享内存的管理:先描述,再组织 * 3. System V共享内存核心接口 * (1)生成唯一Key:ftok * (2)创建/获取共享内存:shmget

By Ne0inhk
【Linux网络系列】:JSON+HTTP,用C++手搓一个web计算器服务器!

【Linux网络系列】:JSON+HTTP,用C++手搓一个web计算器服务器!

🔥 本文专栏:Linux网络Linux实践系列 🌸作者主页:努力努力再努力wz 💪 今日博客励志语录:别害怕选错,人生最遗憾的从不是‘选错了’,而是‘我本可以’。每一次推倒重来的勇气,都是在给灵魂贴上更坚韧的勋章。 ★★★ 本文前置知识: 序列化与反序列化 引入 在之前的博客中,我详细介绍了序列化 与反序列化 的概念。对于使用 TCP 协议进行通信的双方,由于 TCP 是面向字节流的,在发送数据之前,我们通常需要定义一种结构化的数据来描述传输内容,并以此作为数据的容器。在 C++ 中,这种结构化数据通常表现为对象或结构体。然而,我们不能直接将结构体内存中对应的字节原样发送到另一端,因为直接传递内存字节会引发字节序 和结构体内存对齐 的问题。不同平台、不同编译器所遵循的内存对齐规则可能不同,这可能导致接收方在解析结构体字段时出现错误。 因此,我们需要借助序列化 。序列化 是指将结构化的数据按照预定的规则转换为连续的字节流。其主要目的是屏蔽平台差异,使得位于不同平台的进程能够以统一的方式解析该字节流。序列化通常分为两种形式:文本序列化 与二进制序列化 。 文

By Ne0inhk
【算法一周目】位间流转,数字律动——洞察 C++ 位运算中的精妙与哲思

【算法一周目】位间流转,数字律动——洞察 C++ 位运算中的精妙与哲思

文章目录 * 常见位运算 * 1. 位1的个数 * 2. 比特位计数 * 3.汉明距离 * 4. 只出现一次的数字 * 5. 只出现一次的数字 III * 6. 只出现一次的数字 II * 7. 判定字符是否唯一 * 8. 丢失的数字 * 9. 两整数之和 * 10. 只出现一次的数字 II 常见位运算 1. 判断一个数的二进制表示的第x位是0还是1 * (n >> x) & 1 2. 将一个数的二进制表示的第x位修改成1 * n |= (1 << x) 3. 将一个数的二进制表示的第x位修改成0 * n &= ~ (1 << x) 4.

By Ne0inhk
探索实现C++ STL容器适配器:优先队列priority_queue

探索实现C++ STL容器适配器:优先队列priority_queue

前引: 在算法竞赛中,选手们常常能在0.01秒内分出胜负;在实时交易系统中,毫秒级的延迟可能意味着数百万的盈亏;在高并发服务器中,每秒需要处理数万条不同优先级的请求——这些系统背后,都隐藏着同一种强大的数据结构: 优先队列(priority_queue) 作为C++标准库中最优雅的数据结构适配器,priority_queue完美封装了堆算法的高效性,却只需几行代码即可实现复杂优先级管理。本文将深入剖析: (1) 堆原理与优先队列的机械美学 (2)定制化优先级策略的高级技巧 (3)实战性能对比与编译器级优化 (4)在万亿级数据处理中的独特优势 目录 优先队列介绍 优先队列的渊源 实例化 插入元素 访问元素 获取元素个数 判断优先队列是否为空 删除堆顶元素 · 优先队列的模拟实现 类模板 插入元素 访问元素 获取元素个数 判断优先队列是否为空 删除堆顶元素 效果展示 优先队列介绍 优先队列priority_queue 是 C++ 标准模板库(

By Ne0inhk