#导入MD文档图片#【强烈推荐,建议收藏】来吧!一文完全搞定哈希表!

哈希表是个啥?

小白: 庆哥,什么是哈希表?这个哈希好熟悉,记得好像有HashMap和HashTable之类的吧,这是同样的嘛?java

庆哥: 这个哈希确实常常见,足以说明它是个使用很是频繁的玩意儿,并且像你说的HashMap和HashTable之类的与哈希这个词确定是有关系的,那哈希是个啥玩意啊,这个我们仍是得先来搞明白啥是个哈希表。数组

咱们看看百科解释吧:markdown

散列表Hash table,也叫哈希表),是根据(Key)而直接访问在内存存储位置的数据结构。也就是说,它经过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称作散列函数,存放记录的数组称作散列表数据结构

怎么样?看到这个,你知道哈希表是什么了嘛?ide

小白: 我以前是对哈希表一窍不通啊,不过看了这个百科的解释,我知道以下这些关于哈希表的简单知识点:函数

一、哈希表其实也叫散列表,两个是一个玩意,英文是Hash Table性能

二、哈希表是一个数据结构设计

这两个概念是比较清晰的,至于其余的说什么映射函数叫作散列函数,存放记录的数组叫作散列表这个就有点模糊了,尤为说存放记录的数组称为散列表,那意思是哈希表是个数组?3d

庆哥: 首先你说的很清晰的两点说的是很准确的,哈希表也叫作散列表,这只不过是叫法而已,英文单词是Hash table,看到这个英文单词基本上就能猜到,哈希表其实就是直接根绝英文单词音译过来的,至此你应该知道了啥是哈希了吧,对于另一点,那就很重要了,那就是哈希表实际上是一种数据结构。指针

要知道数据结构有不少中,每一种都有各自的特色,那么哈希表既然也是一种数据结构,那它有什么特色呢?按照百科的解释,咱们大体能知道:能够根据一个key值来直接访问数据,所以查找速度快

对了,你知道最基本的几个数据结构中,哪一个的查询效率是最高的嘛?

小白: 据我所知,应该是数组吧,咱们能够直接使用数组下标来访问数据,所以查询效率是很高滴

庆哥: 对,很是对,哈希表其实本质上就是一个数组 。

小白: 那为啥还叫哈希表呢?,哈希表确定有啥特别的吧,为啥本质上是一个数组呢?

哈希表本质是数组?

庆哥: 必须滴啊,哈希表本质上是个数组,只能说它的底层实现是用到了数组,简单点说,在数组的这个基础上再捯饬捯饬,加工加工,变得更加有特点了,而后人家就自立门户,叫哈希表

小白: 这是咋个回事啊

庆哥: 为何说哈希表的本质是个数组呢?那就得看看,哈希表是怎么来实现的了,通常来讲啊,实现哈希表咱们能够采用两种方法:

一、数组+链表

二、数组+二叉树

简单点就有这么两种方式,其实说白了,不管哪一个都是必须有数组啊,都是再数组的基础上取搞其余的,并且好比第一种数组+链表的形式,本质上是出现哈希冲突的一种解决办法,使用链表存放,因此综合起来叫作数组+链表的方式来实现一个哈希表,另外数组中通常就是存放的单一的数据,而哈希表中存放的是一个键值对,这是个区别吧!

小白: 停!!!有点迷糊,什么哈希冲突,什么玩意儿啊

庆哥: ,好吧好吧,我说的有点着急了,你就记住,哈希表在本质上就是个数组就ok了。

小白: 但是我仍是像知道为啥啊?

庆哥: 别着急啊,咱慢慢来说,另外在百科上有这么一个例子,能够帮助你更好的理解哈希表是个啥,它是这样说的:

在这里插入图片描述

怎么样?看的懂嘛?

小白: 反正是有点模糊,这其中提到的函数关系啊,关键字啊,散列函数还有什么函数法则的有点迷迷糊糊的

哈希表的几个概念

啥是散列函数

