算法图解——读书笔记05

散列表

假设你在一家杂货店上班。有顾客来买东西时,你得在一个本子上查找价格。如果本子的内容不是按字母顺序排列的,你可能为查找苹果(apple)的 价格而浏览每一行,这需要很长时间。此时你使用的是简单查找,如果本子的内容是按照字母顺序排序的,可使用二分查找来找出苹果的价格,你知道,二分查找的速度非常快,但是作为收银员,在本子中查找价格是一件很痛苦的事情,哪怕本子的内容是有序的,在查找价格时,你都能感觉到顾客的怒气。看来真的需要一名能够记住所有商品价格的雇员,这样你就不用查找了:问她就能马上知道答案。
在这里插入图片描述
不管商品有多少,这位雇员(假设她的名字为Maggie)报出任何商品的价格的时间都为O(1),速度比二分查找都快。
在这里插入图片描述
你希望查找商品价格的时间为O(1),即你希望查找速度像Maggie那么快,这是散列函数的用武之地。

散列函数

散列函数是这样的函数,即无论你给它什么数据,它都还你一个数字。
在这里插入图片描述
如果用专业术语来表达的话,我们会说,散列函数“将输入映射到数字”。你可能认为散列函数输出的数字没有什么规律,但其实散列函数必须满足一些要求。
❑ 它必须是一致的。例如,假设你输入apple时得到的是4,那么每次输入apple时,得到的都必须为4。如果不是这样,散列表将毫无用处。
❑ 它应将不同的输入映射到不同的数字。例如,如果一个散列函数不管输入是什么都返回1,它就不是好的散列函数。最理想的情况是,将不同的输入映射到不同的数字。

散列函数将输入映射为数字,这有何用途呢?你可使用它来打造你的“Maggie”!

为此,首先创建一个空数组。
在这里插入图片描述
你将在这个数组中存储商品价格。下面来将苹果的价格加入到这个数组中,为此,将Aapple作为输入交给散列函数。
在这里插入图片描述
散列函数的输出为3,因此我们将苹果的价格存储到数组的索引3处。
在这里插入图片描述
下面将牛奶(milk)的价格存储到数组中。为此,将milk作为散列函数的输入。
在这里插入图片描述
散列函数的输出为0,因此我们将牛奶的价格存储在索引0处。
在这里插入图片描述
不断地重复这个过程,最终整个数组将填满价格。
在这里插入图片描述
现在假设需要知道鳄梨(avocado)的价格。你无需在数组中查找,只需将avocado作为输入交给散列函数。
在这里插入图片描述
它将告诉你鳄梨的价格存储在索引4处。果然,你在那里找到了。
在这里插入图片描述
散列函数准确地指出了价格的存储位置,你根本不用查找!之所以能够这样,具体原因如下。

❑散列函数总是将同样的输入映射到相同的索引。每次你输入avocado,得到的都是同一个数字,因此,你可首先使用它来确定将鳄梨的价格存储在什么地方,并在以后使用它来确定鳄梨的价格存储在什么地方。
❑ 散列函数将不同的输入映射到不同的索引。avocado映射到索引4, milk映射到索引0。每种商品都映射到数组的不同位置,让你能够将其价格存储到这里。
❑ 散列函数知道数组有多大,只返回有效的索引。如果数组包含5个元素,散列函数就不会返回无效索引100。

刚才你就打造了一个“Maggie”!你结合使用散列函数和数组创建了一种被称为散列表(hashtable)的数据结构,散列表由键和值组成。

应用案例

将散列表用于查找
手机都内置了方便的电话簿,其中每个姓名都有对应的电话号码。
在这里插入图片描述
假设你要创建一个类似这样的电话簿,将姓名映射到电话号码。该电话簿需要提供如下功能。
❑ 添加联系人及其电话号码。
❑ 通过输入联系人来获悉其电话号码。这非常适合使用散列表来实现!在下述情况下,使用散列表是很不错的选择。
❑ 创建映射。
❑ 查找。

创建电话簿非常容易。首先,新建一个散列表。
在这里插入图片描述
顺便说一句,Python提供了一种创建散列表的快捷方式——使用一对大括号。
在这里插入图片描述
下面在这个电话簿中添加一些联系人的电话号码。
在这里插入图片描述
这就成了!现在,假设你要查找Jenny的电话号码,为此只需向散列表传入相应的键。
在这里插入图片描述
防止重复
假设你负责管理一个投票站。显然,每人只能投一票,但如何避免重复投票呢?有人来投票时,你询问他的全名,并将其与已投票者名单进行比对。

如果名字在名单中,就说明这个人投过票了,因此将他拒之门外!否则,就将他的姓名加入到名单中,并让他投票。现在假设有很多人来投过了票,因此名单非常长。
在这里插入图片描述
每次有人来投票时,你都得浏览这个长长的名单,以确定他是否投过票。但有一种更好的办法,那就是使用散列表!

为此,首先创建一个散列表,用于记录已投票的人。
在这里插入图片描述
有人来投票时,检查他是否在散列表中。
在这里插入图片描述
如果“tom”在散列表中,函数get将返回它;否则返回None。你可使用这个函数检查来投票的人是否投过票!
在这里插入图片描述
将散列表用作缓存
来看最后一个应用案例:缓存。如果你在网站工作,可能听说过进行缓存是一种不错的做法。下面简要地介绍其中的原理。假设你访问网站facebook.com。

