【简约而不简单:神级代码的小秘密】| 第一章 哈希表

【简约而不简单:神级代码的小秘密】| 第一章 哈希表,第1张

【简约而不简单:神级代码的小秘密】| 第一章 哈希表 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表达式可能描述的概念不够明确,以10000个数为例。这里的log,统一以2为底进行计算,后续章节不再赘述。

        

        

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

1.2.3 哈希去重

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

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

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

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

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

1.2.4 关键词搜索

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

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

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

 public boolean longestDupSubstring(String s,String word) {
        if(s.length() 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的做法应运而生。

        常见的有:UUID,snowflake,此处不进行详细描述。大致的生成方式是,根据时间,主机号等固定参数,生成哈希散列值,由于哈希碰撞的概率非常低,因此可以当做唯一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.3.3 哈希碰撞

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

1.3.3.1 开放地址法

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

        优点:处理简单。

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

1.3.3.2 链地址法

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

 

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

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

 1.3.3.3 红黑树法

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

 

 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5684048.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存