哈希表

Posted 钰娘娘ynn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了哈希表相关的知识,希望对你有一定的参考价值。


1.1 什么是哈希表

        也叫散列表,是以键-值格式存储的数据接口。可以通过键来找到对应的值。

1.1.1 所以,什么是哈希表

        同事里有个叫子豪的精神小伙,门口有一份快递,快递上写着他他的名字。

        “你们这儿有个叫子豪的小伙子吗?”,快递员气喘吁吁的问道。

        “有,在那边,那个穿蓝色衣服的就是。”

        这个过程,其实可以把子豪看作是键,对应的人就是值。这个找人的过程就是哈希查找

1.1.2 哈希键怎么定义

        哈希的键可以理解为一种分类。比如前面找子豪的例子,那可能一个部门不止一个子豪(我们公司有5个,部门有2个)。所以这里“子豪”这个名字,他可以不能一个,是一群人。虽然可能无法准确的找到是谁,但是依靠“子豪”这个名字去找人,依旧非常有效,它把快递员找人的范围从几百个缩减到了5个。假设是个不那么容易重复的名字,立刻就找到是谁了。

        这个键的定义非常关键,它要足够有效,足够明确,重复一致。如此,才能称作“好键”,咳咳咳,谐音梗扣钱!

        1. 足够有效