庆哥: 确实,这都是哈希表中很重要的几个概念,那咱就先搞懂这几个概念吧,我用大白话给你说说这个例子。

好比说,我如今给你个电话本,上面记录的有姓名和对应的手机号,我想让你帮我找王二的手机号是多少,那么你会怎么作呢?

小白: 这样啊,那我就挨个找王二呗?

庆哥: 确实能够,那么你有没有想过,若是这个王二是在最后几页,那你去岂不是前面几页都白找了,有没有更快的方式呢?

小白: 也是哦,那这样的话,是否是能够按照人名给分个类,好比按照首字母来排序,就abcd那样的顺序,这样根据王二我就知道去找w这些,这样不久快不少了

庆哥: 的确,咱们能够按照人名的首字母去弄一个表格,好比像这样:

#导入MD文档图片#【强烈推荐,建议收藏】来吧!一文完全搞定哈希表!

你看,假如如今我让你去帮我找王二的手机号,你一会儿就能找到,不用再挨个的去查找了,这效率就高的多了,那么如今重点来了,人家原本叫王二,你为啥用一个w来标记人家呢?让你找王二为啥你不直接找王二而是去找w呢?

小白: 这个?用w能够更快速的去定位到王二啊

庆哥: 说的很对,咱们取姓名的首字母做为一个标志,就能够很快的找到以这个字母开头的人名了,那么王二也就能更快的被咱们找到,咱们也不用再费力气去找什么张二和李二的,由于人家的名字首字母都不是w。

小白: 那必须啊,这个方法好吧

庆哥: 对对对,你说到点子上了,那就是方法二字,这里咱们就是采用一种方法,什么方法呢?那就是取姓名的首字母作一个排序,那么这是否是就是经过一些特定的方法去获得一个特定的值,好比这里取人名的首字母,那么若是是放到数学中,是否是就是相似一个函数似的,给你一个值,通过某些加工获得另一个值,就像这里的给你我的名,通过些许加工咱们拿到首字母,那么这个函数或者是这个方法在哈希表中就叫作散列函数,其中规定的一些操做就叫作函数法则,这下你知道什么是散列函数了吧

小白: 嗯呢,这下真的是很清楚了,说白了,不就是给我一个值,通过捯饬一下,变成另一个值吗?花个图的话就是这个样子:

#导入MD文档图片#【强烈推荐,建议收藏】来吧!一文完全搞定哈希表!

哈哈,是否是这样?

庆哥: 简单来讲就是这样滴,咋样,这下知道什么是散列函数了吧?

关键值key是啥?

小白: 这下知道了,很清楚,那这个关键字key是个啥玩意?

庆哥: 这个也好理解啊,就像你画的这个图,1是怎么得出来得,是否是根据未加工以前得101得出来得,这个加工过程其实就是个散列函数,而丢给它的这个101就是这个关键值啊,为啥叫它关键值嘞,那是由于咱们要对它作加工才能得出咱们想要的1啊,你说它关不关键

小白: 哦哦,原来是这样啊,这下就明白啦!对了,我如今有这样的一个理解,你看看对不对啊,那就是哈希表就是经过将关键值也就是key经过一个散列函数加工处理以后获得一个值,这个值就是数据存放的位置,咱们就能够根据这个值快速的找到咱们想要的数据,是否是这样啊?

庆哥: 说的很正确,那你如今对以前那个百科的例子懂了嘛?

小白: 嗯呢,这下懂了

庆哥: 嗯呢,那就好,其实吧,上面的那中状况并很差,为啥嘞?你想啊,王二在那个位置,若是再来个王三呢?人家的首字母也是w啊,这咋办,位置被王二占了,那王三咋办?这就是哈希冲突啊,撞衫啦

小白: 阿西吧,又是哈希冲突,它到底彷佛个啥玩意啊

庆哥: 不着急,我们继续来探究哈希表。

再探哈希表

庆哥: 咱们在以前已经知道了哈希表的本质实际上是个数组,数组有啥特色啊?

小白: 数组嘛,那就是下表从0开始啊,连续的,直接经过下标访问,好比下面这样:

