哈希结构的代码实现(开散列、闭散列)
创始人
2025-06-01 09:39:25

哈希结构

unordered系列的关联式容器之所以查找效率比较高,是因为其底层使用了哈希结构。

哈希映射,key值跟存储位置建立映射关系。

哈希表散列表(一种存储结构),通过哈希函数散列函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

常见哈希函数

哈希函数设计原则:哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间;哈希函数计算出来的地址能均匀分布在整个空间中;哈希函数应该比较简单。

  1. 直接定址法–(常用)
    取关键字的某个线性函数为散列地址:Hash(Key)=A∗Key+BHash(Key)= A*Key + BHash(Key)=A∗Key+B

    优点:简单、均匀、无哈希冲突;
    缺点:需要事先知道关键字的分布情况,一堆整型值哈希映射到表,如果这些值太分散了(如2,3,12,9999),消耗空间太大;
    使用场景:适合查找整型、值小且连续的情况

  2. 除留余数法–(常用)
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key % p (p<=m),将关键码转换成哈希地址。有哈希冲突。一般p就取哈希表的size()。

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

哈希冲突

概念

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突哈希碰撞。简而言之是指不同的值映射到相同位置。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

如下一组数据3、5、7、12、999、13,当采用除留余数法时,取p=10,Hash(key) = Key % p,(p<=m),当遇到3和13时,3%10=3,13%10=3,那这个哈希映射对于的位置到底存3还是存13呢?

解决方法一:闭散列

也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。闭散列是不会让存储空间为满的状态,依靠负载因子来判断是否满了。

哈希表中的存储节点结构定义为

enum State
{EMPTY,//该位置为空,可以存放新keyEXIST,//该位置已经记录了keyDELETE,//该位置数据被删除,可以存放新key
};template
struct HashData
{std::pair _kv;State _state;//要用来标记该位置的状态,用于停止搜索的逻辑判断,当状态为
};

根据key找映射位置

key为整型等

当Key为整型值时,找对应的映射位置方法:Hash(Key)= Key % _table.size()。此处用size不能用capapcity,因为后续用下标来访问对应位置的数据时,operator[]会强制检查访问的位置是否超出有效数据范围size,在实际应用中,一般会让size==capacity,避免空间浪费。

key为字符型–BKDR思路

当Key为其他类型时,需要进行二次映射,此时借助仿函数强制转换来完成。BKDR思路

