理解Hash

哈希表(hash table)是从一个集合A到另一个集合B的映射(mapping)

映射是一种对应关系,而且集合A的某个元素只能对应集合B中的一个元素。但反过来,集合B中的一个元素可能对应多个集合A中的元素。如果B中的元素只能对应A中的一个元素,这样的映射被称为一一映射。这样的对应关系在现实生活中很常见,比如:

A  -> B

人 -> 身份证号

日期 -> 星座

上面两个映射中,人 -> 身份证号是一一映射的关系。在哈希表中,上述对应过程称为hashing。A中元素a对应B中元素b,a被称为键值(key),b被称为a的hash值(hash value)

映射在数学上相当于一个函数f(x):A->B。比如 f(x) = 3x + 2。哈希表的核心是一个哈希函数(hash function),这个函数规定了集合A中的元素如何对应到集合B中的元素。比如:

A: 三位整数    hash(x) = x % 10    B: 一位整数

上述对应中,哈希函数表示为hash(x) = x % 10。也就是说,给一个三位数,我们取它的最后一位作为该三位数的hash值。

哈希表在计算机科学中应用广泛。比如在git中,文件内容为键值,并用SHA算法作为hash function,将文件内容对应为固定长度的字符串(hash值)。如果文件内容发生变化,那么所对应的字符串就会发生变化。git通过比较较短的hash值,就可以知道文件内容是否发生变动。

再比如计算机的登陆密码,一般是一串字符。然而,为了安全起见,计算机不会直接保存该字符串,而是保存该字符串的hash值(使用MD5、SHA或者其他算法作为hash函数)。当用户下次登陆的时候,输入密码字符串。如果该密码字符串的hash值与保存的hash值一致,那么就认为用户输入了正确的密码。这样,就算黑客闯入了数据库中的密码记录,他能看到的也只是密码的hash值。上面所使用的hash函数有很好的单向性:很难从hash值去推测键值。因此,黑客无法获知用户的密码。(之前有报道多家网站用户密码泄露的时间,就是因为这些网站存储明文密码,而不是hash值.)

