A hash table (hash map) is a data structure which implements an associative array abstract data type, a structure that can map keys to values. A hash table uses a hash function to compute an index into an array of buckets or slots, from which the desired value can be found.
散列函数
散列函数是这样的函数,无论你给它什么数据,它都会给你一个数字。用专业的话说,“散列函数将输入映射到数字。”
- 散列函数总是将同样的输入映射到相同的索引。
- 散列函数总是将不同的输入映射到不同的索引。(有问题,暂且先这样理解)
- 散列表知道数组有多大,只返回有效的索引。
任何一门优秀的语言都提供了散列表的实现,python提供的散列表的实现为字典。可以通过dict来创建散列表。像之前leetcode中的Two Sum,最快捷的方法就是使用dict。1
2
3
4#第一种创建方法
hash_table = dict()
#第二种创建方法
hash_table = {}
应用
散列表的用途十分广泛,下面介绍一下它的用途。
将散列表用于查找
散列表常被用于大海捞针式的查找。例如,我们常见的DNS解析(将网址映射到IP地址),散列表就是实现这种功能的方式之一。
散列表用于查找,大致分为两个步骤:
当URL不在缓存中时,才让服务器做处理,并将生成的数据存储到缓存中,再返回它。1
2
3
4
5
6
7
8
9cache = {}
def get_page(url):
if cache.get(url):
return cache[url]
else:
data = get_data_from_server(url)
cache[url] = data
return data
冲突
这里我们没有讨论散列表内部的原理,但是我们依旧需要考虑散列表的性能,我们需要搞清楚什么是冲突。
之前谈到散列函数总是将不同的输入映射到不同的索引,这句话实际上是有问题的。因为这只是最理想的情况,实际上散列函数可能将不同的输入映射到同一个索引。这种情况就是冲突。处理冲突的方法有很多,最简单的办法就是:如果两个键映射到了同一个位置,就在这个位置存储一个链表,但是链表很长依然会影响散列表的性能。
因此,需要记住:
- 散列函数很重要;
- 如果散列函数很好,这些链表就不会很长。
性能
在平均情况下,散列表执行各种操作的时间都为O(1),O(1)被称为常量时间。
在最糟情况下,散列表所有操作的运行时间都为O(n),—线性时间,这就比较慢了。
所以,避开最糟情况很重要,为此我们需要:
- 较低的填装因子
- 良好的散列函数
填装因子
填装因子=散列表包含的元素数/位置总数
填装因子是用来度量散列表中有多少位置是空的。大于1意味着元素数量超过位置数,需要在散列表中添加位置,这被称为调整长度(resizing)。
填装因子越低,发生冲突的可能性越小,散列表的性能越高,一个不错的经验规则是:一旦填装因子大于0.7,就调整散列表的长度。
调整散列表的长度开销很大,所以最好不要频繁这样操作。但平均而言,即便考虑调整长度所需的时间,散列表操作所需的时间仍为O(1)。
良好的散列函数
良好的散列函数,让数组中的值呈均匀分布。糟糕的散列函数让值扎堆,导致大量冲突。
SHA就是一种良好的散列函数,之后将会做详细的介绍。
参考书目:
[^1]: 《算法图解》【美】Aditya Bhargava. 编著