#导入MD文档图片#【强烈推荐,建议收藏】来吧!一文完全搞定哈希表!

有一个数组a,咱们能够直接经过a[1]的形式来访问到数值7,因此查询效率很高。

庆哥: 彻底正确,那么哈希表本质上是个数组,那它跟这个相似吗?咱们来再深刻探究一下,首先看个图:

#导入MD文档图片#【强烈推荐,建议收藏】来吧!一文完全搞定哈希表!

这张图但是信息量很大啊,你看出来个啥了嘛?

小白: 这个?我看到了哈希函数,这是啥?它跟散列函数有啥区别啊?还有Entry是个什么鬼,还有键值对,蒙圈啊

哈希函数

庆哥: 别蒙圈啊,我来仔细跟你说说,其实这个哈希函数就是咱们以前说的散列函数,为啥嘞?这就跟哈希表也叫作散列表同样啊,你叫做散列表的时候有个散列函数,那你叫哈希表的时候,也得有个哈希函数啊,这样才公平嘛,咋样,知道了吧?

小白: 我去,原来是这么回事啊,那键值对跟Entry嘞?

键值对和Entry

庆哥: 这个但是值得好好说道说道,咱们知道哈希表本质上是个数组,难道就跟数组的基本使用那样,存个数值,而后经过下表读取之类的嘛?固然不是啦,对于哈希表,它常常存放的是一些键值对的数据,啥是键值对啊,就是咱们常常说的key-value啊,简单点说就是一个值对应另一个值,好比a对应b,那么a就是key,b是value,哈希表存放的就是这样的键值对,在哈希表中是经过哈希函数将一个值映射到另一个值的,因此在哈希表中,a映射到b,a就叫作键值,而b呢?就叫作a的哈希值,也就是hash值。

咋样,这块明白了嘛?

小白: 嗯嗯,明白的,庆哥继续!

庆哥: 那好,咱们继续,键值对说的简单点就是有一个key和一个value对应着,好比这张图里的学生信息:

#导入MD文档图片#【强烈推荐,建议收藏】来吧!一文完全搞定哈希表!

学生的学号和姓名就是一个键值对啊,根据这个学号就能找到这个学生的姓名,那啥是Entry嘞,咱们都知道键值对,在不少语言中也许都有键值对,说白了就是个大众脸啊,咋弄,在咱jdk中可不能那么俗气,不能再叫键值对了,叫啥嘞,那就叫Entry吧

咋样,知道啥是键值对和Entry了吧!

小白: 必须滴啊,讲的那么生动,这张图感受远不止如此啊,庆哥继续啊

哈希表如何存数据

庆哥: 好滴,那我们就继续,来讲说哈希表是如何存放数据的,记得看上面的图啊,咱们按照这个图来讲,咱们已经知道了哈希表本质是个数组,因此这里有个数组,长度是8,如今咱们要作的是把这个学生信息存放到哈希表中,也就是这个数组中去,那咱们须要考虑怎么去存放呢?

这里的学号是个key,咱们以前也知道了,哈希表就是根据key值来经过哈希函数计算获得一个值,这个值就是用来肯定这个Entry要存放在哈希表中的位置的,实际上这个值就是一个下标值,来肯定放在数组的哪一个位置上。

好比这里的学号是101011,那么通过哈希函数的计算以后获得了1,这个1就是告诉咱们应该把这个Entry放到哪一个位置,这个1就是数组的确切位置的下标,也就是须要放在数组中下表为1的位置,如图中所示。

咱们以前已经介绍过什么是Entry了,因此这里你要知道,数组中1的位置存放的是一个Entry,它不是一个简单的单个数值,而是一个键值对,也就是存放了key和value,key就是学号101011,value就是张三,咱们通过哈希函数计算得出的1只是为了肯定这个Entry该放在哪一个位置而已。

如今咱们就成功把这个Entry放到了哈希表中了,怎么样,这块听懂了嘛?