注意,hash只要求从A到B的对应为一个映射,它并没有限定该对应关系为一一映射。因此会有这样的可能:两个不同的键值对应同一个hash值。这种情况叫做hash碰撞(hash collision)或者hash 冲突。比如网络协议中的checksum就可能出现这种状况,即所要校验的内容与原文并不同,但与原文生成的checksum(hash值)相同。再比如,MD5算法常用来计算密码的hash值。已经有实验表明,MD5算法有可能发生碰撞,也就是不同的明文密码生成相同的hash值,这将给系统带来很大的安全漏洞。(参考hash collision

Hash函数设计的好坏直接影响到对Hash表的操作效率。下面举例说明:

假如对上述的联系人信息进行存储时,采用的Hash函数为:姓名的每个字的拼音开头大写字母的ASCII码之和。

address(张三)=ASCII(Z)+ASCII(S)=90+83=173;

address(李四)=ASCII(L)+ASCII(S)=76+83=159;

address(王五)=ASCII(W)+ASCII(W)=87+87=174;

address(张帅)=ASCII(Z)+ASCII(S)=90+83=173;

假如只有这4个联系人信息需要进行存储,这个Hash函数设计的很糟糕。首先,它浪费了大量的存储空间,假如采用char型数组存储联系人信息的话,则至少需要开辟174*12字节的空间,空间利用率只有4/174,不到5%;另外,根据Hash函数计算结果之后,address(张三)和address(李四)具有相同的地址,这种现象称作冲突,对于174个存储空间中只需要存储4条记录就发生了冲突,这样的Hash函数设计是很不合理的。所以在构造Hash函数时应尽量考虑关键字的分布特点来设计函数使得Hash地址随机均匀地分布在整个地址空间当中。通常有以下几种构造Hash函数的方法:

取关键字或者关键字的某个线性函数为Hash地址,即address(key)=a*key+b;如知道学生的学号从2000开始,最大为4000,则可以将address(key)=key-2000作为Hash地址。

对关键字进行平方运算,然后取结果的中间几位作为Hash地址。假如有以下关键字序列{421,423,436},平方之后的结果为{177241,178929,190096},那么可以取{72,89,00}作为Hash地址。

将关键字拆分成几部分,然后将这几部分组合在一起,以特定的方式进行转化形成Hash地址。假如知道图书的ISBN号为8903-241-23,可以将address(key)=89+03+24+12+3作为Hash地址。

如果知道Hash表的最大长度为m,可以取不大于m的最大质数p,然后对关键字进行取余运算,address(key)=key%p。在这里p的选取非常关键,p选择的好的话,能够最大程度地减少冲突,p一般取不大于m的最大质数。

假设关键字是以r为基的数,并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。

例如有某些人的生日数据如下:

年. 月. 日

75.10.03

85.11.23

86.03.02

86.07.12

85.04.21

96.02.15

经分析,第一位,第二位,第三位重复的可能性大,取这三位造成冲突的机会增加,所以尽量不取前三位,取后三位比较好

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即

H(key)=random(key) ,其中random为随机函数。通常用于关键字长度不等时采用此法。

哈希表处理冲突主要有开放寻址法、再散列法、链地址法(拉链法)和建立一个公共溢出区四种方法

通过构造性能良好的哈希函数,可以减少冲突,但一般不可能完全避免冲突,因此解决冲突是哈希法的另一个关键问题。创建哈希表和查找哈希表都会遇到冲突,两种情况下解决冲突的方法应该一致。下面以创建哈希表为例,说明解决冲突的方法。常用的解决冲突方法有以下四种:

这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。这种方法有一个通用的再散列函数形式:Hi=(H(key)+di)%m   i=1,2,…,n,其中H(key)为哈希函数,m 为表长,di称为增量序列。增量序列的取值方式不同,相应的再散列方式也不同。主要有以下三种:

di=1,2,3,…,m-1

这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。

di=12,-12,22,-22,…,k2,-k2    ( k<=m/2)

这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。

di=伪随机数序列。

具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点。

例如,已知哈希表长度m=11,哈希函数为:H(key)= key  %  11,则H(47)=3,H(26)=4,H(60)=5,假设下一个关键字为69,则H(69)=3,与47冲突。

如果用线性探测再散列处理冲突,下一个哈希地址为H1=(3 + 1)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 + 2)% 11 = 5,还是冲突,继续找下一个哈希地址为H3=(3 + 3)% 11 = 6,此时不再冲突,将69填入5号单元。

如果用二次探测再散列处理冲突,下一个哈希地址为H1=(3 + 12)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 - 12)% 11 = 2,此时不再冲突,将69填入2号单元。

如果用伪随机探测再散列处理冲突,且伪随机数序列为:2,5,9,……..,则下一个哈希地址为H1=(3 + 2)% 11 = 5,仍然冲突,再找下一个哈希地址为H2=(3 + 5)% 11 = 8,此时不再冲突,将69填入8号单元。

从上述例子可以看出,线性探测再散列容易产生“二次聚集”,即在处理同义词的冲突时又导致非同义词的冲突。例如,当表中i, i+1 ,i+2三个单元已满时,下一个哈希地址为i, 或i+1 ,或i+2,或i+3的元素,都将填入i+3这同一个单元,而这四个元素并非同义词。线性探测再散列的优点是:只要哈希表不满,就一定能找到一个不冲突的哈希地址,而二次探测再散列和伪随机探测再散列则不一定。

这种方法是同时构造多个不同的哈希函数:

Hi=RH1(key),i=1,2,3,…,n.

当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。链地址法适用于经常进行插入和删除的情况。

