python中哈希表用法,python 哈希值

  python中哈希表用法,python 哈希值

  哈希表或哈希表是一种常用的数据存储方案。本文将从开发者的角度探索哈希的世界,感兴趣的朋友可以跟边肖一起学习。

  00-1010 1.前言2。哈希表2.1哈希函数2.2哈希算法2.3常用哈希算法2.4哈希冲突3。摘要

  

目录

  哈希表或哈希表是一种常用的数据存储方案。

  哈希表是一种抽象的数据结构,需要开发者根据哈希表数据结构的存储需求定制API。对于大多数高级语言,都提供了可以直接使用的实现API,比如JAVA中的地图集合,C中的地图容器,Python中的字典.

  用户可以使用API中的方法完成添加、删除、修改和查找哈希表等一系列操作。

  如何学习哈希表?

  可以从两个角度入手:

  用户观点:你只需要知道哈希表是基于键和值对存储的解决方案,还需要熟悉基于不同计算机语言提供的哈希表数据结构的API实现,学会使用API中的方法。站在开发者的角度,你需要了解哈希表的底层实现原理,以及实现过程中需要解决的各种问题。本文将从开发人员的角度探索散列世界。

  

1. 前言

  什么是哈希表?

  哈希表是基于键和值对的数据结构,底层一般是列表(数组)。

  众所周知,基于list (array)的查询速度很快,时间复杂度为O(1),是常数。

  列表的底层存储结构是一个连续的内存区域。只要给出数据在列表(数组)中的位置,就可以直接查询数据。理论上是这样,但实际上查询数据的时间复杂度不一定是常数。

  如果存储了以下学生信息,则学生信息包括学生的姓名和学号。存储学生数据时,如果学生ID为0的学生存储在列表0位置,则学生ID为1的学生存储在列表1位置.

  这里,学生的ID号与列表的索引号相关联。当你查询某个学生时,你知道这个学生的身份证号和学生数据在列表中存储的位置。可以认为查询时间复杂度为O(1)。

  之所以能达到常量级别,是因为有信息关联(学号与数据的存储位置关联)。

  还有一点,学生的学号是公共信息和常用信息,很容易获取。

  然而,当没有存储数据时,可以找到与列表位置相关联的信息。举个例子,如果把所有的英语单词都存储起来,就不可能把每个英语单词都编号。即使它们有编号,这里的编号也只是一个序号。没有数据意义的数据对用户是不友好的,没有人能记住哪个英文单词对应哪个数字。

  因此,当你使用列表存储英语单词时,你需要询问时间,因为没有单词的存储位置。仍然需要使用线性、二进制等查询算法。此时的时间复杂度由所使用的查询算法的时间复杂度决定。

  如果存储在列表中的上述学生信息被插入、删除等。而数据原来的位置被改变,因为与学号和位置相关的信息被破坏,再次查询时只能使用其他查询算法,不可能达到常量级别。

  是否存在一种方案,能最大化地优化数据的存储和查询?

  通过上面的分析,我们可以得出一个结论,要提高查询速度,就要想办法把数据和位置关联起来。这是哈希表的核心思想。

  

2. 哈希表

  哈希表引入了关键字的概念,可以看作是数据的别名。如上表所示,可以给每个学生一个别名,这就是关键字。

  在这里,Tip:的关键词是名称的拼音缩写,关键词与数据的相关性强,便于记忆和查询。

  有了关键字后,将关键字映射到列表中的有效位置。映射方法是哈希表中最重要的概念哈希函数。

  关键字是一个桥梁,即与真实数据和在哈希表中的位置相关。

  关键字也可以是需要保存的数据本身。

  hash函数的作用:提供将关键字映射到列表的定位算法,是哈希表存储数据的核心。下图显示了数据、哈希函数和哈希表。

  间的关系,可以说哈希函数是数据进入哈希表的入口。

  

  数据最终会存储在列表中的哪一个位置,完全由哈希算法决定。

  当需要查询学生数据时,同样需要调用哈希函数对关键字进行换算,计算出数据在列表中的位置后就能很容易查询到数据。

  如果忽视哈希函数的时间复杂度,基于哈希表的数据存储和查询时间复杂度是O(1)。

  如此说来哈希函数算法设计的优劣是影响哈希表性能的关键所在。

  

  