小白: 嗯嗯,听懂了,不过看到这里我产生了一个疑问,那就是这个哈希函数,是否是有一个特定的加工过程,好比能够通过某种计算把101011转换成1,那么有没有可能其余的学号通过哈希函数的计算也得出1呢?那这个时候是否是就撞衫啦

哈希冲突

庆哥: 的确,你分析得很正确,咱们再来看下面这张图:

#导入MD文档图片#【强烈推荐,建议收藏】来吧!一文完全搞定哈希表!

你说的这种状况就像图中展现的那样,学号为102011的李四,他的学号通过哈希函数的计算也得出了1,那么也要放到数组中为1的位置,但是这个位置以前已经被张三占了啊,这怎么办?这种状况就是哈希冲突或者也叫哈希碰撞。

既然出现了这状况,不能无论李四啊,总得给他找个位置啊,怎么找呢?

小白: 我猜确定有什么方法能够给李四找位置

处理哈希冲突

庆哥: 那必须滴啊,有什么方法呢?其实关于哈希冲突的解决办法有好几种嘞,可是我这里只介绍两种主要的方法,一个是开放寻址法,一个是拉链法。

那什么是开放寻址法呢?咱们继续来看图:

#导入MD文档图片#【强烈推荐,建议收藏】来吧!一文完全搞定哈希表!

我以为看图就足以说明问题了,这里所说的开放寻址法其实简单来讲就是,既然位置被占了,那就另外再找个位置不就得了,怎么找其余的位置呢?这里其实也有不少的实现,咱们说个最基本的就是既然当前位置被占用了,咱们就看看该位置的后一个位置是否可用,也就是1的位置被占用了,咱们就看看2的位置,若是没有被占用,那就放到这里呗,固然,也有可能2的位置也被占用了,那咱就继续往下找,看看3的位置,一次类推,直到找到空位置。

对了,Java中的ThreadLocal就是利用了开放寻址法。

小白: 啥是ThreadLocal啊

庆哥: 咋滴,你不知道啊,没事,庆哥最近会写一篇ThreadLocal的文章,到时候记得来看哦!

小白: 嗯嗯,我会好好看看的。那什么是拉链法啊?

庆哥: 拉链法也是比较经常使用的,像以前你说的HashMap就是使用了这种方法,那这个方法是怎么个回事呢?咱们继续来看图:

#导入MD文档图片#【强烈推荐,建议收藏】来吧!一文完全搞定哈希表!

以前说的开放寻址法采用的方式是在数组上另外找个新位置,而拉链法则不一样,仍是在该位置,但是,该位置被占用了咋整,总不能打一架,谁赢是谁的吧,固然不是这样,这里采用的是链表,什么意思呢?就像图中所示,如今张三和李四都要放在1找个位置上,可是张三先来的,已经占了这个位置,待在了这个位置上了,那李四呢?解决办法就是链表,这时候这个1的位置存放的不仅仅是以前的那个Entry了,此时的Entry还额外的保存了一个next指针,这个指针指向数组外的另一个位置,将李四安排在这里,而后张三那个Entry中的next指针就指向李四的这个位置,也就是保存的这个位置的内存地址,若是还有冲突,那就把又冲突的那个Entry放在一个新位置上,而后李四的Entry中的next指向它,这样就造成了一个链表。

好啦,这就是拉链法,咋样,明白不

小白: 信息量很多啊,好在庆哥讲的比较清楚,明白啦

庆哥: 明白了就好,那我问你一个问题啊,针对开放寻址和拉链法,你有没有以为会产生什么问题呢?

小白: 嗯嗯,我还真有问题,首先是这个拉链法啊,若是冲突的不少,那这个增长的链表岂不是很长,这样也不咋好吧

庆哥: 的确,若是冲突过多的话,这块的链表会变得比较长,怎么处理呢?这里举个例子吧,拿java集合类中的HashMap来讲吧,若是这里的链表长度大于等于8的话,链表就会转换成树结构,固然若是长度小于等于6的话,就会还原链表。以此来解决链表过长致使的性能问题。

