书到用时方恨少,事非经过不知难。这篇文章主要讲述Python 散列表查询_进入<
哈希函数;
为结界的世界相关的知识,希望能为你提供帮助。
1. 前言哈希表
或称为散列表
,是一种常见的、使用频率非常高的数据存储方案。
哈希表
属于抽象数据结构,需要开发者按哈希表
数据结构的存储要求进行 API
定制,对于大部分高级语言而言,都会提供已经实现好的、可直接使用的 API
,如 java
中有 MAP
集合、C++
中的 MAP
容器,python
中的字典……
使用者可以使用 API
中的方法完成对哈希表
的增、删、改、查……一系列操作。
如何学习哈希表?
可以从 2
个角度开始:
- 使用者角度:只需要知道
哈希表
是基于键
、值
对存储的解决方案,另需要熟悉不同计算机语言提供的基于哈希表
数据结构的API
实现,学会使用API
中的方法。 - 开发者的角度:则需要知道
哈希表
底层实现原理,以及实现过程中需要解决的各种问题。本文将站在开发者的角度,带着大家一起探究哈希
的世界。
哈希表
是基于键
、值
对存储的数据结构,底层一般采用的是列表(数组)
。【Python 散列表查询_进入< 哈希函数; 为结界的世界】大家都知道,基于
列表(数组)
的查询速度非常快,时间复杂度是 O(1)
,常量级别的。列表的底层存储结构是连续的内存区域,只要给定数据在列表(数组)中的位置,就能直接查询到数据。理论上是这么回事,但在实际操作过程,查询数据的时间复杂度却不一定是常量级别的。
如存储下面的学生信息,学生信息包括学生的姓名和学号。在存储学生数据时,如果把学号为
0
的学生存储在列表 0
位置,学号为 1
的学生存储在列表 1
位置……文章图片
这里把学生的学号和列表的索引号进行关联,查询某一个学生时,知道了学生的学号也就知道了学生数据存储在列表中的位置,可以认为查询的时间复杂度为
O(1)
。但是,不是存储任何数据时,都可以找到与列表位置相关联的信息。比如存储所有的英文单词,不可能为每一个英文单词编号,即使编号了,编号在这里也仅仅是流水号,没有数据含义的数据对于使用者来讲是不友好,谁也无法记住哪个英文单词对应哪个编号。
所以使用列表存储英文单词后需要询时,因没有单词的存储位置。还是需要使用如
线性
、二分
……之类的查询算法,这时的时间复杂度由使用的查询算法的时间复杂度决定。如果对上述存储在列表的学生信息进行了
插入
、删除
……等操作,改变了数据原来的位置后,因破坏了学号与位置关联信息,再查询时也只能使用其它查询算法,不可能达到常量级。是否存在一种方案,能最大化地优化数据的存储和查询?
通过上述的分析,可以得出一个结论,要提高查询的速度,得想办法把
数据
与位置
进行关联。而哈希表
的核心思想便是如此。2.1 哈希函数
哈希表
引入了关键字
概念,关键字
可以认为是数据的别名。如上表,可以给每一个学生起一个别名,这个就是关键字
。文章图片
有了
关键字
后,再把关键字
映射成列表中的一个有效位置,映射方法就是哈希表
中最重要的概念哈希函数
。哈希函数
的功能:提供把关键字
映射到列表
中的位置算法,是哈希表
存储数据的核心所在。如下图,演示数据
、哈希函数
、哈希表
之间的关系,可以说哈希函数
是数据进入哈希表
的入口。文章图片
数据最终会存储在列表中的哪一个位置,完全由
哈希算法
决定。当需要查询学生数据时,同样需要调用
哈希函数
对关键字
进行换算,计算出数据在列表中的位置后就能很容易查询到数据。如果忽视
哈希函数
的时间复杂度,基于哈希表
的数据存储和查询时间复杂度是 O(1)
。如此说来
哈希函数
算法设计的优劣是影响哈希表
性能的关键所在。2.2哈希算法
哈希算法
决定了数据的最终存储位置,不同的哈希算法
设计方案,也关乎哈希表
的整体性能,所以,哈希算法
就变得的尤为重要。下文将介绍并纵横比较几种常见的
哈希算法
的设计方案。使用
哈希表
存储数据时,关键字
可以是数字类型也可以是非数字类型,其实,关键字
可以是任何一种类型。这里先讨论当关键字
为非数字类型时设计哈希算法
的基本思路。如前所述,已经为每一个学生提供了一个以姓名的拼音缩写的
关键字
。现在如何把
关键字
映射到列表
的一个有效位置?这里可以简单地把拼音看成英文中的字母,先分别计算每一个字母在字母表中的位置,然后相加,得到的一个数字。
使用上面的
哈希思想
对每一个学生的关键字
进行哈希:zjl
的哈希值为26+10+12=48
。llj
的哈希值为12+12+10=34
。cl
的哈希值为3+12=15
。zxy
的哈希值为26+25+24=75
。
哈希值
是表示数据在列表中的存储位置,现在假设一种理想化状态,学生的姓名都是 3
个汉字,意味着关键字
也是 3
个字母,采用上面的的哈希算法
,最大的哈希值
应该是zzz=26+26+26=78
,意味着至少应该提供一个长度为 78
的列表 。如果,现在仅仅只保存
4
名学生,虽然只有 4
名学生,因无法保证学生的关键字
不出现zzz
,所以列表长度还是需要 78
。如下图所示。文章图片
采用这种
哈希算法
会导致列表的空间浪费严重,最直观想法是对哈希值
再做约束,如除以 4
再取余数,把哈希值
限制在 4
之内,4
个数据对应 4
个哈希值。我们称这种取余数方案为取余数算法
。重新对
4
名学生的关键字进行哈希。zjl
的哈希值为26+10+12=48
,48
除以4
取余数,结果是0
。llj
的哈希值为12+12+10=34
,34
除以4
取余数,结果是2
。cl
的哈希值为3+12=15
,15
除以4
取余数,结果是3
。zzz
的哈希值为26+26+26=78
,78
除以4
取余数,结果是2
。
文章图片
演示图上出现了一个很奇怪的现象,没有看到
李连杰
的存储信息。4
个存储位置存储 4
学生,应该是刚刚好,但是,只存储了 3
名学生。且还有 1
个位置是空闲的。现在编码验证一下,看是不是人为因素引起的。
哈希函数def hash_code(key):
# 设置字母 A 的在字母表中的位置是 1
pos = 0
for i in key:
i = i.lower()
res = ord(i) - ord(a) + 1
pos += res
return pos % 4
测试代码:
# 哈希表
hash_table = [None] * 4
# 计算关键字的哈希值
idx = hash_code(zjl)
# 根据关键字换算出来的位置存储数据
hash_table[idx] = 周杰伦
idx = hash_code(llj)
hash_table[idx] = 李连杰
idx = hash_code(cl)
hash_table[idx] = 成龙
idx = hash_code(zzz)
hash_table[idx] = 张志忠
print(哈希表中的数据:, hash_table)输出结果:
哈希表中的数据: [周杰伦, None, 张志忠, 成龙]
执行代码,输出结果,依然还是没有看到
李连杰
的信息。原因何在?
这是因为
李连杰
和张志忠
的哈希值都是 2
,导致在存储时,后面存储的数据会覆盖前面存储的数据,这就是哈希中的典型问题,哈希冲突问题
。所谓
哈希冲突
,指不同的关键字
在进行哈希算法
后得到相同的哈希值
,这意味着,不同关键字
所对应的数据会存储在同一个位置,这肯定会发生数据丢失,所以需要提供算法,解决冲突问题。针对上面的问题,有一种想当然的冲突解决方案,扩展列表的存储长度,如把列表扩展到长度为
8
。
哈希函数def hash_code(key):
# 设置字母 A 的在字母表中的位置是 1
pos = 0
for i in key:
i = i.lower()
res = ord(i) - ord(a) + 1
pos += res
return pos % 8# 哈希表
hash_table = [None] * 8# 保存所有学生
idx = hash_code(zjl)
hash_table[idx] = 周杰伦
idx = hash_code(llj)
hash_table[idx] = 李连杰
idx = hash_code(cl)
hash_table[idx] = 成龙
idx = hash_code(zzz)
hash_table[idx] = 张志忠
print(哈希表中的数据:, hash_table)输出结果:
哈希表中的数据: [周杰伦, None, 李连杰, None, None, None, 张志忠, 成龙]
文章图片
貌似解决了冲突问题,其实不然,当试着设置列表的长度为
6
、7
、8
、9
、10
时,只有当长度为 8
时没有发生冲突,这还是在要存储的数据是已知情况下的尝试。如果数据是动态变化的,显然这种扩展长度的方案绝对不是本质解决冲突的方案。即不能解决冲突,且产生大量空间浪费。
如何解决
哈希冲突
,会在后文详细介绍,这里还是回到哈希算法
上。综上所述,我们对
哈希算法
的理想要求是:- 为每一个
关键字
生成一个唯一的哈希值
,保证每一个数据都有只属于自己的存储位置。 - 哈希算法的性能时间复杂度要低。
2
个条件的哈希算法
几乎是不可能有的,面对数据量较多时,哈希冲突
是常态。所以,只能是尽可能满足。因冲突的存在,即使为
100
个数据提供 100
个有效存储空间,还是会有空间闲置。这里把实际使用空间和列表提供的有效空间相除,得到的结果,称之为哈希表的占有率(载荷因子)
。如上述,当列表长度为
4
时, 占有率为 3/4=0.75
,当列表长度为 8
时,占有率为 4/8=0.5
,一般要求占率控制 在0.6~0.9
之间。2.3 常见哈希算法
前面在介绍什么是
哈希算法
时,提到了取余数法
,除此之外,还有几种常见的哈希算法
。2.3.1折叠法折叠法:将
关键字
分割成位数相同的几个部分(最后一部分的位数可以不同)然后取这几部分的叠加和(舍去进位)作为哈希值。折叠法又分移位叠加和间界叠加。
- 移位叠加:将分割后的每一部分的最低位对齐,然后相加。
- 间界叠加:从一端沿分割线来回折叠,然后对齐相加。
现在使用用
哈希表
存储订单数据,且以订单编号为关键字
,订单金额为值
。订单编号 | 订单金额 |
---|---|
20201011 | 400.00 |
19981112 | 300.00 |
20221212 | 200 |
第一步:把订单编号
20201011
按每3
位一组分割,分割后的结果:202、010、11
。第二步: 把分割后的数字相加
202+010+11
,得到结果:223
。再使用取余数法,如果哈希表的长度为 10
,则除以 10
后的余数为3
。第三步:对其它的
关键字
采用相同的处理方案。关键字 | 哈希值 |
---|---|
20201011 | 3 |
19981112 | 2 |
20221212 | 6 |
移位叠加哈希算法def hash_code(key, hash_table_size):
# 转换成字符串
key_s = str(key)
# 保存求和结果
s = 0
# 使用切片
for i in range(0, len(key_s), 3):
s += int(key_s[i:i + 3])
return s % hash_table_size# 商品信息
products = [[20201011, 400.00], [19981112, 300], [20221212, 200]]
# 哈希表长度
hash_size = 10
# 哈希表
hash_table = [None] * hash_size
# 以哈希表方式进行存储
for p in products:
key = hash_code(p[0], hash_size)
hash_table[key] = p[1]
# 显示哈希表中的数据
print("哈希表中的数据:",hash_table)
# 根据订单号进行查询
hash_val = hash_code(19981112, hash_size)
val = hash_table[hash_val]
print("订单号为0的金额为1".format(19981112, val))输出结果
哈希表中的数据: [None, None, 300, 400.0, None, None, 200, None, None, None]
订单号为19981112的金额为300
间界叠加法:
间界叠加法,会间隔地把要相加的数字进行反转。
如订单编号
19981112
按3
位一组分割,分割后的结果:199、811、12
,间界叠加操作求和表达式为 199+118+12=339
,再把结果 339 % 10=9
。编码实现间界叠加算法:
间界叠加哈希算法def hash_code(key, hash_table_size):
# 转换成字符串
key_s = str(key)
# 保存求和结果
s = 0
# 使用切片
for i in range(0, len(key_s), 3):
# 切片
tmp_s = key_s[i:i + 3]
# 反转
if i % 2 != 0:
tmp_s = tmp_s[::-1]
s += int(tmp_s)
return s % hash_table_size# 商品信息(数据样例)
products = [[20201011, 400.00], [19981112, 300], [20221212, 200]]
# 哈希表长度
hash_size = 10
# 哈希表
hash_table = [None] * hash_size
# 以哈希表方式进行存储
for p in products:
key = hash_code(p[0], hash_size)
hash_table[key] = p[1]
# 显示哈希表中的数据
print("哈希表中的数据:", hash_table)
# 根据订单号进行查询
hash_val = hash_code(19981112, hash_size)
val = hash_table[hash_val]
print("订单号为0的金额为1".format(19981112, val))输出结果:
哈希表中的数据: [None, None, None, 400.0, None, None, 200, None, None, 300]
订单号为19981112的金额为300
2.3.2 平方取中法平方取中法:先是对
关键字
求平方,再在结果中取中间位置的数字。求平方再取中算法,是一种较常见的哈希算法,从数学公式可知,求平方后得到的中间几位数字与关键字的每一位都有关,取中法能让最后计算出来的哈希值更均匀。
因要对
关键字
求平方,关键字
只能是数字或能转换成数字的类型,至于关键字
本身的大小范围限制,要根据使用的计算机语言灵活设置。如下面的图书数据,图书包括图书编号和图书名称。现在需要使用哈希表保存图书信息,以图书编号为关键字,图书名称为值。
图书编号 | 图书名称 |
---|---|
58 | python 从入门到精通 |
67 | C++ STL |
78 | Java 内存模型 |
第一步:对图书编号
58
求平方,结果为 3364
。第二步:取
3364
的中间值36
,然后再使用取余数方案。如果哈希表的长度为 10
,则 36%10=6
。第三步:对其它的关键字采用相同的计算方案。
编码实现平方取中算法:
哈希算法
平方取中def hash_code(key, hash_table_size):
# 求平方
res = key ** 2
#取中间值,这里取中间 2 位(简化问题)
res = int(str(res)[1:3])
# 取余数
return res % hash_table_sizehash_table_size = 10
hash_table = [None]*hash_table_size
# 图书信息
books = [[58, "python 从入门到精通"], [67, "C++ STL"], [78, "Java 内存模型"]]
for b in books:
hash_val = hash_code(b[0],hash_table_size)
hash_table[hash_val]=b[1]# 显示哈希表中的数据
print("哈希表中的数据:", hash_table)
# 根据编号进行查询
hash_val = hash_code(67, hash_table_size)
val = hash_table[hash_val]
print("编号为0的书名为1".format(67, val))
上述求平方取中间值的算法仅针对于本文提供的图书数据,如果需要算法具有通用性,则需要根据实际情况修改。
2.3.3 直接地址法直接地址法:提供一个与
关键字
相关联的线性函数
。如针对上述图书数据,可以提供线性函数 f(k)=2*key+10
。key
为图书编号。当关键字
不相同时,使用线性函数
得到的值也是唯一的,所以,不会产生哈希冲突,但是会要求哈希表
的存储长度比实际数据要大。这种算法在实际应用中并不多见。
实际应用时,具体选择何种
哈希算法
,完全由开发者定夺,哈希算法
的选择没有固定模式可循,虽然上面介绍了几种算法,只是提供一种算法思路。2.4 哈希冲突
哈希冲突
是怎么引起的,前文已经说过。现在聊聊常见的几种哈希冲突
解决方案。2.4.1 线性探测当发生
哈希冲突
后,会在冲突位置之后寻找一个可用的空位置。如下图所示,使用取余数哈希算法
,保存数据到哈希表中。文章图片
解决冲突的流程:
78
和26
的哈希值都是0
。而因为78
在26
的前面,78
先占据哈希表的0
位置。
- 当存储
26
时,只能以0
位置为起始位置,向后寻找空位置,因1
位置没有被其它数据占据,最终保存在哈希表的1
位置。
- 当存储数字
14
时,通过哈希算法计算,其哈希值是1
,本应该要保存在哈希表中1
的位置,因1
位置已经被26
所占据,只能向后寻找空位置,最终落脚在2
位置。
哈希冲突
的数据保存在其它数据的哈希位置,如果冲突的数据较多,则占据的本应该属于其它数据的哈希位置也较多,这种现象称为哈希聚集
。查询流程:
以查询数据
14
为例。- 计算
14
的哈希值,得到值为1
,根据哈希值在哈希表中找到对应位置。 - 查看对应位置是否存在数据,如果不存在,宣告查询失败,如果存在,则需要提供数据比较方法。
- 因
1
位置的数据26
并不等于14
。于是,继续向后搜索,并逐一比较。 - 最终可以得到结论
14
在哈希表的编号为2
的位置。
删除流程:
以删除数字
26
为例。- 按上述的查询流程找到数字
26
在哈希表中的位置1
。
- 设置位置
1
为删除状态,一定要标注此位置曾经保存过数据,而不能设置为空状态。为什么?
添加数据:
线性探测法解决哈希冲突def hash_code(key, hash_table, num):
# 哈希表的长度
size = len(hash_table)
# 取余数法计算哈希值
hash_val = key % num
# 检查此位置是否已经保存其它数据
if hash_table[hash_val] is not None:
# 则从hash_val 之后寻找空位置
for i in range(hash_val + 1, size + hash_val):
if i >
= size:
i = i % size
if hash_table[i] is None:
hash_val = i
break
return hash_val# 哈希表
hash_table = [None] * 15
src_nums = [25, 78, 56, 32, 88, 26, 73, 81, 14]
for n in src_nums:
hash_val = hash_code(n, hash_table, 13)
hash_table[hash_val] = nprint("哈希表中的数据:", hash_table)输出结果:
哈希表中的数据: [78, 26, 14, 81, 56, None, 32, None, 73, None, 88, None, 25, None, None]
基于线性探测的数据查询过程和存储过程大致相同:
def get(key, hash_table, num):
# 哈希表的长度
size = len(hash_table)
# 取余数法计算哈希值
hash_val = key % num
is_exist = False
# 检查此位置是否已经保存其它数据
if hash_table[hash_val] is None:
# 不存在
return None
if hash_table[hash_val] != key:
# 则从hash_val 之后寻找空位置
for i in range(hash_val + 1, size + hash_val):
if i >
= size:
i = i % size
if hash_table[i] == key:
hash_val = i
is_exist = True
break
else:
is_exist=True
if is_exist:
return hash_val# 测试
res = get(25, hash_table, 13)
print(res)
为了减少数据聚集,可以采用增量线性探测法,所谓
增量
指当发生哈希冲突后,探测空位置时,使用步长值大于 1
的方式跳跃式向前查找。目的是让数据分布均匀,减小数据聚集。除了采用增量探测之外,还可以使用
再哈希
的方案。也就是提供2
个哈希函数,第 1
次哈希值发生冲突后,再调用第 2
个哈希函数再哈希,直到冲突不再产生。这种方案会增加计算时间。2.4.4 链表法
上面所述的冲突解决方案的核心思想是,当冲突发生后,在哈希表中再查找一个有效空位置。
这种方案的优势是不会产生额外的存储空间,但易产生数据聚集,会让数据的存储不均衡,并且会违背初衷,通过
关键字
计算出来的哈希值
并不能准确描述数据正确位置。链表法
应该是所有解决哈希冲突
中较完美的方案。所谓链表法
,指当发生哈希冲突
后,以冲突位置为首结点构建一条链表,以链表方式保存所有发生冲突的数据。如下图所示:文章图片
链表方案解决冲突,无论在存储、查询、删除时都不会影响其它数据位置的
独立性
和唯一性
,且因链表的操作速度较快,对于哈希表的整体性能都有较好改善。编码实现链表法:链表实现需要定义 2 个类,1 个是结点类,1 个是哈希类。
结点类class HashNode():
def __init__(self, value):
self.value = https://www.songbingjia.com/android/value
self.next_node = None哈希类class HashTable():
def __init__(self):
# 哈希表,初始大小为 15,可以根据需要动态修改
self.table = [None] * 15
# 实际数据大小
self.size = 0存储数据
key:关键字
value:值def put(self, key, value):
hash_val = self.hash_code(key)
# 新结点
new_node = HashNode(value)
if self.table[hash_val] is None:
# 本代码采用首结点保存数据方案
self.table[hash_val] = new_node
self.size+=1
else:
move = self.table[hash_val]
while move.next_node is not None:
move = move.next_node
move.next_node = new_node
self.size+=1查询数据def get(self, key):
hash_val = self.hash_code(key)
if self.table[hash_val] is None:
# 数据不存在
return -1if self.table[hash_val].value == key:
# 首结点就是要找的数据
return self.table[hash_val].value# 移动指针
move = self.table[hash_val].next_node
while move.value != key and move is not None:
move = move.next_node
if move is None:
return -1
else:
return move.valuedef hash_code(self, key):
# 这里仅为说明问题,13 的选择是固定的
hash_val = key % 13
return hash_val# 原始数据
src_nums = [25, 78, 56, 32, 88, 26, 39, 82, 14]
# 哈希对象
hash_table = HashTable()
# 把数据添加到哈希表中
for n in src_nums:
hash_table.put(n, n)
# 输出哈希表中的首结点数据
for i in hash_table.table:
if i is not None:
print(i.value,end=" ")
print("\\n-------------查询-----------")
print(hash_table.get(26))输出结果:
78 14 56 32 88 25
-------------查询-----------
26
3.总结
哈希表
是一种高级数据结构,其存储、查询性能非常好,在不考虑哈希哈希算法和哈希冲突的时间复杂度情况下,哈希查找时间复杂度可以达到常量级,成为很多实际应用场景下的首选。研究
哈希表
,着重点就是搞清楚哈希算法
以及如何解决哈希冲突
。在算法的世界时,没有固定的模式,开发者可以根据自己的需要自行设计哈希算法。推荐阅读
- 大厂的优惠券系统是如何设计的()
- 可观测性(运维风向标!)
- 手把手教你安装Ubuntu
- vue2版本中slot的基本使用详解
- CentOS 开机启动流程
- Go语言入门很简单(如何在 Go 语言中使用 MySQL)
- C语言_数组的查找替换排序拼接
- K8S-ConfigMap与Secret
- Linux系统基础入门知识文件管理