(1) 你向Facebook的服务器发出请求。
(2) 服务器做些处理,生成一个网页并将其发送给你。
(3) 你获得一个网页。
在这里插入图片描述
例如,Facebook的服务器可能搜集你朋友的最近活动,以便向你显示这些信息,这需要几秒钟的时间。作为用户的你,可能感觉这几秒钟很久,进而可能认为Facebook怎么这么慢!另一方面,Facebook的服务器必须为数以百万的用户提供服务,每个人的几秒钟累积起来就相当多了。为服务好所有用户,Facebook的服务器实际上在很努力地工作。有没有办法让Facebook的服务器少做些工作,从而提高Facebook网站的访问速度呢?

假设你有个侄女,总是没完没了地问你有关星球的问题。火星离地球多远?月球呢?木星呢?每次你都得在Google搜索,再告诉她答案。这需要几分钟。现在假设她老问你月球离地球多远,很快你就记住了月球离地球238900英里。因此不必再去Google搜索,你就可以直接告诉她答案。这就是缓存的工作原理:网站将数据记住,而不再重新计算。

在这里插入图片描述
这就是缓存,具有如下两个优点。
❑ 用户能够更快地看到网页,就像你记住了月球与地球之间的距离时一样。下次你侄女再问你时,你就不用再使用Google搜索,立刻就可以告诉她答案。
❑ Facebook需要做的工作更少。

缓存是一种常用的加速方式,所有大型网站都使用缓存,而缓存的数据则存储在散列表中!
在这里插入图片描述
小结:
❑ 模拟映射关系;
❑ 防止重复;
❑ 缓存/记住数据,以免服务器再通过处理来生成它们。

冲突
所谓冲突即:给两个键分配的位置相同。

举个栗子:假设你有一个数组,它包含26个位置。
在这里插入图片描述
而你使用的散列函数非常简单,它被字母顺序分配数组的位置。
在这里插入图片描述
你可能已经看出了问题。如果你要将苹果的几个存储到散列表中,分配给你的是第一个位置。
在这里插入图片描述
接下来,你要将香蕉的价格存储到散列表中,分配给你的是第二个位置。
在这里插入图片描述
一切顺利!但现在你要将鳄梨的价格存储到散列表中,分配给你的又是第一个位置。
在这里插入图片描述
不好,这个位置已经存储了苹果的价格!怎么办?这种情况就被称为冲突!这是个问题。如果你将鳄梨的价格存储到这个位置,将覆盖苹果的价格,以后再查询苹果的价格时,得到的是鳄梨的价格!冲突很糟糕,必须要避免。处理冲突的方式很多,最简单的方式如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表。
在这里插入图片描述
在这个例子中,apple和avocado映射到了同一个位置,因此在这个位置存储一个链表。在需要查询香蕉的价格时,速度依然很快。但在需要查询苹果的价格时,速度要慢些:你必须在相应的链表中找到apple。如果这个链表很短,也没什么大不了——只需搜索三四个元素。但是,假设你工作的杂货店只销售名称以字母A打头的商品。
在这里插入图片描述
等等!除第一个位置外,整个散列表都是空的,而第一个位置包含一个很长的列表!换言之,这个散列表中的所有元素都在这个链表中,这与一开始就将所有元素存储到一个链表中一样糟糕:散列表的速度会很慢。

这里的经验教训有两个。
❑ 散列函数很重要。前面的散列函数将所有的键都映射到一个位置,而最理想的情况是,散列函数将键均匀地映射到散列表的不同位置。
❑ 如果散列表存储的链表很长,散列表的速度将急剧下降。然而,如果使用的散列函数很好,这些链表就不会很长!

填装因子
散列表的梯安装因子很容易计算
在这里插入图片描述
散列表使用数组来存储数据,因此你需要计算数组中被占用的位置数。例如,下述散列表的填装因子为2/5,即0.4。
在这里插入图片描述
填装因子大于1意味着商品数量超过了数组的位置数。一旦填装因子开始增大,你就需要在散列表中添加位置,这被称为调整长度(resizing)。例如,假设有一个像下面这样相当满的散列表。
在这里插入图片描述
你就需要调整它的长度。为此,你首先创建一个更长的新数组:通常将数组增长一倍。

接下来,你需要使用函数Hash将所有的元素都插入到这个全新的散列表中。
在这里插入图片描述

这个新散列表的填装因子为3/8,比原来低多了!填装因子越低,发生冲突的可能性就越小,散列表的性能越高,一个不错的经验规则是:一旦填装因子大于0.7,就调整散列表的长度。

良好的散列函数让数组中的值呈均匀分布。
在这里插入图片描述

糟糕的散列函数让值扎堆,导致大量的冲突。
在这里插入图片描述
散列函数的结果必须是均匀分布的,这很重要。它们的映射范围必须尽可能大。最糟糕的散列函数莫过于将所有输入都映射到散列表的同一个位置。

总结:
❑ 你可以结合散列函数和数组来创建散列表。
❑ 冲突很糟糕,你应使用可以最大限度减少冲突的散列函数。
❑ 散列表的查找、插入和删除速度都非常快。
❑ 散列表适合用于模拟映射关系。
❑ 一旦填装因子超过0.7,就该调整散列表的长度。
❑ 散列表可用于缓存数据(例如,在Web服务器上)。
❑ 散列表非常适合用于防止重复。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值