5、散列表
5、散列表
散列表——最有用的基本数据结构之一。
散列表的内部机制:实现、冲突和散列函数。
5.1 散列函数
散列函数是这样的函数,即无论你给它神秘数据,它都还你一个数字。(散列函数“将输入映射到数字”)。
散列函数必须满足:
(1)它必须是一致的。
(2)它应将不同的输入映射到不同的数字。最理想的情况是,将不同的输入映射到不同的数字。例如,如果一个散列函数不管输入是神秘都返回1,它就不是好的散列函数。
散列函数总是将同样的输入映射到相同的索引。
散列函数将不同的输入映射到不同的索引。
散列函数知道数组由多大,只返回有效的索引。如果数组包含五个元素,散列函数就不会返回无效索引100。
结合使用散列函数和数组创建了一种被称为散列表(hash table)的数据结构。数组和链表都被直接映射到内存,但散列表更复杂,它使用散列函数来确定元素的存储位置。
散列表也被称为散列映射、映射、字典和关联数组。
散列表的速度很快!
散列表使用数组来存储数据,因此其获取元素的速度与数组一样快。
Python 提供的散列表实现为字典,你可使用函数 dict 来创建散列表。
散列表适合用于:
(1)仿真映射关系;
(2)防止重复;
(3)缓存/记住数据,以免服务器再通过处理来生成它们。
代码清单5-1 散列表
# -*- coding:UTF-8 -*- # 创建一个空的散列表 book = dict() # 在其中添加一些商品的价格 # 一个苹果的价格为67美分 book["apple"] = 0.67 # 牛奶的价格为1.49美元 book["milk"] = 1.49 book["avocado"] = 1.49 print book # result: {'avocado': 1.49, 'apple': 0.67, 'milk': 1.49} # 鳄梨的价格 print book["avocado"] # result: 1.49
散列表由键和值组成。在前面的散列表 book 中,键为商品名,值为商品价格。散列表将键映射到值。
5.2 将散列表用于查找
手机都内置了方便的电话簿,其中每个姓名都有对应的电话号码。假设你要创建一个类似这样的电话簿,将姓名映射到电话号码。该电话簿提供如下功能:
(1)添加联系人及其电话号码;
(2)通过输入联系人来获取其电话号码。
代码清单5-2 使用散列表用于查找电话号码
# -*- coding:UTF-8 -*- # 新建一个散列表 # phone_book = {} 与 phone_book = dict()等效 phone_book = dict() # 在这个电话簿中添加一些联系人的电话号码 phone_book["jenny"] = 8675309 phone_book["emergency"] =911 # 查找Jenny的电话号码 print phone_book["jenny"]
将网址映射到 IP 地址,这个过程被称为 DNS 解析(DNS resolution),散列表是提供这种功能的方式之一。
5.3 防止重复
如果你将已投票的姓名存储在列表中,这个函数的速度终将变得非常慢,因为它必须使用简单查找搜索整个列表。但这里将它们存储在散列表中,而散列表让你能够迅速知道来投票的人是否投过票。使用散列表来检查是否重复,速度非常快。
代码清单5-3 使用散列表防止重复
# -*- coding:UTF-8 -*- # 创建一个散列表,用于记录已投票的人 voted = {} def check_voter(name): # 有人来投票时,检查他是否在散列表中 if voted.get(name): print "kick them out!" else: voted[name] = True print "let them vote!" check_voter("tom") # result: let them vote! check_voter("mike") # result: let them vote! check_voter("mike") # result: kick them out!
5.4 将散列表用作缓存
缓存的工作原理:网站将数据记住,而不再重新计算。
缓存是一种常用的加速方式,所有大型网站都使用缓存,而缓存的数据则存储在散列表中。
代码清单5-4 请求Facebook的一个URL伪代码
cache = {} if cache.get{url}: # 返回缓存的数据 return cache[url] else: data = get_data_from_server(url) # 先将数据保存到缓存中 cache[url] = data return data
仅当URL不在缓存中时,你才让服务器做些处理,并将处理生成的数据存储到缓存中,再返回它。这样,当下次有人请求该URL时,你就可以直接发送缓存中的数据,而不用再让服务器进行处理了。
5.5 冲突
处理冲突的方式很多,最简单的办法如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表。
最理想的情况是,散列函数将键均匀地映射到散列表的不同位置。
如果散列表存储的链表很长,散列表的速度将急剧下降。然而,如果使用的散列函数很好,这些链表就不会很长。
5.6 性能
散列表的性能:
在平均情况下,散列表执行各种操作的时间都为。被称为常量时间。它并不意味着马上,而是说不管散列表多大,所需的时间都相同。
在使用散列表时,避开最糟糕情况至关重要。为此,需要避免冲突,而要避免冲突,需要有:
(1)较低的填装因子;
(2)良好的散列函数。