反例:把公司身高3米以下(不包含)的人分为一类,3米以上人的分为一类。

                吐槽:全公司都是3米以下人,对于找人没有任何帮助呀(#`O′)!!!

                正例:1.5米到1.7米(不包含)分为一类,1.7米以上分为一类

        2. 足够明确

反例:把公司胖的分为一类,瘦的分为一类。

吐槽: o(* ̄▽ ̄*)o,没有标准我怎么知道自己算哪类。

                正例:体重60公斤以内的分为1类,60公斤以上的分为1类。

        3. 重复一致

反例:扔骰子,扔到3或一下的分为一类。

吐槽:怎么排好了重新再来一次,好多人位置变来变去,忽左忽右(⊙﹏⊙)

                正例:按姓的笔画数分类。

        具体地,数字通常使用取余的方式,因为取余可以较为均匀的分配到每个格子之内。而对于字母和符号,可以转为ASCII码或者Unicode码,进一步当作数字处理。计算机的主要处理方式是使用数据存储地址作为哈希取余的方式。

哈希表:把不同的分类放在不同的位置,从而实现快速查找的存储方式。

哈希查找:通过哈希表的分类,查找到对应类别内容的过程。

1.2 哈希表有啥用?

        其实,哈希上学的时候已经学过了。可直到毕业一年后,我也只知道这个有键值对的东西它叫哈希表,一直不会用。所以我觉得非常有必要描述一下,可能很多人会跟我一样。

1.2.1 散列函数

        比如你有个网站,可以查看影视评分。假设黑客截取了你的数据包,发现某个网址可以去查这个评分,恶意的反复访问这个网址,网站的资源就会被占满,最终无法访问,导致网站瘫痪宕机。

        为了预防黑客,可以用散列函数,给数据校验。每次传网址的时候,我们加两个参数,一个时间一个哈希。通过时间+参数配合,获取MD5(MD5 Message-Digest Algorithm一种被广泛使用的密码散列函数)校验。一方面可以防止信息被篡改,另一方面可以为信息定义失效时间,降低黑客恶意占用网络资源的风险。

1.2.2 统计计数

        还记得上学时候班长是怎么选的吗?它大概有几个步骤:

        1. 每位同学为自己喜爱的候选人投下可能会赢的票

        2. 在黑板上,分散的把候选人的名字写下来

        3. A同学读出名字,B同学在对应的名字下画“正”字

【简约而不简单:神级代码的小秘密】|

         按照不同的键值,把数量进行累计的过程,就叫做哈希计数。我们通过名字找到类别,从而找到对应的人,为对应的人加选票,最终可以知道每个人获取多少票。假设我们不用哈希计数,采用先排序(以快速排序为例),后计数的方式,平均复杂度是O(nlogn)。而使用哈希进行计数,插入时间复杂度为O(1),查找的时间复杂度为O(1),若是冲突较高的情况则为O(logn)。此处不做展开,后续章节将做进一步说明。这个效率大大增加了,大O表达式可能描述的概念不够明确,以10000个数为例。这里的log,统一以2为底进行计算,后续章节不再赘述。

O(nlogn)=10000*sqrt(10000)=10000*100=1000000

O(logn)=sqrt(10000)=100

        可见,使用哈希计数和排序后再计数的效率明显不同,它能大大提高程序的运行效率。

1.2.3 哈希去重

        互联网公司之间要举办一场运动会。

        其中BAT公司参加跑步的有100个人我们知道他们的名字,参加羽毛球比赛的有100个人我们知道他们的名字,参加团建游戏的有100个人我们知道他们的名字,其中,有一些人参加了多项比赛,这三份名单每一份各自对应一份统计参赛人员的名单。

        为了鼓励参赛的选手,运组委为每一位参赛的选手准备了一份经典时尚的小礼品。已知的参赛人员信息,只有三份名单统计表。       

        已知,参赛选手的姓名没有重复,运组委如何快速的统计这份名单,将小礼品分发给每位参赛选手呢?

        这种时候,可以采用哈希表的方式来处理。从 1.1.2 哈希键怎么定义 中我们知道了哈希的键具有重复一致的特性,那么,可以每次拿到一份名单就求出哈希值,把这个名字放在对应的位置,最后统计的数目就是名字的总数。

1.2.4 关键词搜索

        已知一个关键词长度为 m ,使用这个关键词,在一段长度为的文本 n 里进行搜索,需要怎么操作呢?我们当然可以一个个位置去匹配,但是这样做的时间复杂度是O(mn), 非常的耗时。

        那我们怎么办呢?可以用哈希。使用关键词生成一段哈希。然后从头到尾不断通过求出长度为 n 的文本中的长度为 m 的哈希值,与其进行匹配,就可以达成找到字符串的目的。这个时间复杂度是 O(n)。

        附代码,有兴趣的朋友可以看一下:

public boolean longestDupSubstring(String s,String word) 
if(s.length()<word.length())
return false;
Random random = new Random();
// 生成两个进制
int a1 = random.nextInt(75) + 26;
// 生成两个模
int mod1 = random.nextInt(Integer.MAX_VALUE - 1000000007 + 1) + 1000000007;
int n = s.length();
int m = word.length();
//v为哈希键
int v = 0;
for (int i = 0; i < m; ++i)
v = ((word.charAt(i) - a)+a1*v%mod1)%mod1;

return check(s,v,m,n,a1,mod1);


public boolean check(String s,int v, int m, int n,int a1, int mod1)
long aL1 = pow(a1, m, mod1);
long h1 = 0;
for (int i = 0; i < m; ++i)
h1 = (h1 * a1 % mod1 + (s.charAt(i)-a)) % mod1;


//哈希键一致,粗略认为含有关键字
if(h1==v)
return true;
for(int i = m; i < n; i++)
h1 = (h1 * a1-(s.charAt(i-m)-a)*aL1 + (s.charAt(i)-a)) % mod1;
//哈希键一致,粗略认为含有关键字
if(h1==v)
return true;

return false;


public long pow(int a, int m, int mod)
long ans = 1;
long contribute = a;
while (m > 0)
if (m % 2 == 1)
ans = ans * contribute % mod;
if (ans < 0)
ans += mod;


contribute = contribute * contribute % mod;
if (contribute < 0)
contribute += mod;

m /= 2;

return ans;

1.2.5 分布式

         在单一系统中,我们可以选用自增的ID,例如第一个是1,第二个是2...,以最后一个id递增1个的形式,来设计id,这样做的好处是,我们可以很方便的获取到下一个id,可以保证id一定不会发生重复。

        但是在分布式中,这种方式变的行不通了。分布式读者可以暂时理解为多个服务器协作,完成同样的功能。采用分布式的好处,是服务器可以在固定时间内承载更大的访问量。如果我们还是采取自增的方式,那同一时间内,只能有一台服务器获取到ID,不能达到我们期望服务器承受更大访问量的预期。因此,采用哈希键的方式生成ID的做法应运而生。

        常见的有:UUIDsnowflake,此处不进行详细描述。大致的生成方式是,根据时间,主机号等固定参数,生成哈希散列值,由于哈希碰撞的概率非常低,因此可以当做唯一ID来使用。

        除了生成唯一ID的运用,还有像服务器轮询算法,数据库分片查询等方面也有所运用。

1.3 哈希表的副作用

        在1.2节,我们阐述了哈希的一些常见用法,可见,哈希表的好处是很多的,那么它有没有什么副作用呢?那当然是有的。

1.3.1 有效信息丢失

        假设我有 1 到 10 个数 [1,2,3,4,5,6,7,8,9,10],进行哈希键计算时,采用 5 的余数的方式。像下面这样:

        

【简约而不简单:神级代码的小秘密】|

        可以看到,再此期间,我们可以使用 1 到 10 这 10 个数字,得到0-4四个余数;但是无法通过0-4 4个余数,还原出 1-10 10个数字,这个过程中,信息出现了丢失。甚至无法通过某个余数 3 ,还原出原本的数字是3还是7还是10。

        那么,通过哈希映射的方式,必然会产生数据丢失吗?答案是否定的,当键和原始数据的数值是一一对应的情况,可以认为除了排序以外,没有产生数据丢失,请看下面这个例子:

[1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5]

        数据有一定的重复,采用 1.2.2 哈希计数 的方式进行统计,我们得到这样的映射关系:

[1=>4,2=>4,3=>4,4=>4,5=>4]

        这个过程,对每个数据的数量进行统计,不但保留了数据,而且压缩了保存空间,节约了内存。

        从前面的分析中,我们可以得到这样一个结论:

        当哈希键与原始数据的键不是一一对应的情况,会产生信息丢失;

        当哈希键与原始数据的键是一一对应的情况,除了原始数据的排列顺序会丢失外,能完整的保留数据,甚至可以达到数据压缩的优化效果。

1.3.2 键重复

        对于同一个键,产生了多条有效信息,这个过程叫做键重复。对于键重复,我们可以怎样处理呢?

1.3.2.1 丢弃法

       在前面的章节 1.2.3 哈希去重 中描述了使用哈希去除重复元素的方式。对于无需保留重复数据的情况,可以直接丢弃,不做处理。

1.3.2.2 替换法

        有些情况,我们只需要最近的数据。例如我们查看短信,微信,QQ等通讯聊天工具时,会给我们一个消息提示列表,每个群组和个人,只保留最近的一条消息。这个过程中,聊天对象就是键,消息就是映射的值,对于同一个聊天对象发送多条数据的情况,新的聊天记录会替换掉旧的聊天记录,这就是替换方式。

1.3.2.3 列表法

        在 1.1.2 哈希键怎么定义 我们提到,哈希键是一种分类方式。有时候使用哈希,是用来分类的。那么,我们可以把哈希的映射值,设置成一个列表,满足对应哈希条件的分到此分类中。

1.3.2.4 降低冲突

       不得不讲讲雪花算法,雪花算法是为了解决多台机器同一毫秒多台机器同时生成 ID ,但是冲突概率很低的一种方式。它采用了:时间戳毫秒数+同步支持 1024 台机器+ 10 位顺序号,每个机器每毫秒同一台机器可以生成 4096 个 ID 不重复,这意味着需要再同一毫秒,对同一台机器访问 4096 次,假设选择机器的方式是通过随机,那么对于同一台机器重复概率为:

        1/(1024*4096)=1/4,194,304        

        不同机器间,同一毫秒重复的概率是:  

        1/(1024*4096*1023)=1/(4,290,772,992)

        

        再加上一毫秒的限制条件,实际上冲突概率非常低,近似可以认为是不会冲突的。

1.3.3 哈希碰撞

        通过 1.3.1 有效信息丢失 我们知道,当哈希键与原始数据的键不是一一对应的情况,是会产生信息丢失的。不光哈希键会产生重复,哈希键映射的存储哈希的索引位置也会产生重复。键映射的存储哈希的索引位置,产生重复的过程叫做哈希碰撞。处理哈希碰撞的方法有很多,这里只介绍有代表性的两种开放地址法和链地址法,另外介绍一种红黑树法

1.3.3.1 开放地址法

        出现重复,就通过规则,找到下一个没有存储键的地址进行存储。

        优点:处理简单。

        缺点:冲突加剧后,存储和查找键的效率降低,只能存储和给定空间大小一致或者更少的数据。

1.3.3.2 链地址法

        每个索引位置不单单放一个值,而是将数据以链表的形式进行存储。

【简约而不简单:神级代码的小秘密】|

        优点:无需重复查找键值,相对于开放地址法,同样定义的索引空间,可以存储更多的值,仍保证较高的效率。

        缺点:冲突加剧后,查询效率逐步降低。考虑到所有键都存在一个索引位的最坏情况,查询效率会由 O()退化至 O(n)。

 1.3.3.3 红黑树法

        出于降低链地址法带来的高冲突,进而导致查询效率降低冲突的影响,红黑树解决哈希冲突的方式,应运而生。红黑树属于二叉查找数,它的插入,查找,删除效率都是O(1)。在冲突较高的情况,可采用红黑树的方式替换链地址法,从而降低高冲突带来的负面效果。最低效率可由O(n^2)上升到O(logn)。


以上是关于哈希表的主要内容,如果未能解决你的问题,请参考以下文章

[数据结构] 哈希表 (开放寻址法+拉链法)

算法与数据结构之哈希表

哈希表/散列表

哈希表

算法哈希表的诞生(Java)

Java哈希表(散列表)解析