2.2 哈希算法

  哈希算法决定了数据的最终存储位置,不同的哈希算法设计方案,也关乎哈希表的整体性能,所以,哈希算法就变得的尤为重要。

  下文将介绍并纵横比较几种常见的哈希算法的设计方案。

  Tip:无论使用何种哈希算法,都有一个根本,哈希后的结果一定是一个数字,表示列表(哈希表)中的一个有效位置。也称为哈希值。

  使用哈希表存储数据时,关键字可以是数字类型也可以是非数字类型,其实,关键字可以是任何一种类型。这里先讨论当关键字为非数字类型时设计哈希算法的基本思路。

  如前所述,已经为每一个学生提供了一个以姓名的拼音缩写的关键字。

  现在如何把关键字映射到列表的一个有效位置?

  这里可以简单地把拼音看成英文中的字母,先分别计算每一个字母在字母表中的位置,然后相加,得到的一个数字。

  使用上面的哈希思想对每一个学生的关键字进行哈希:

  

  • 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=4848除以4取余数,结果是0
  • llj的哈希值为12+12+10=3434除以4取余数,结果是2
  • cl的哈希值为3+12=1515除以4取余数,结果是3
  • zzz的哈希值为26+26+26=7878除以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,导致在存储时,后面存储的数据会覆盖前面存储的数据,这就是哈希中的典型问题,哈希冲突问题。

  所谓哈希冲突,指不同的关键字在进行哈希算法后得到相同的哈希值,这意味着,不同关键字所对应的数据会存储在同一个位置,这肯定会发生数据丢失,所以需要提供算法,解决冲突问题。

  Tip:研究哈希表,归根结底,是研究如何计算哈希值以及如何解决哈希值冲突的问题。

  针对上面的问题,有一种想当然的冲突解决方案,扩展列表的存储长度,如把列表扩展到长度为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 折叠法

  折叠法:将关键字分割成位数相同的几个部分(最后一部分的位数可以不同)然后取这几部分的叠加和(舍去进位)作为哈希值。

  折叠法又分移位叠加和间界叠加。

  

  • 移位叠加:将分割后的每一部分的最低位对齐,然后相加。
  • 间界叠加:从一端沿分割线来回折叠,然后对齐相加。

  因有相加求和计算,折叠法适合数字类型或能转换成数字类型的关键字。假设现在有很多商品订单信息,为了简化问题,订单只包括订单编号和订单金额。

  现在使用用哈希表存储订单数据,且以订单编号为关键字,订单金额为值。

  订单编号订单金额20201011400.0019981112300.0020221212200

  移位叠法换算关键字的思路:

  第一步:把订单编号20201011按每3位一组分割,分割后的结果:202、010、11。

  按2位一组还是3位一组进行分割,可以根据实际情况决定。

  第二步:把分割后的数字相加202+010+11,得到结果:223。再使用取余数法,如果哈希表的长度为10,则除以10后的余数为3。

  这里除以10仅是为了简化问题细节,具体操作时,很少选择列表的长度。

  第三步:对其它的关键字采用相同的处理方案。

  关键字哈希值202010113199811122202212126

  编码实现保存商品订单信息:

  

  移位叠加哈希算法

  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 平方取中法

  平方取中法:先是对关键字求平方,再在结果中取中间位置的数字。

  求平方再取中算法,是一种较常见的哈希算法,从数学公式可知,求平方后得到的中间几位数字与关键字的每一位都有关,取中法能让最后计算出来的哈希值更均匀。

  因要对关键字求平方,关键字只能是数字或能转换成数字的类型,至于关键字本身的大小范围限制,要根据使用的计算机语言灵活设置。

  如下面的图书数据,图书包括图书编号和图书名称。现在需要使用哈希表保存图书信息,以图书编号为关键字,图书名称为值。

  图书编号图书名称58python 从入门到精通67C++ STL78Java 内存模型

  使用平方取中法计算关键字的哈希值:

  第一步:对图书编号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_size

  hash_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。

  系数2和常数10的选择会影响最终生成的哈希值的大小。可以根据哈希表的大小和操作的数据含义自行选择。

  key为图书编号。当关键字不相同时,使用线性函数得到的值也是唯一的,所以,不会产生哈希冲突,但是会要求哈希表的存储长度比实际数据要大。

  这种算法在实际应用中并不多见。

  实际应用时,具体选择何种哈希算法,完全由开发者定夺,哈希算法的选择没有固定模式可循,虽然上面介绍了几种算法,只是提供一种算法思路。

  

  

