散列表

散列表


散列表主要由 散列函数数组 组成,其中:

  1. 散列函数: 主要是用来产生数据索引。其输入与输出是一一对应的;它知道数组有多大,只返回有效的索引;
  2. 数组: 主要是用来存储数据。

例如,用散列表来存储水果的价格:

在这里插入图片描述

Python 中提供的散列表实现是 字典,例如:利用函数 dict 来创建散列表。

book = dict()
book['apple'] = 0.67 # 苹果的价格为 67美分
book['milk'] = 1.49 # 牛奶的价格为 1.49 美元
book['avocado'] = 1.49 # 雪梨的价格

print(book['avocado']) # 查询雪梨的价格

散列表的查询操作时间复杂度为: O ( 1 ) O(1) O(1)


散列表作为映射


散列表可以用于电话簿,将姓名与电话号码进行映射。例如:

phone_book = dict() # 或者 phone_book = {}
phone_book['jenny'] = 8675309
phone_book['emergency'] = 911
phone_book['SOS'] = 110

散列表用来排查重复项


假设你负责管理一个投票站。显然,每人只能投一票,但如何避免重复投票呢?有人来投票时,你询问他的全名,并将其与已投票者名单进行比对。
如果名字在名单中,就说明这个人投过票了,因此将他拒之门外!否则,就将他的姓名加入到名单中,并让他投票。现在假设有很多人来投过了票,因此名单非常长。
每次有人来投票时,你都得浏览这个长长的名单,以确定他是否投过票。但有一种更好的办法,那就是使用散列表!

voted = {} # 记录已经投票的人

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

check_voter("tom") # let them vote!
check_voter("mike") # let them vote!
check_voter("mike") # kick them out!

如果你将已投票者的姓名存储在列表中,这个函数的速度终将变得非常慢,因为它必须使用简单查找搜索整个列表。但这里将它们存储在了散列表中,而散列表让你能够迅速知道来投票的人是否投过票。使用散列表来检查是否重复,速度非常快。


将散列表用作缓存


用于存储已经访问或者保存的网页数据。

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时,你就可以直接发送缓存中的数据,而不用再让服务器进行处理了。


散列表的冲突


散列表的冲突是指:不同的输入映射到相同的索引。例如,有下面这种场景:假设你有一个数组,它包含26个位置,对应26个英文字母:

在这里插入图片描述

  1. 当你输入 APLLE 的时候会映射到索引为 0 0 0 的位置;
  2. 当你输入 BANANA 的时候会映射到索引为 1 1 1 的位置;
  3. 当你输入 AROCADDS 的时候又会映射到索引为 0 0 0 的位置;这种情况就称之为 散列表的冲突

如何解决散列表的冲突呢?处理冲突的方式很多,最简单的办法如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表。如下所示:

在这里插入图片描述

如果这个链表很短,也没什么大不了——只需搜索三四个元素。如果,这个链表很长,如下所示,效率就不怎么高了。

在这里插入图片描述

这个散列表中的所有元素都在这个链表中,这与一开始就将所有元素存储到一个链表中一样糟糕:散列表的速度会很慢。
由此,我们能够得到两个非常重要的经验:

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

散列表的性能


理想情况下,散列表的性能是 O ( 1 ) O(1) O(1);在最糟糕的情况下(所有的数据在一个链表中),散列表的性能为 O ( n ) O(n) O(n)。散列表与数组和链表的性能对比如下:

最理想的情况下,我们希望散列表的性能处于平均情况。因此散列表必须满足如下两个因素:

  1. 较低的填装因子;
  2. 良好的散列函数;

填装因子


填装因子的计算公式如下:
填 装 因 子 = 散 列 表 包 含 的 元 素 数 位 置 总 数 填装因子 = \frac{散列表包含的元素数}{位置总数} =
填装因子越低,发生冲突的可能性越小,散列表的性能越高。一个不错的经验规则是:一旦填装因子大于 0.7 0.7 0.7,就调整散列表的长度。调整长度的开销很大,因此你不会希望频繁地这样做。但平均而言,即便考虑到调整长度所需的时间,散列表操作所需的时间也为O(1)。


良好的散列函数


良好的散列函数让数组中的值呈均匀分布;糟糕的散列函数让值扎堆,导致大量的冲突。
什么样的散列函数是良好的呢?一般用 SHA 函数用作散列函数。


总结


散列表是一种功能强大的数据结构,其操作速度快。Python 中的散列函数实现为字典。

  1. 散列表由散列函数和数组组成;
  2. 散列表的查找、插入和删除速度都为 O ( 1 ) O(1) O(1)
  3. 填装因子一般 < = 0.7 <= 0.7 <=0.7
  4. 散列函数一般用 SHA 函数实现;
  5. 散列表可用于映射、放重复、缓存数据;

参考


  1. 《算法新解》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值