【数据结构与算法】之散列表(Java实现)---第十篇

博主秋招提前批已拿百度、字节跳动、拼多多、顺丰等公司的offer,可加微信:pcwl_Java 一起交流秋招面试经验,可获得博主的秋招简历和复习笔记。 

目录:

一、散列表基本概念

1、基本定义

2、散列表思想

二、散列函数

1、定义

2、散列函数设计的基本要求

3、如何设计散列函数

三、散列冲突

1、开放寻址法

2、链表法

3、如何选择散列冲突解决的方法

四、装载因子

五、散列表应用场景

1、Java中的HashMap

1、Word文档中单词拼写检查功能

2、假设我们有10万条URL访问日志,如何按照访问次数给URL排序?

3、有两个字符串数组,每个数组大约有10万条字符串,如何快速找出两个数组中相同的字符串?

六、总结


一、散列表基本概念

1、基本定义

散列表(Hash  Table,又叫哈希表),是根据关键码值(Key  Value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

2、散列表思想

符号表是一种用于存储键值对(Key -- Value)的数据结构,数组也可以看做是一种特殊的符号表,其中数组的索引即为键,数组元素为相应的值。也就是说,当符号表所有的键都是较小的整数时,我们可以使用数组来实现符号表,将数组的索引作为键,而索引处的数组元素即为对应的值,但是这一表示仅限于所有的键都是比较小的整数时,否则可能会使用一个非常大的数组。而散列表是对以上策略的一种升级,它可以支持任意的键而并没有对他们作过多的限定,对于基于散列表的符号表,若我们要在其中查找一个键,需要以下步骤,当然这也是散列表的思想:

散列表思想:

(1)使用散列函数将给定键转化为一个“数组的索引”,理想情况下,不同的key会被转化为不同的索引,但是在实际情况中,我们会遇到不同的键转化为相同索引的情况,这种情况叫做散列冲突/碰撞,后文中会详细讲解;

(2)得到了索引后,我们就可以像访问数组一样,通过这个索引访问到相应的键值对。

散列表是“时间--空间”权衡的例子。当我们的空间无限大时,我们可以直接使用一个很大的数组来保存键值对,并用key作为数组索引,因为空间不受限,所以我们的键的取值可以无穷大,因此查找任何键都只需要一次普通的数组访问。反过来,若查找操作没有任何时间上的限制,我们就可以直接使用链表来保存所有的键值对,这样把空间的使用降到最低,但查找时只能顺序查找。

然而,在时间的应用中,我们的时间和空间都是有限的。所以,我们必须要在两者之间做出权衡,散列表在时间和空间的使用上找到了一个很大的平衡点。散列表的一个优势在于我们只需要调整散列算法的相应参数而无需对其他部分的代码做任何修改就能在时间和空间上做出策略调整。

从上面可以看出:散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来,可以说,如果没有数组,就没有散列表。


二、散列函数

1、定义

散列函数(又叫哈希函数),顾名思义,它是一个函数。我们可以把它定义为hash(key),其中key表示元素的键值,hasn(key)的键表示经过散列函数计算得到的散列值。

2、散列函数设计的基本要求

(1)散列函数计算得到的散列值是一个非负整数;

(2)如果key1  =  key2,那么hash(key1)  ==  hash(key2);

(3)如果key1  !=  key2,那么hash(key1)  !=  hash(key2)。

解释下上面三点:

第一点:数组下标是从0开始的,所以散列函数生成的散列值也要上非负整数;

第二点:相同的key,经过散列函数得到的散列值也应该是相同的;

第三点:这个要求看起来没什么问题,但是在真实情况下,要想找到一个不同的key对应的散列值都不一样的散列函数,几乎是不可能的。即便是业界非常著名的MD5、SHA、CRC等哈希算法,也无法避免这种散列冲突。而且,因为数组的存储空间有限,所以也会加大散列冲突的概率。

3、如何设计散列函数

散列函数的设计的好坏,决定了散列冲突的概率大小,也直接决定了散列表的性能,那么我们应该如何设计散列函数呢?

(1)散列函数的设计不能太复杂,过于复杂的散列函数,势必会消耗很多计算时间,也会间接的影响到散列表的性能;

(2)散列函数生成的值要尽可能随机且均匀分布,这样才能避免或者最小化散列冲突,而且即便出现冲突,散列到每个槽里的数据也会比较平均,不会再出现某个槽里数据特别多的情况。

实际工作中,我们还需要综合考虑各种因素,比如:关键字的长度、特点、分布以及散列表的大小等。常用的散列函数的设计方法有:直接寻址法、平方取中法、随机数法等等,这些方法了解即可,不是重点。

<1> 直接寻址法:

取k或者k的某个线性函数为Hash值;

特点:由于直接寻址法相当于有多少个关键字就必须有多少个相应的地址去对应,所以不会产生冲突,也正因为这样,所以实际中很少用到这种方法。

<2> 数字分析法:

首先分析待存的一组关键字,比如是一个班级学生的出生年月日,我们发现他们的出生年份大体相同,那么我们肯定不能用他们的年来作为存储地址,这样出现冲突的概率非常大。但是,我们发现月日的具体数字差别很大,如果我们用月日来作为Hash值,则会明显降低冲突几率。因此,数字分析法就是找出关键字的规律,尽可能的用差异数据来构造Hash地址;

特点:需要提前知道所有的关键字,才能分析运用此种方法,所以不太常用。

<3> 平方取中法:

先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。这种方法比较常用。

例:我们把英文字母在字母表中的位置序号作为该英文字母的内部编码。例如K的内部编码为11,E的内部编码为05,Y的内部编码为25,A的内部编码为01, B的内部编码为02。由此组成关键字“KEYA”的内部代码为11052501,同理我们可以得到关键字“KYAB”、“AKEY”、“BKEY”的内部编码。之后对关键字进行平方运算后,取出第7到第9位作为该关键字哈希地址,如下图所示:

关键字 内部编码 内部编码的平方值 H(k)关键字的哈希地址
KEYA 11050201 122157778355001 778
KYAB 11250102 126564795010404 795
AKEY 01110525 001233265775625 265
BKEY 02110525 004454315775625 315

<4> 折叠法:

将关键字分割成位数相同的几部分(最后一部分位数可以不同),然后取这几部分的叠加和(去除进位)作为散列地址。数位叠加可以有移位叠加和间界叠加两种方法。移位叠加是将分割后的每一部分的最低位对齐,然后相加;间界叠加是从一端向另一端沿分割界来回折叠,然后对齐相加。

<5>随机数法:

选择一个随机函数,取关键字的随机函数值作为Hash地址 ,通常用于关键字长度不同的场合。

特点:通常,关键字长度不相等时,采用此法构建Hash函数 较为合适。

<6>除留取余法:

取关键字被某个不大于Hash表 长m 的数p 除后所得的余数为Hash地址 。

特点:这是最简单也是最常用的Hash函数构造方法。可以直接取模,也可以在平方法、折叠法之后再取模。

值得注意的是,在使用除留取余法 时,对p 的选择很重要,如果p 选的不好会容易产生同义词 。

由经验得知:p 最好选择不大于表长m的一个质数 、或者不包含小于20的质因数的合数。

【说明:上诉6种方法出自于:https://blog.csdn.net/xiaoxik/article/details/74926090


三、散列冲突

【ps:此段落内容摘抄自极客时间的《数据结构与算法之美》专栏】

首先,我们需要知道的是:再好的散列函数也无法避免散列冲突。

如何处理冲突是哈希表不可缺少的一个方面,现在完整的描述下处理冲突:【不适用于链表法】<

  • 17
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值