算法图解笔记——Chapter 5 Hash Table
Author: Seven Zou
Email: zoushiqi0404@gmail.com
Language: Python2.7
5 散列表
回顾:前面几节学到数组、链表和栈(栈不能用于查找),还学到了简单查找、二分查找查找类算法,其运行时间分别为
O
(
n
)
O(n)
O(n)、
O
(
l
o
g
n
)
O(logn)
O(logn)。
假定存在一种以名称为输入,对应数字结果为输出的场景,则针对简单查找有
O
(
n
)
O(n)
O(n),二分查找有
O
(
l
o
g
n
)
O(logn)
O(logn)。
而存在一种运行时间为
O
(
1
)
O(1)
O(1)的查找数据结构,即引入以下概念"散列函数"。
5.1 散列函数
如上述场景,散列函数就是将输入映射到数字的一种函数,且需满足一定需求:
- 它必须是一致的。e.g.假设输入apple输出为4,那么每次输入apple时,得到必须是4。
- 它应不同输入映射到不同的数字。
设存在以下数组,且已知每个Value对应的Label。通过散列函数可在输入Label时,通过找到其对应的Index进而获取其对应Value。
Index | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
Value | value1 | value2 | value3 | value4 | value5 |
结合使用散列函数和数组创建为散列表(Hash Table)的数据结构,其包含额外逻辑的数据结构。数组和链表都直接映射到内存,而散列表
通过使用散列函数来确定元素的存储位置。
Remark: 散列表适用于模拟映射关系;散列表适用于防止重复。
# -*- coding: utf-8 -*-
book = dict() # 散列表,在python中为字典,直接调用dict()
book["apple"] = 0.67 # 一个苹果的价格为67美分
book["milk"] = 1.49 # 牛奶的价格为1.49美元
book["avocado"] = 1.49
print book
print book["avocado"] # 鳄梨的价格
Output: {'avocado': 1.49, 'apple': 0.67, 'milk': 1.49}
1.49
5.2 Case
- Case 1 电话簿
1.添加联系人及电话号码;2.通过输入联系人就可获取对应号码。
# -*- coding: utf-8 -*-
phone_book = dict() # 散列表
# phone_book = {} # 与前面等效
phone_book["jenny"] = 8675309
phone_book["emergency"] = 911
print phone_book["jenny"]
Output: 8675309
- Case 2 投票箱
防止已投过票的人二次投票,需要检索人名进行确认。
# -*- coding: utf-8 -*-
def check_voter(name):
if voted.get(name): # 检查name在表中,返回let them vote!,否则返回kick them out!
print "kick them out!"
else:
voted[name] = True
print "let them vote!"
check_voter("tom") # tom第一次投票
check_voter("mike") # mike第一次投票
check_voter("mike") # mike第二次投票会被拒绝
Output: let them vote!
let them vote!
kick them out!
- Case 3 作为缓存
缓存的工作原理在于:网站将数据记住,而不再重新计算。优点在于:1.用户能够更快地看到网站;2.运营商需要做的工作更少。而缓存是一种常用的加速方式,所以大型网站都使用缓存,而缓存的数据则存储在散列表中!一个网站页面不止包含主页,还包含诸多子页。因此,它需要将页面URL映射到页面数据。
# -*- coding: utf-8 -*-
cache = {}
def get_page(url):
if cache.get(url):
return cache[url] # 返回缓存的数据
else:
data = get_data_from_server(url)
cache[url] = data # 先将数据保存到缓存中
return data
5.3 冲突与性能
如果在根据Label通过Index寻找对应的Value时,发生了新旧值覆盖问题,那么散列表就发生了冲突。这种情况极有可能发生,主要源于散列函数不能将所以的Label均匀地映射到散列表的不同位置。但也存在Solution,可以在冲突的位置建立一个链表,这样就可以纵向检索,但如果链表过长,会导致速度极大下降。所以选择合适的散列函数是及其重要的,这里就不免提到性能。散列表所有的操作的运行时间都是 O ( n ) O(n) O(n)线性时间。所以为了避免最糟情况(冲突),较低的填装因子、良好的散列函数是必不可少的。
操作 | 散列表(平均情况) | 散列表(最糟情况) | 数组 | 链表 |
---|---|---|---|---|
查找 | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) |
插入 | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
删除 | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
_Note:_填装因子计算公式如下,其度量的是散列表中有多少位置是空的。(因子超过0.7,就该调整散列表的长度)
填
装
因
子
=
散
列
表
包
含
的
元
素
数
/
位
置
总
数
填装因子=散列表包含的元素数/位置总数
填装因子=散列表包含的元素数/位置总数
Reference
[美]Aditya Bhargava/袁国忠, 算法图解, 北京:人民邮电出版社, 2017.3.
附个人Github地址: https://github.com/shiqi0404/Algorithm_Diagram,其中包括笔记、Code还有书本pdf版。