小白: 为啥是小于等于6啊,咋不是7嘞

庆哥: 这样设计是由于中间有个7做为一个差值,来避免频繁的进行树和链表的转换,由于转换频繁也是影响性能的啊。

小白: 嗯嗯,这个知道了,关于开放寻址也有个疑问,那就是若是一直找不到空的位置咋整啊?

庆哥: 这个不会的,为啥嘞?你这样想,是由于你考虑了一个前提,那就是位置已经被占光了,没有空位置了,可是实际状况是位置不会被占光的,由于有必定量的位置被占了的时候就会发生扩容。

小白: 阿西吧,还有扩容,那这个扩容是咋回事呢?

哈希表的扩容

庆哥: 其实这里不只仅是由于你说的那种状况才会扩容,还有一个很重要的缘由就是当哈希表被占的位置比较多的时候,出现哈希冲突的几率也就变高了,因此颇有必要进行扩容。

那么这个扩容是怎么扩的呢?这里通常会有一个增加因子的概念,也叫做负载因子,简单点说就是已经被占的位置与总位置的一个百分比,好比一共十个位置,如今已经占了七个位置,就触发了扩容机制,由于它的增加因子是0.7,也就是达到了总位置的百分之七十就须要扩容。

还拿HashMap来讲,当它当前的容量占总容量的百分之七十五的时候就须要扩容了。

并且这个扩容也不是简单的把数组扩大,而是新建立一个数组是原来的2倍,而后把原数组的全部Entry都从新Hash一遍放到新的数组。

小白: 这个从新Hash一遍是啥意思啊?

庆哥: 由于数组扩大了,因此通常哈希函数也会有变化,这里的Hash也就是把以前的数据经过新的哈希函数计算出新的位置来存放。

小白: 嗯嗯,原来是这么回事啊,懂了,对了,那哈希表的数据读取怎么操做的啊?

哈希表如何读取数据

庆哥: 要知道这个读取操做,咱们还来看这个图:

#导入MD文档图片#【强烈推荐,建议收藏】来吧!一文完全搞定哈希表!

好比咱们如今要经过学号102011来查找学生的姓名,怎么操做呢?咱们首先经过学号利用哈希函数得出位置1,而后咱们就去位置1拿数据啊,拿到这个Entry以后咱们得看看这个Entry的key是否是咱们的学号102011,一看是101011,什么鬼,一边去,这不是咱们要的key啊,而后根据这个Entry的next知道下一给位置,在比较key,好成功找到李四。

小白: 哦哦,原来是这么回事啊,那对于开放寻址那种是否是也是这个思路,先肯定到这个位置,而后再看这个位置上的key是否是咱们要的,如过不是那就看看下一个位置的。

庆哥: 能够的,彻底正确,好了如今咱们对哈希表的讲解已经差很少了,那么你以为对于哈希表而言,什么是核心呢?

哈希函数是核心

小白: 我以为应该是哈希函数吧,通过上面的讲解,我以为,若是一个哈希函数设计的足够好的话,就会减小哈希冲突的几率,若是设计的很差,那就会常常撞衫,那就很影响性能了,好比刚开始咱们举的那个例子,拿姓名的首字母来肯定位置,这个哈希函数的设计就不咋滴,好比王二,王三,王四什么的,这都会冲突啊

庆哥: 的确,在哈希表中,哈希函数的设计很重要,一个好的哈希函数能够极大的提高性能,并且若是你的哈希函数设计的比较简单粗陋,那很容易被那些不怀好意的人捣乱,好比知道了你哈希函数的规则,故意制造容易冲突的key值,那就有意思了,你的哈希表就会一直撞啊,一直撞啊

小白: 哈哈,那设计哈希函数有什么方法吗?

庆哥: 必须有啊,好比有直接定址法,数字分析法,折叠法,随机数法和除留余数法等等,要不要继续讲啊

小白: 我去,仍是不要了吧,消化不了啊,此次先到这吧,谢谢庆哥

感谢各位大大的阅读

相关文章
相关标签/搜索