template<>//特化
struct HashFunc
{size_t operator()(const std::string& key){size_t hash = 0;for (auto ch : key){hash *= 131;//BKDR思路 每次的结果31、131、13131...hash += ch;}return hash;}
};

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索,因此线性探测采用标记的伪删除法来删除一个元素,即状态置为DELETE。

负载因子–控制扩容

散列表的载荷因子定义为: a = 填入表中的元素个数 / 散列表的长度size。a是散列表装满程度的标志因子。

由于表长是定值,a与“填入表中的元素个数”成正比,所以,a越大,表明填入表中的元素越多,产生冲突的可能性就越大,空间利用率越高;反之,a越小,标明填入表中的元素越少,产生冲突的可能性就越小,空间利用率越低。典型的以空间换时间的做法。

实际上,散列表的平均查找长度是载荷因子a的函数,只是不同处理冲突的方法有不同的函数。
对于开放定址法,荷载因子是特别重要因素,应严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中(cachemissing)按照指数曲线上升。

超过标定的载荷因子,先扩容散列表,再重新建立映射关系

闭散列会占用别的键值对的位置,空间利用率比较低,不太合理。那如何寻找下一个空位置呢?

查找空位置的方法一:线性探测

从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

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

while (_tables[hashi]._state == EXIST)
{//线性探测++hashi;hashi %= _tables.size();//避免越界
}

查找空位置的方法二:二次探测

找下一个空位置的方法为:HiH_iHi​ = (H0H_0H0​ + i2i^2i2 ) % m, 或者:HiH_iHi​ = (H0H_0H0​ - i2i^2i2 ) % m。其中:i =1,2,3…, H0H_0H0​是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小(_tables.size())。

size_t hashi = hf(kv.first) % _tables.size();//Hash()函数

解决方法二:开散列(更优)

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

当使用无序容器存储键值对时,会先申请一整块连续的存储空间,但此空间并不用来直接存储键值对,而是存储各个链表的头指针,各键值对真正的存储位置是各个链表的节点。STL 标准库通常选用 vector 容器存储各个链表的头指针。

Insert()

当有新键值对存储到无序容器中时,整个存储过程分为

  1. 若负载因子是否超过1.0,要扩容;
  2. 将该键值对中键Key带入设计好的哈希函数,会得到一个哈希值(一个整数,用 H 表示);
  3. 将 H 和无序容器拥有桶的数量 n 做整除运算(即 H % n),该结果即表示应将此键值对存储到的桶的编号;
  4. new一个新节点存储此键值对,同时将该节点头插到相应编号的桶上。
//普通版本的插入
bool Insert(const std::pair& kv)
{if (Find(kv.first))return false;if (_tables.size() == _n)//默认情况下,无序容器的最大负载因子为 1.0{//扩容HashTable newHT;newHT._tables.resize(_tables.size() * 2);for (auto cur : _tables){while (cur){newHT.Insert(cur->_kv);//每个节点都是重新new再插入的,当数据量大的时候不合适cur = cur->_next;}}_tables.swap(newHT._tables);}Hash hf;//根据映射关系找下标size_t hashi = hf(kv.first) % _tables.size();Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;
}

普通版本插入的实现,思路是new一个HashTable,让这个表里面的_tables重新扩容,再插入每个节点,能让数据重新排布,减少哈希冲突。不好的地方在于,1、每个节点都是重新new再插入的,当数据量大的时候不合适;2、HashTable有自己的析构函数,当出了函数要调用析构,释放所有节点。==>总的来说,是节点无法重复利用造成的性能消耗。

新的思路是,直接new一个_tables,循环利用旧表节点。对每个旧表节点进行重新映射(这一步不可省略),再头插到新表对应位置,将旧表中存储的指针置空,找不到其他指针,相当于让旧表的析构函数起不了实质性的作用即可。最后交换新旧表即完成扩容。

bool Insert(const std::pair& kv)
{if (Find(kv.first))return false;if (_tables.size() == _n)//默认情况下,无序容器的最大负载因子为 1.0{//扩容std::vector newtables;newtables.resize(_tables.size() * 2, nullptr);for (size_t = 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);//再交换新旧表}Hash hf;//根据映射关系找下标size_t hashi = hf(kv.first) % _tables.size();Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;
}

负载因子

哈希表存储结构有一个重要的属性,称为负载因子(load factor)。该属性同样适用于无序容器,用于衡量容器存储键值对的空/满程序,即负载因子越大,意味着容器越满,即各链表中挂载着越多的键值对,这无疑会降低容器查找目标键值对的效率;反之,负载因子越小,容器肯定越空,但并不一定各个链表中挂载的键值对就越少。

负载因子的计算方法为: 负载因子 = 容器存储的总键值对 / 桶数。

if (_tables.size() == _n)//默认情况下,无序容器的最大负载因子为 1.0

**默认情况下,无序容器的最大负载因子为 1.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;}
}

哈希表的size()最好为素数的处理方法

研究人员认为,当模一个素数时,哈希冲突概率更小。

size_t hashi = hf(kv.first) % _tables.size();

// 除留余数法,最好模一个素数,即要求哈希表的size是素数
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 (int 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];
}

在一些场景下,如数据较大、考虑空间和效率等,桶的底层结构可以由单链表改成红黑树。

开散列与闭散列比较

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

注意map和set、unordermap和unorderset对key的要求不一样,序列容器的要求key能比较大小;无序容器要求key能转换成整数。

相关内容

热门资讯

