news 2026/7/12 18:40:13

c++: 哈希表的实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
c++: 哈希表的实现



一、哈希的概念

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

二、哈希的实现方法

2.1直接定址法

就是给出的范围是一个比较集中的范围时,就可以用一个确定的数组去映射对应的关系,比如下面这道题: 387:字符串中第一个唯一字符
这道题的思路可以是直接定址法,因为26个字母很集中并且范围小,开销的空间也不大

classSolution{public:intfirstUniqChar(string s){intcount[26]={0};for(autoch:s){count[ch-'a']++;}for(inti=0;i<s.size();++i){if(count[s[i]]==1){returni;}}return-1;}

2.2开放定址法

我们可知当一些数据不集中时,映射到数组的对应下标的空间开销很大。就可能导致栈溢出并且很浪费内存,所以要解决这个难题需要以下的方法

2.2.1哈希函数

⼀个好的哈希函数应该让N个关键字被等概率的均匀的散列分布到哈希表的M个空间中,但是实际中却很难做到,但是我们要尽量往这个⽅向去考量设计。

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

简单来说,有N个数据,哈希表的空间为M,就是把当前数据(key)要通过Key%M的方式,把Key映射到M以内的空间中,但这也会面临一个问题,就是可能多个值是映射到同一个空间中,这就是哈希冲突

2.2.3解决哈希冲突

1.当使⽤除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等。如果是 ,那么key%本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了。如:{63 , 31}看起来没有关联的值,如果M是16,也就是2^4 ,那么计算出的哈希值都是15,因为63的⼆进制后8位是00111111,31的⼆进制后8位是00011111。如果是10^X,就更明显了,保留的都是10进值的后x位,如:{112,12312},如果M是100,也就是 ,那么计算出的哈希值都12
,所以,当使⽤除法散列法时,建议M取不太接近2的整数次幂的⼀个质数(素数)
但再怎么样哈希冲突都是不可避免的,只能通过这样的方式尽可能的减少哈希冲突

2.2.4负载因子

假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,那么负载因⼦=N/M,负载因⼦有些地⽅也翻译为载荷因⼦/装载因⼦等,他的英⽂为loadfactor。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低

2.2.5开放定址法的实现思路

了前面的知识铺垫,当⼀个关键字key⽤哈希函数计算出的位置冲突了,则按照某种规则找到⼀个没有存储数据的位置进⾏存储,开放定址法中负载因⼦⼀定是⼩于1的。这⾥的规则有三种:线性探测、⼆次探测、双重探测。这里主要讲的实现方法是线性探测

2.2.6 线性探测

如果通过哈希函数算出来的空间已经存在,也就是这个值会发生哈希冲突,那就从哈希冲突的位置的下一个位置进行探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛到哈希表尾,则回绕到哈希表头的位置。
所以公式为:
h(key) = hash0 = key % M
hc(key,i) = hashi = (hash0+i) % M, i = {1,2,3,…,M −1};最多探测M-1次,但有负载因子的存在,会在存一定量数据后进行扩容,所以几乎不可能达到最坏情况

三、哈希表的具体实现(存pair的实现版)

哈希表的基本框架

enumstatus{EXIST,DELETE,EMPTY};template<classK,classV>structHashData{pair<K,V>_kv;status _status=EMPTY;};template<classK>structHashFunc{size_toperator()(constK&key){return(size_t)key;}};template<>structHashFunc<string>{size_toperator()(conststring&key){size_t keyi=0;for(autoe:key){keyi+=e;keyi*=131;}returnkeyi;}};template<classK,classV,classHash=HashFunc<K>>classHashTable{public:HashTable(){_table.resize(53);}inlineunsignedlong__stl_next_prime(unsignedlongn){// Note: assumes long is at least 32 bits.staticconstint__stl_num_primes=28;staticconstunsignedlong__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};constunsignedlong*first=__stl_prime_list;constunsignedlong*last=__stl_prime_list+__stl_num_primes;constunsignedlong*pos=lower_bound(first,last,n);returnpos==last?*(last-1):*pos;}private:vector<HashData<K,V>>_table;size_t n=0;};

首先:__stl_next_prime是M扩容的容量选择,这是源码库里面的值,这些质数能更好的降低哈希冲突,vector中为什么要存HashData,不用pair值,因为删除操作vector只能删尾部的值,你想删任意值做不到,所以需要用状态来代替删除,并且查找的时候 ,也是以状态来查所以就用个类来存储pair值和该值的状态
仿函数的实现就是:因为哈希表的建立实现要在key能转化为整数去能进行计算才可以,所以面对字符串的储存需要将字符串转为整数,然后通过哈希函数和线性探测来确定位置

3.1insert的实现

boolinsert(constpair<K,V>&key){if(find(key.first)){returnfalse;}//扩容问题if((double)n/(double)_table.size()>0.7){HashTable<K,V,Hash>_newtable;_newtable._table.resize(__stl_next_prime(_table.size()+1));for(autoe:_table){if(e._status==EXIST){_newtable.insert(e._kv);}}_table.swap(_newtable._table);}Hash ha;size_t hash0=ha(key.first)%_table.size();size_t hashi=hash0;inti=1;while(_table[hashi]._status==EXIST){hashi=(hash0+i)%_table.size();++i;}_table[hashi]._status=EXIST;_table[hashi]._kv=key;++n;returntrue;}

扩容逻辑就是走了个递归先将新扩容的哈希表将原来的旧的哈希表重新映射,然后再通过swap来交换底层的指针达到扩容
这里的M用size(),不用capacity()的原因是:如果用capacity(来当M那映射出来的位置就超过了size,那你哈希底层的数据结构是vector,用[]取值的话是再size以内,所以不行,所以我们才在构造函数时手动给值,尽量使size和capacity差不多-

3.2find的实现

HashData<K,V>*find(constK&key){Hash ha;size_t hash0=ha(key)%_table.size();size_t hashi=hash0;inti=1;while(_table[hashi]._status!=EMPTY){if(_table[hashi]._kv.first==key&&_table[hashi]._status!=DELETE){return&_table[hashi];}hashi=(hash0+i)%_table.size();++i;}returnnullptr;}

这里就体现了用状态来标记是否存在就很好能体会到了

3.3erase的实现

boolerase(constK&key){autokeyi=find(key);if(keyi){keyi->_status=DELETE;returntrue;}returnfalse;}

四、代码的全部展示

#pragmaonce#include<iostream>#include<vector>#include<string>usingnamespacestd;enumstatus{EXIST,DELETE,EMPTY};template<classK,classV>structHashData{pair<K,V>_kv;status _status=EMPTY;};template<classK>structHashFunc{size_toperator()(constK&key){return(size_t)key;}};template<>structHashFunc<string>{size_toperator()(conststring&key){size_t keyi=0;for(autoe:key){keyi+=e;keyi*=131;}returnkeyi;}};//开放性地址法:// 1. 除数留余法: key%M//2. 负载因子:存储了N个值,哈希表的⼤⼩为M,那么负载因⼦= N/M//3.线性探测: 如果当前位置存在哈希冲突,那就线性走下一个位置直到没有哈希冲突template<classK,classV,classHash=HashFunc<K>>classHashTable{public:HashTable(){_table.resize(53);}inlineunsignedlong__stl_next_prime(unsignedlongn){// Note: assumes long is at least 32 bits.staticconstint__stl_num_primes=28;staticconstunsignedlong__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};constunsignedlong*first=__stl_prime_list;constunsignedlong*last=__stl_prime_list+__stl_num_primes;constunsignedlong*pos=lower_bound(first,last,n);returnpos==last?*(last-1):*pos;}boolinsert(constpair<K,V>&key){if(find(key.first)){returnfalse;}//扩容问题if((double)n/(double)_table.size()>0.7){HashTable<K,V,Hash>_newtable;_newtable._table.resize(__stl_next_prime(_table.size()+1));for(autoe:_table){if(e._status==EXIST){_newtable.insert(e._kv);}}_table.swap(_newtable._table);}Hash ha;size_t hash0=ha(key.first)%_table.size();size_t hashi=hash0;inti=1;while(_table[hashi]._status==EXIST){hashi=(hash0+i)%_table.size();++i;}_table[hashi]._status=EXIST;_table[hashi]._kv=key;++n;returntrue;}HashData<K,V>*find(constK&key){Hash ha;size_t hash0=ha(key)%_table.size();size_t hashi=hash0;inti=1;while(_table[hashi]._status!=EMPTY){if(_table[hashi]._kv.first==key&&_table[hashi]._status!=DELETE){return&_table[hashi];}hashi=(hash0+i)%_table.size();++i;}returnnullptr;}boolerase(constK&key){autokeyi=find(key);if(keyi){keyi->_status=DELETE;returntrue;}returnfalse;}private:vector<HashData<K,V>>_table;size_t n=0;};

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/12 18:39:58

架构解析:ImageSharp内存映射技术实现超大图像零拷贝处理

架构解析&#xff1a;ImageSharp内存映射技术实现超大图像零拷贝处理 【免费下载链接】ImageSharp :camera: A modern, cross-platform, 2D Graphics library for .NET 项目地址: https://gitcode.com/gh_mirrors/im/ImageSharp 技术挑战与行业痛点分析 在现代数字图像…

作者头像 李华
网站建设 2026/7/12 18:39:53

MetaTube SDK Go测试策略:单元测试、集成测试与端到端测试

MetaTube SDK Go测试策略&#xff1a;单元测试、集成测试与端到端测试 【免费下载链接】metatube-sdk-go MetaTube SDK & API Server in Golang 项目地址: https://gitcode.com/gh_mirrors/me/metatube-sdk-go MetaTube SDK Go作为一款专业的元数据获取和图像处理Go…

作者头像 李华
网站建设 2026/7/12 18:39:46

全球最大数据中心黄了!美国电网卡住AI,GobiX在中国戈壁破局

马斯克向天要电、孙正义向地烧气&#xff0c;当全球都在为AI的电发愁时&#xff0c;远景把答案写在了戈壁滩上。37.7C&#xff0c;干趴了一台国家级AI超算。6月底&#xff0c;英国遭遇有记录以来最热的六月。剑桥大学的Dawn——全英最快的AI超算之一——冷却系统当场热瘫&#…

作者头像 李华
网站建设 2026/7/12 18:39:43

AI 辅助技术博客选题:从社区热点到个性化内容规划

AI 辅助技术博客选题&#xff1a;从社区热点到个性化内容规划 技术博客最难的不是写&#xff0c;而是定题。灵感昨天还在&#xff0c;今天打开编辑器就一片空白。热点想追但怕同质化&#xff0c;冷门想写但怕没人看。本文介绍一套用 AI 辅助选题的工作流&#xff1a;从社区热点…

作者头像 李华
网站建设 2026/7/12 18:39:08

5分钟快速上手Spoilerwall:用电影剧透隐藏你的开放端口

5分钟快速上手Spoilerwall&#xff1a;用电影剧透隐藏你的开放端口 想要保护服务器端口不被扫描器轻易发现吗&#xff1f;Spoilerwall 这款创新的网络安全工具为您带来了全新的防护思路&#xff01;这款开源项目通过电影剧透来巧妙隐藏开放端口&#xff0c;让网络扫描器看到的是…

作者头像 李华
网站建设 2026/7/12 18:37:10

从零开始认识CAN网通信

从零开始认识CAN网通信引言&#xff1a;为什么需要CAN总线&#xff1f;物理层&#xff1a;差分信号与“线与”逻辑电平定义“线与”逻辑为什么用差分信号&#xff1f;数据链路层&#xff1a;报文、仲裁与帧结构CAN通信模型非破坏性仲裁机制标准帧 vs 扩展帧CAN报文接收与发送流…

作者头像 李华