菜鸟学算法--散列表

目录

散列函数

应用案例

将散列表用于查找

防止重复

将散列表用作缓存

小结

冲突

性能

填装因子


本片为转载文章,内容来自书籍《算法图解》

散列函数

菜鸟学算法--散列表

  • 它必须是一致的。例如,假设你输入apple时得到的是4,那么每次输入apple时,得到的都必须为4。如果不是这样,散列表将毫无用处。
  • 它应将不同的输入映射到不同的数字。例如,如果一个散列函数不管输入是什么都返回1,
  • 它就不是好的散列函数。最理想的情况是,将不同的输入映射到不同的数字。

应用案例

将散列表用于查找

散列表被用于大海捞针式的查找。例如,你在访问像http://adit.io这样的网站时,计算机必须将adit.io转换为IP地址。

无论你访问哪个网站,其网址都必须转换为IP地址。

菜鸟学算法--散列表

这不是将网址映射到IP地址吗?好像非常适合使用散列表啰!这个过程被称为DNS解析(DNS resolution),散列表是提供这种功能的方式之一。

防止重复

假设你负责管理一个投票站。显然,每人只能投一票,但如何避免重复投票呢?有人来投票时,你询问他的全名,并将其与已投票者名单进行比对。每次有人来投票时,你都得浏览这个长长的名单,以确定他是否投过票。但有一种更好的办法,那就是使用散列表

菜鸟学算法--散列表

将散列表用作缓存

来看最后一个应用案例:缓存。如果你在网站工作,可能听说过进行缓存是一种不错的做法。下面简要地介绍其中的原理。假设你访问网站facebook.com。

(1) 你向Facebook的服务器发出请求。
(2) 服务器做些处理,生成一个网页并将其发送给你。
(3) 你获得一个网页

菜鸟学算法--散列表

如果你登录了Facebook,你看到的所有内容都是为你定制的。你每次访问facebook.com,其服务器都需考虑你感兴趣的是什么内容。但如果你没有登录,看到的将是登录页面。每个人看到的登录页面都相同。Facebook被反复要求做同样的事情:“当我注销时,请向我显示主页。”有鉴于此,它不让服务器去生成主页,而是将主页存储起来,并在需要时将其直接发送给用户。

菜鸟学算法--散列表

这就是缓存,具有如下两个优点。

  •  用户能够更快地看到网页,就像你记住了月球与地球之间的距离时一样。下次你侄女再问你时,你就不用再使用Google搜索,立刻就可以告诉她答案。
  • Facebook需要做的工作更少。

缓存是一种常用的加速方式,所有大型网站都使用缓存,而缓存的数据则存储在散列表中!

小结

这里总结一下,散列表适合用于:

  • 模拟映射关系;
  • 防止重复;
  • 缓存/记住数据,以免服务器再通过处理来生成它们。

冲突

我们来看一个简单的示例。假设你有一个数组,它包含26个位置。

菜鸟学算法--散列表

而你使用的散列函数非常简单,它按字母表顺序分配数组的位置。

菜鸟学算法--散列表

你可能已经看出了问题。如果你要将苹果的价格存储到散列表中,分配给你的是第一个位置。

菜鸟学算法--散列表

接下来,你要将香蕉的价格存储到散列表中,分配给你的是第二个位置。

菜鸟学算法--散列表

一切顺利!但现在你要将鳄梨的价格存储到散列表中,分配给你的又是第一个位置。

菜鸟学算法--散列表

不好,这个位置已经存储了苹果的价格!怎么办?这种情况被称为冲突(collision):给两个键分配的位置相同。这是个问题。如果你将鳄梨的价格存储到这个位置,将覆盖苹果的价格,以后再查询苹果的价格时,得到的将是鳄梨的价格!冲突很糟糕,必须要避免。处理冲突的方式很多,最简单的办法如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表。

菜鸟学算法--散列表

这里的经验教训有两个。

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

性能

在平均情况下,散列表执行各种操作的时间都为O(1)。O(1)被称为常量时间。你以前没有见过常量时间,它并不意味着马上,而是说不管散列表多大,所需的时间都相同。例如,你知道的,简单查找的运行时间为线性时间。

菜鸟学算法--散列表

二分查找的速度更快,所需时间为对数时间。

菜鸟学算法--散列表

在散列表中查找所花费的时间为常量时间。

菜鸟学算法--散列表

一条水平线,看到了吧?这意味着无论散列表包含一个元素还是10亿个元素,从其中获取数据所需的时间都相同。实际上,你以前见过常量时间——从数组中获取一个元素所需的时间就是固定的:不管数组多大,从中获取一个元素所需的时间都是相同的。在平均情况下,散列表的速度确实很快。

填装因子

散列表的填装因子很容易计算。

菜鸟学算法--散列表

散列表使用数组来存储数据,因此你需要计算数组中被占用的位置数。例如,下述散列表的填装因子为2/5,即0.4。

菜鸟学算法--散列表

下面这个散列表的填装因子为多少呢?

菜鸟学算法--散列表

如果你的答案为1/3,那就对了。填装因子度量的是散列表中有多少位置是空的。
假设你要在散列表中存储100种商品的价格,而该散列表包含100个位置。那么在最佳情况下,每个商品都将有自己的位置。

菜鸟学算法--散列表

这个散列表的填装因子为1。如果这个散列表只有50个位置呢?填充因子将为2。不可能让每种商品都有自己的位置,因为没有足够的位置!填装因子大于1意味着商品数量超过了数组的位置数。一旦填装因子开始增大,你就需要在散列表中添加位置,这被称为调整长度(resizing)。例如,假设有一个像下面这样相当满的散列表。

菜鸟学算法--散列表

你就需要调整它的长度。为此,你首先创建一个更长的新数组:通常将数组增长一倍。

菜鸟学算法--散列表

接下来,你需要使用函数 hash 将所有的元素都插入到这个新的散列表中。

菜鸟学算法--散列表

这个新散列表的填装因子为3/8,比原来低多了!填装因子越低,发生冲突的可能性越小,散列表的性能越高。一个不错的经验规则是:一旦填装因子大于0.7,就调整散列表的长度。
你可能在想,调整散列表长度的工作需要很长时间!你说得没错,调整长度的开销很大,因此你不会希望频繁地这样做。但平均而言,即便考虑到调整长度所需的时间,散列表操作所需的时间也为O(1)。