2.4 哈希冲突

  哈希冲突是怎么引起的,前文已经说过。现在聊聊常见的几种哈希冲突解决方案。

  2.4.1 线性探测

  当发生哈希冲突后,会在冲突位置之后寻找一个可用的空位置。如下图所示,使用取余数哈希算法,保存数据到哈希表中。

  哈希表的长度设置为15,除数设置为13。

  

  解决冲突的流程:

  

  • 7826的哈希值都是0。而因为7826的前面,78先占据哈希表的0位置。
  • 当存储26时,只能以0位置为起始位置,向后寻找空位置,因1位置没有被其它数据占据,最终保存在哈希表的1位置。
  • 当存储数字14时,通过哈希算法计算,其哈希值是1,本应该要保存在哈希表中1的位置,因1位置已经被26所占据,只能向后寻找空位置,最终落脚在2位置。

  线性探测法让发生哈希冲突的数据保存在其它数据的哈希位置,如果冲突的数据较多,则占据的本应该属于其它数据的哈希位置也较多,这种现象称为哈希聚集。

  查询流程:

  以查询数据14为例。

  

  • 计算14的哈希值,得到值为1,根据哈希值在哈希表中找到对应位置。
  • 查看对应位置是否存在数据,如果不存在,宣告查询失败,如果存在,则需要提供数据比较方法。
  • 1位置的数据26并不等于14。于是,继续向后搜索,并逐一比较。
  • 最终可以得到结论14在哈希表的编号为2的位置。

  所以,在查询过程中,除了要提供哈希函数,还需要提供数据比较函数。

  删除流程:

  以删除数字26为例。

  按上述的查询流程找到数字26在哈希表中的位置1

  设置位置1为删除状态,一定要标注此位置曾经保存过数据,而不能设置为空状态。为什么?

  如果设置为空状态,则在查询数字14时,会产生错误的返回结果,会认为14不存在。为什么?自己想想。

  编码实现线性探测法:

  添加数据:

  

  线性探测法解决哈希冲突

  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] = n

  print("哈希表中的数据:", hash_table)

  输出结果:

  哈希表中的数据: [78, 26, 14, 81, 56, None, 32, None, 73, None, 88, None, 25, None, None]

  

  Tip:为了保证当哈希值发生冲突后,如果从冲突位置查到哈希表的结束位置还是没有找到空位置,则再从哈希表的起始位置,也就是0位置再搜索到冲突位置。冲突位置是起点也是终点,构建一个查找逻辑环,以保证一定能找到空位置。

  

for i in range(hash_val + 1, size + hash_val):

   pass

  

  基于线性探测的数据查询过程和存储过程大致相同:

  

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.2 链表法

  上面所述的冲突解决方案的核心思想是,当冲突发生后,在哈希表中再查找一个有效空位置。

  这种方案的优势是不会产生额外的存储空间,但易产生数据聚集,会让数据的存储不均衡,并且会违背初衷,通过关键字计算出来的哈希值并不能准确描述数据正确位置。

  链表法应该是所有解决哈希冲突中较完美的方案。所谓链表法,指当发生哈希冲突后,以冲突位置为首结点构建一条链表,以链表方式保存所有发生冲突的数据。如下图所示:

  

  链表方案解决冲突,无论在存储、查询、删除时都不会影响其它数据位置的独立性和唯一性,且因链表的操作速度较快,对于哈希表的整体性能都有较好改善。

  使用链表法时,哈希表中保存的是链表的首结点。首结点可以保存数据也可以不保存数据。

  编码实现链表法:链表实现需要定义 2 个类,1 个是结点类,1 个是哈希类。

  

  结点类

  class HashNode():

   def __init__(self, value):

   self.value = 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 -1

   if 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.value

   def 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.总结

  哈希表是一种高级数据结构,其存储、查询性能非常好,在不考虑哈希哈希算法和哈希冲突的时间复杂度情况下,哈希查找时间复杂度可以达到常量级,成为很多实际应用场景下的首选。

  研究哈希表,着重点就是搞清楚哈希算法以及如何解决哈希冲突。在算法的世界时,没有固定的模式,开发者可以根据自己的需要自行设计哈希算法。

  以上就是一文详解Python中哈希表的使用的详细内容,更多关于Python哈希表的资料请关注盛行IT软件开发工作室其它相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: