boxmoe_header_banner_img

Hello! 欢迎来到我的博客!

加载中

文章导读

数据结构-散列表&哈希表


avatar
xiaoifei 2026年8月22日 30

引入

对于学生信息有如下结构

struct student {
	id int,
	name string,
}

如果要在一堆学生数组中查找某一个学生的信息,那么可以用遍历的方法,例如我想通过学号查找学生信息那么可以这么写

{id:2,name:"李四"}
{id:1,name:"张三"}
{id:3,name:"王五"}
foreach s in students {
	if s.id == 3 {
		print(s.name)
	}
}

但是这样查找效率太低了,因此我们想到为什么不直接将学号当作下标访问,例如我想找学号为3的,直接使用arr[3]
在存储阶段我们将id作为数组下标,例如我想存放1-3的学号学生信息,我就创建空间为4的数组,然后将学号1对应的信息放入arr[1]中,学号2放入arr[2]中,以此类推。这样查找的时候就能直接根据学号定位下标。恭喜你发明了Hash表

我们常用的字典就是以Hash实现的,通过set(1,"张三")键值对来保存数据
Hash表可以看作一个数组,数组内每一个空间存放键值对信息,并且数组的下标与KEY对应,这样通过KEY就可以直接定位到数据,平均时间复杂度O(1),最坏为O(n)

Hash函数/散列函数

{id:1001,name:"张三"}
{id:1002,name:"李四"}
{id:1003,name:"王五"}

已知上面的信息,id是从1001开始的,我们不可能创建一个空间为1004的数组,因为我们只需要存放三个数据,因此需要用到散列函数。

散列函数维护了键值对之间的映射关系
最常用的映射关系如下

  • 直接定址(线性映射):
  • 除留余数:H(Key) = key % p

直接定址

满足H(Key) = Key或H(Key) = k * Key + b
取偏移量b = 1000,则H(Key) = Key – 1000,但是这种方法只适用于Key连续的情况,如果Key稀疏且跨度大依旧会带来空间上的浪费

优点:映射简单,不会产生碰撞
缺点:要求Key连续分布否则会带来大量空间浪费

除留余数

H(Key) = key % p,(p一般取小于等于表长的最大质数)

优点:能够将Key放到连续空间
缺点:会产生Key碰撞,降低散列表性能

碰撞:两个Key通过散列函数计算后映射到数组同一个下标

散列表性能决定因素:

  1. 散列函数(选用哪种映射策略)
  2. 装填因子(表中元素数量 / 表长)
  3. 碰撞处理方法

装填因子:装填因子 = 表中元素数 / 表长。开放定址法一般要求装填因子 < 1(常见阈值 0.7~0.75),超过时需要扩容(再散列)

碰撞处理方法

开放定址法

线性探测法

表尾的下一个位置是表首,碰撞后依次探测下一个位置,直到遇到空闲位置

对于线性探测法删除Key不能直接将下标所在元素删除,因为如果查找的元素在删除元素的后面,那么在线性探测时探测到空会误认为后面没有查找的元素了,导致查找失败。所以删除元素需要打上删除标记。

  • 在查找时,遇到空会放弃查找,而删除标记不会影响查找
  • 做插入时,空位置和删除标记位置可以插入

弊端:堆积问题

平方探测法

冲突时按照+1²,-1²,+2²,-2²,+3²,-3²,…的顺序进行探测(表尾之后是表首)
表长=某个4k+3的质数(k为正整数) 时一定能探测到所有位置

  • 在查找时,遇到空会放弃查找,而删除标记不会影响查找
  • 做插入时,空位置和删除标记位置可以插入

拉链法

冲突时通过挂载串联单链表实现查找

  • 插入时,头插法和尾插法都可以
  • 删除时,直接让上一个节点的尾指针指向下一个节点即可

注意

编程语言 API 里说的 hash(),只是将Key(如字符串)进行转化为一个 int,不含映射。
如Java的String.hashCode()只把字符串变成整数。真正映射到桶,是HashMap容器内部做的事。



评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字

插入代码