jp是哪个国家的缩写(jp是哪... 本篇文章极速百科给大家谈谈jp是哪个国家的缩写,以及jp是哪个国家的缩写名字对应的知识点,希望对各位...
苹果手机显示不支持此配件怎么办... 不支持此配件怎么解决 苹果iphone可能不支持此配件怎么办怎么解除不支持此配件 不支持此配件怎么解...
支付宝借呗的利息是多少,蚂蚁借... 支付宝借呗的利息是多少目录支付宝借呗的利息是多少蚂蚁借呗利息是怎么计算的蚂蚁借呗的利息是多少借呗的利...
关于兰字的词语或成语越多越好.... 关于兰字的词语或成语越多越好.目录关于兰字的词语或成语越多越好.有关兰字的成语有哪些关于兰的词语或成...
宝马m5多少钱是不是很贵呢?(... 本篇文章极速百科给大家谈谈宝马m5多少钱是不是很贵呢?,以及宝马m5li多少钱对应的知识点,希望对各...
辽宁省喀左县在哪个城市,辽宁省... 辽宁省喀左县在哪个城市目录辽宁省喀左县在哪个城市辽宁省朝阳市喀左县的邮政编码辽宁省喀左县在哪里辽宁省...
关于marcjacobs香水,... 关于marcjacobs香水目录关于marcjacobs香水marcjacobs香水(探索时尚与艺术...
四级英语考试时间分配,大学英语... 四级英语考试时间分配目录四级英语考试时间分配大学英语四级考多长时间?英语四级考试时间安排?英语四级考...
dnfbuff强化有什么用,地... dnfbuff强化有什么用目录dnfbuff强化有什么用地下城buff强化栏DNF中人物的Buff有...
幼儿园孩子新年祝福语简短,适合... 幼儿园孩子新年祝福语简短目录幼儿园孩子新年祝福语简短适合幼儿园小朋友说的新年祝福语幼儿园老师给小朋友...
正断层有哪些断层组合类型,断层... 正断层有哪些断层组合类型目录正断层有哪些断层组合类型断层的组合类型简答题 断层的类型及组合形式有哪些...
腿皮肤干燥起皮怎么办,腿上起干... 腿皮肤干燥起皮怎么办目录腿皮肤干燥起皮怎么办腿上起干皮怎么办腿干燥脱皮怎么办?腿上皮肤干燥起皮怎么办...
怎么用qq号注册微信,如何用q... 怎么用qq号注册微信目录怎么用qq号注册微信如何用qq号注册微信号呢?微信用qq号怎么注册?怎么用q...
招聘技巧和方法有哪些,招聘途径... 招聘技巧和方法有哪些目录招聘技巧和方法有哪些招聘途径和方法有哪些最有效的招聘方法有哪些?招聘的渠道和...
美团怎么评价,美团外卖怎么给好... 美团怎么评价目录美团怎么评价美团外卖怎么给好评?美团30字通用好评语有哪些?美团怎么评价商家的菜品呢...
龙虾片可以用空气炸锅吗,空气炸... 龙虾片可以用空气炸锅吗目录龙虾片可以用空气炸锅吗空气炸锅龙虾片用空气炸锅炸虾片的做法分享空气炸锅能炸...
小楼是什么烟(小楼多少钱一条)... 本篇文章极速百科给大家谈谈小楼是什么烟,以及小楼多少钱一条对应的知识点,希望对各位有所帮助,不要忘了...
电动汽车的品牌有那些?(电动汽... 本篇文章极速百科给大家谈谈电动汽车的品牌有那些?,以及电动汽车的品牌有哪些?对应的知识点,希望对各位...
全开麦和半开麦区别是什么,半开... 全开麦和半开麦区别是什么目录全开麦和半开麦区别是什么半开麦和全开麦的区别一般韩国音乐节目中歌手用的麦...
转机的流程是什么,飞机中转需要... 转机的流程是什么目录转机的流程是什么飞机中转需要什么流程坐飞机中转机需要注意什么,怎么转?飞机中转需...
什么的田野填空三字词语,什么的... 什么的田野填空三字词语目录什么的田野填空三字词语什么的田野 什么的田野如何组词恰当的词语()的田野什...
wps排序怎么操作步骤,wps... wps排序怎么操作步骤目录wps排序怎么操作步骤wps怎么排序wps表格可以设置排序么?wps怎么自...
中秋节思念家人的诗句,八月十五... 中秋节思念家人的诗句目录中秋节思念家人的诗句八月十五思念亲人的诗词中秋节思念亲人的诗句。中秋节思念家...
新买的苹果手机怎么激活,苹果手... 4. 同意条款和条件:在屏幕上出现的界面中,滑动屏幕并同意苹果的条款和条件。 5. 选择语言和...
烯丙基取代和加成的条件 极速百... 烯丙基取代和加成的条件目录烯丙基取代和加成的条件烯丙基取代和加成的条件
薛平贵与王宝钏结局,薛平贵与王... 薛平贵与王宝钏结局目录薛平贵与王宝钏结局薛平贵与王宝钏大结局????薛平贵与王宝钏结局 薛平贵...
给自己背影唯美简短一句话,给背... 给自己背影唯美简短一句话目录给自己背影唯美简短一句话给背影的唯美简短说说拍了自己的背影要怎么发说说给...
万宝路薄荷香烟有几种,万宝路薄... 万宝路薄荷香烟有几种目录万宝路薄荷香烟有几种万宝路薄荷脆味道如何?万宝路常见的薄荷味香烟有哪些万宝路...
石家庄是哪个省的 极速百科网 ... 石家庄是哪个省的目录石家庄是哪个省的石家庄是哪个省的 石家庄是河北省的省会,是国务院批复确定的...
怎么双开软件,如何实现应用程序... 怎么双开软件目录怎么双开软件如何实现应用程序的双开怎么双开软件 操作步骤以分身大师为例: ...