与开放定址法相比,拉链法有如下几个优点:

  • (1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
  • (2)由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
  • (3)开放定址法为减少冲突,要求装填因子α(装填因子=表中的记录数/哈希表的长度)较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
  • (4)在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填入散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。 因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表.(注意:在这个方法里面是把元素分开两个表来存储)

当冲突太多的时候,我们一般采用的方法时拉链法,采用拉链法的原因是动态申请空间,至于优点在上面已经阐述了.冲突太多的时候会产生堆积状态,我们将H(key)相同的关键字都统一放到一个链里,当出现冲突的时候我们就把该元素接在链表后面,这样可以避免产生堆积现象,缩短平均查找长度.

当数据表太小数据太多可以通过建立一个溢出表,专门用来存放哈希表中放不下的记录.

Hash表的平均查找长度包括查找成功时的平均查找长度和查找失败时的平均查找长度。

查找成功时的平均查找长度=表中每个元素查找成功时的比较次数之和/表中元素个数;

查找不成功时的平均查找长度相当于在表中查找元素不成功时的平均比较次数,可以理解为向表中插入某个元素,该元素在每个位置都有可能,然后计算出在每个位置能够插入时需要比较的次数,再除以表长即为查找不成功时的平均查找长度。

下面举个例子:

有一组关键字{23,12,14,2,3,5},表长为14,Hash函数为key%11,则关键字在表中的存储如下:

地址     0     1     2     3      4     5    6   7   8    9  10   11   12    13

关键字        23    12   14     2     3    5

比较次数         1      2    1     3     3     2

因此查找成功时的平均查找长度为(1+2+1+3+3+2)/6=11/6;

查找失败时的平均查找长度为(1+7+6+5+4+3+2+1+1+1+1+1+1+1)/14=38/14;

这里有一个概念装填因子=表中的记录数/哈希表的长度,如果装填因子越小,表明表中还有很多的空单元,则发生冲突的可能性越小;而装填因子越大,则发生冲突的可能性就越大,在查找时所耗费的时间就越多。因此,Hash表的平均查找长度和装填因子有关。有相关文献证明当装填因子在0.5左右的时候,Hash的性能能够达到最优。因此,一般情况下,装填因子取经验值0.5。(也就是说所需的实际空间为元素数目的2倍)

Hash表大小的确定也非常关键,如果Hash表的空间远远大于最后实际存储的记录个数,则造成了很大的空间浪费,如果选取小了的话,则容易造成冲突。在实际情况中,一般需要根据最终记录存储个数和关键字的分布特点来确定Hash表的大小。还有一种情况时可能事先不知道最终需要存储的记录个数,则需要动态维护Hash表的容量,此时可能需要重新计算Hash地址。

时间: 2024-10-07 10:47:30

理解Hash的相关文章

深入理解 hash 函数、HashMap、LinkedHashMap、TreeMap 【中】

LinkedHashMap - 有序的 HashMap 我们之前讲过的 HashMap 的性能表现非常不错,因此使用的非常广泛.但是它有一个非常大的缺点,就是它内部的元素都是无序的.如果在遍历 map 的时候, 我们希望元素能够保持它被put进去时候的顺序,或者是元素被访问的先后顺序,就不得不使用 LinkedHashMap. LinkdHashMap 继承了 HashMap,因此,它具备了 HashMap 的优良特性-高性能.在HashMap 的基础上, LinkedHashMap 又在内部维

深入理解 hash 函数、HashMap、LinkedHashMap、TreeMap 【上】

前言 Map 是非常常用的一种数据接口.在 Java 中,提供了成熟的 Map 实现. 图 1 最主要的实现类有 Hashtable.HashMap.LinkedHashMap和 TreeMap.在 HashTable 的子类中,还有 Properties的实现.Properties 是专门读取配置文件的类,我们会在稍后介绍.这里首先值得关注的是 HashMap 和 HashTable 两套不同的实现,两者都实现了 Map 接口.从表面上看,并没有多大差别,但是在内部实现上却有些微小的细节. 首

简单理解Hash算法的作用

什么是Hash Hash算法,简称散列算法,也成哈希算法(英译),是将一个大文件映射成一个小串字符.与指纹一样,就是以较短的信息来保证文件的唯一性的标志,这种标志与文件的每一个字节都相关,而且难以找到逆向规律. 举个列子: 服务器存了10个文本文件,你现在想判断一个新的文本文件和那10个文件有没有一个是一样的.你不可能去比对每个文本里面的每个字节,很有可能,两个文本文件都是5000个字节,但是只有最后一位有所不同,但这样的,你前面4999位的比较就是毫无意义.那一个解决办法,就是在存储那10个文

[redis数据结构]之 hash类型

在讲解语法知识之前,教你如何掌握各种hash的基本潜规则,在不同的语言中,有点称之为hash.有的是map,但不管这么样,hash你可以看作是key-value一组的集合.我先将java中map的概念擅自的加入到redis的hash中,让我们更加容易的理解hash的潜规则: 一.hash潜规则 1.映射模型 值集合映射,键集合映射,Entry集合映射(key-value,有得称之为item(python)). 2.一般的方法列表 添加.删除.更新.判断键是否存在.对应映射模型的key集合.val

数据结构基础-Hash Table详解(转)

理解Hash 哈希表(hash table)是从一个集合A到另一个集合B的映射(mapping). 映射是一种对应关系,而且集合A的某个元素只能对应集合B中的一个元素.但反过来,集合B中的一个元素可能对应多个集合A中的元素.如果B中的元素只能对应A中的一个元素,这样的映射被称为一一映射.这样的对应关系在现实生活中很常见,比如: A  -> B 人 -> 身份证号 日期 -> 星座 上面两个映射中,人 -> 身份证号是一一映射的关系.在哈希表中,上述对应过程称为hashing.A中元

iOS判断对象相等 重写isEqual、isEqualToClass、hash

相等的概念是探究哲学和数学的核心,并且对道德.公正和公共政策的问题有着深远的影响. 从一个经验主义者的角度来看,两个物体不能依据一些观测标准中分辨出来,它们就是相等的.在人文方面,平等主义者认为相等意味着要保持每个人的社会.经济.政治和他们住地的司法系统都一致. 对程序员来说,协调好逻辑和感官能力来理解我们塑造的'相同'的语义是一项任务.'相同的问题'(的探讨)太微妙,同时有太容易被忽视.对语义没有充分的理解就直接去实现它,可能会导致没必要的工作和不正确的结果.因此对数学和逻辑系统的深刻理解与按

HASH算法详解

做了几年开发,一直不理解HASH算法的原理,今天偶从百度知道上看到一个牛人神一样的理解: 这个问题有点难度,不是很好说清楚. 我来做一个比喻吧. 我们有很多的小猪,每个的体重都不一样,假设体重分布比较平均(我们考虑到公斤级别),我们按照体重来分,划分成100个小猪圈. 然后把每个小猪,按照体重赶进各自的猪圈里,记录档案. 好了,如果我们要找某个小猪怎么办呢?我们需要每个猪圈,每个小猪的比对吗? 当然不需要了. 我们先看看要找的这个小猪的体重,然后就找到了对应的猪圈了. 在这个猪圈里的小猪的数量就

第六章——根据执行计划优化性能(1)——理解哈希、合并、嵌套循环连接策略

原文:第六章--根据执行计划优化性能(1)--理解哈希.合并.嵌套循环连接策略 前言: 本系列文章包括: 1. 理解Hash.Merge.Nested Loop关联策略. 2.在执行计划中发现并解决表/索引扫描. 3. 介绍并在执行计划中发现键查找并解决它们. 对于性能优化,需要集中处理以下的问题: 1. 为你的环境创建性能基线. 2. 监控现在的性能并发现瓶颈. 3. 解决瓶颈以便得到更好的性能. 一个预估执行计划是描述查询将会如何执行的一个蓝图,而一个实际执行计划就是一个查询执行时实际发生的

【连载】关系型数据库是如何工作的?(6) - Hash表

最后我们介绍的重要数据结构就是Hash表.当你需要快速查找的时候非常有用,而且理解Hash表会有助于我们以后理解常用数据库Join方式之一Hash join.这种数据结构常被数据库用作存储内部数据结构:表锁或缓存池(后续章节会介绍). Hash表能够通过元素Key快速找到元素的,为了构建一张Hash表,你需要定义: 一个元素的Key: 一个关于Key的Hash函数,Key的hash值就代表元素所在的位置(我们通常称为Hash桶): 一个关于Key的比较函数,一旦你找到了正确的桶,你就可以通过比较