简单了解一下哈希表(C++)
哈希表(Hash Table),又称散列表(从译名上看,有散乱排序的意思),是一种根据关键码值(Key)直接访问记录的数据结构。它通过哈希函数将关键码映射到表中的存储位置,实现快速查找、插入和删除操作,理想情况下时间复杂度接近 O(1)
哈希表的哈希函数(将数据元素的关键键值映射为元素存储位置的函数)有很多种,这里只讲一下 直接定址法 和 除法散列法(除留余数法)
直接定址法:
当关键字的范围比较集中时,就可以直接使用关键字的值作为存储位置的下标,比如集中在 [0,99] 范围内的,就可以开辟一个大小为100的数组,每个关键字的值就是数组下标;又或者使用关键字的某个线性函数值作为哈希地址,即通过公式 H(key)=key 或 H(key)=a×key+b(a、b 为常数)建立关键字与存储位置的一一对应关系,从而实现根据关键字直接计算存储位置。
需要注意的是,直接定址法具有局限性:它只适用于范围集中 的 整型
除法散列法(除留余数法):
除法散列法又叫除留余数法,就是用关键字 % 上哈希表大小 得到的值(余数),用来映射到开辟的空间
不管是用哪种哈希函数,都会有多个值对一个位置的情况,叫做 哈希冲突/哈希碰撞
要注意的是,哈希冲突是不可避免的,所以只能去寻找好的哈希构造方法来减少冲突的次数
当使用除留余数法时,要避免哈希表的大小为 2的幂或者10的幂 等,因为 % 可以去掉能整除的,留下不能被整除的
这里以 10的幂为例,12002和3456002 两个值如果都 % 上1000,就都只会留下002,计算出来的哈希值相同,会发生哈希冲突
所以最好不要用2的幂、10的幂,这只让部分位参与运算
让32个位都参与运算,得到的结果才是更均匀的
建议取不太接近2的整数次幂的一个质数
所以除留余数法它是对哈希表大小M是有要求的
处理冲突
既然哈希冲突避免不了,就要想办法处理哈希冲突
处理哈希冲突的主要方式有四种:开放地址法、链地址法、再哈希法和建立公共溢出区
这里讲一下 开放地址法和链地址法
开放定址法
开放地址法形成的哈希表叫做闭散列哈希表
原理:当发生哈希冲突时,会按照某种规则找到一个没有存储数据的位置进行存储
有三种不同的开放定址法,分别叫做 线性探测、二次探测、双重探测
这里只讲一下其中的线性探测
线性探测
从发生冲突的位置开始,依次线性往后探寻,直到找到某一个没有存储数据的位置为止,如果探测到了哈希表尾,就返回哈希头部继续寻找
需要注意,线性探测会出现连续冲突的问题,因为冲突数据会放到其它位置,下一个数据如果就指向这个位置,就也需要往后寻找位置
负载因子
在开放定址里,负载因子就是数组槽的占有率,在链定址法里,负载因子就是平均每个链表的长度(元素总数除以桶的个数)
负载因子越大,哈希冲突的概率越高,空间利用率越高;负载因子越小,哈希冲突的概率越低,空间利用率越低
开放定址法会控制 负载因子 在1以下,所以不会出现找不到空间存放数据的情况
底层实现
哈希表底层使用的数组,数组类型是个结构体,结构体里面有着要存的值和当前状态,状态可以帮助查找元素过程中不会受到 被删掉的元素的干扰,即跳过可能存在于后续位置的元素
hash表实现的时候还需要提供仿函数
一个将key值转成整型(因为只有整型可以%hash表大小),一些可以直接转的比如 double、float这些就可以使用hash底层默认的仿函数,但是像string这样不能强转的类型,就需要用户提供对应的仿函数(库里面使用的不需要传仿函数,是因为专门为string搞了特化)
另一个用来获取key值,这个主要是用来适配pair类型的存储,使用key键来进行hash函数
链定址法
使用链定址法的hash表又叫做开散列哈希桶
它提供了从根本上解决冲突的方法,它使用链表将冲突的数据连接起来,并挂在哈希表的一个位置下面,此时冲突数据不再会影响到别的数据存储
链地址法实现的负载因子不再有小于1的限制,它可以在大于1的时候再进行扩容
