散列表(字典)

问题

你在一家杂货店上班。有顾客来买东西时,你得在一个本子中查找价格。

如果本子的内容不是按字母顺序排列的,你可能为查找苹果(apple)的价格而浏览每一行,这需要很长的时间。使用的是简单查找,需要浏览每一行。时间复杂度为 O ( n ) O(n) O(n)

如果本子的内容是按字母顺序排列的,可使用二分查找来找出苹果的价格,这需要的时间更短,为 O ( l o g n ) O(log n) O(logn)

前面介绍了两种用于查找的数据结构:数组链表,为了针对上面的问题,有个更快的查找方式,引入散列表。查找时使用散列函数

散列函数

散列函数是这样的函数,即无论你给它什么数据,它都还你一个数字。用专业术语来表达的话,我们会说,散列函数“将输入映射到数字”。

需要满足的要求:

  • 单一映射,对于同样的输入,总能得到相同的输出。
  • 应将不同的输入映射到不同的数字。(可逆函数)

对于上面的问题,使用散列表来解决,首先创建一个空数组。在这个数组中存储商品价格。如苹果价格,为此,将apple作为输入交给散列函数,散列函数输出为数组索引,根据索引我们就能找到对应位置苹果的价格。

散列函数准确地指出了价格的存储位置,你根本不用查找!之所以能做到,原因有:

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

不同于数组链表都被直接映射到内存,散列表更复杂,它使用散列函数来确定元素的存储位置。

散列表也被称为散列映射、映射、字典和关联数组。

应用案例

将散列表用于查找

创建一个话簿,每个姓名都有对应的电话号码,需要提供如下功能:

  • 添加联系人及其电话号码。
  • 通过输入联系人来获悉其电话号码。

这非常适合使用散列表来实现!在下述情况下,使用散列表是很不错的选择。

  • 创建映射。
  • 查找。

创建电话簿非常容易。首先,新建一个散列表。

phone_book = dict()

Python还提供了一种创建散列表的快捷方式——使用一对大括号。

phonr_book = { } 	#与phone_book = dict()等效

下面在电话簿中添加一些联系人的电话号码:

phone_book["jenny"] = 8675309
phone_book["emergency"] = 120

现在,假设你要查找Jenny的电话号码,为此只需向散列表传入相应的键.

>>> print phone_book["jenny"]
8675309

散列表被用于大海捞针式的查找。例如,你在访问像http://adit.io这样的网站时,计算机必须将adit.io转换为IP地址。这个过程被称为DNS解析(DNS resolution),这不是将网址映射到IP地址吗?散列表是提供这种功能的方式之一.

防止重复

假设你负责管理一个投票站。显然,每人只能投一票,但如何避免重复投票呢?有人来投票时,你询问他的全名,并将其与已投票者名单进行比对。

为此,首先创建一个散列表,用于记录已投票的人。

voted = { }

有人来投票时,检查他是否在散列表中。

value = voted.get("tom")

如果“tom”在散列表中,函数get将返回它;否则返回None。你可使用这个函数检查来投票的人是否投过票!

代码如下:

voted = {}

def check_voter(name):
    if voted.get(name):
        print "kick them out!"
    else:
        voted[name] = True
        print "let them vote!"

使用散列表来检查是否重复,速度非常快。

将散列表用作缓存

假设你访问网站facebook.com:

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

facebook的服务器处理需要时间,为了少做工作,提高facebook网站的访问速度,服务器使用缓存,对于经常使用的主页等,不让服务器生成它,而是将主页存储起来,在需要时直接发送给用户。缓存具有如下优点:

  • 用户能够更快地看到网页。
  • Facebook需要做的工作更少。

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

Facebook不仅缓存主页,还缓存About页面、Contact页面、Terms and Conditions页面等众多其他的页面。因此,它需要将页面URL映射到页面数据

当你访问Facebook的页面时,它首先检查散列表中是否存储了该页面。
代码如下:

cache = {}

def get_page(url):
    if cache.get(url):
        return cache[url]
    else:
        data = get_data_from_server(url)
        cache[url] = data
        return data

仅当URL不在缓存中时,你才让服务器做些处理,并将处理生成的数据存储到缓存中,再返回它。这样,当下次有人请求该URL时,你就可以直接发送缓存中的数据,而不用再让服务器进行处理了。

冲突

前面讲到,散列函数最好是可逆函数,其总是将不同的键映射到数组的不同位置。实际上,几乎不可能编写出这样的散列函数。

当给两个键分配的位置相同时,就会产生冲突(collision),不同的输入得到相同的映射值。冲突很糟糕,必须要避免。处理冲突的方式很多,最简单的办法如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表

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

如何选择好的散列函数呢?

性能

在平均情况下,散列表的查找(获取给定索引处的值)速度与数组一样快,而插入和删除速度与链表一样快,因此它兼具两者的优点!但在最糟情况下,散列表的各种操作的速度都很慢。因此,在使用散列表时,避开最糟情况至关重要。为此,需要避免冲突。而要避免冲突,需要有:

  • 较低的填装因子;
  • 良好的散列函数。

装填因子

装 填 因 子 = 散 列 表 中 包 含 的 元 素 数 / 位 置 总 数 装填因子=散列表中包含的元素数/位置总数 =/

一旦填装因子开始增大,你就需要在散列表中添加位置,这被称为调整长度(resizing)

  • 首先创建一个更长的新数组:通常将数组增长一倍。
  • 使用函数hash将所有的元素都插入到这个新的散列表中。

填装因子越低,发生冲突的可能性越小,散列表的性能越高。一个不错的经验规则是:一旦填装因子大于0.7,就调整散列表的长度。调整长度的开销很大,因此你不会希望频繁地这样做。

良好的散列函数

良好的散列函数让数组中的值呈均匀分布,糟糕的散列函数让值扎堆,导致大量的冲突。

小结

散列表是一种功能强大的数据结构,其操作速度快,还能让你以不同的方式建立数据模型。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值