算法图解第五章笔记与习题(散列表)

算法图解第五章笔记与习题(散列表)



5.1 散列函数

散列函数是这样的函数,即无论你给它什么数据,它都还你一个数字。即:将任意输出映射为数字。同时,散列函数同时遵循着一些规则:

  • 它必须是一致的。例如,假设你输入apple时得到的是4,那么每次输入apple时,得到的都必须为4。如果不是这样,散列表将毫无用处。
  • 它应将不同的输入映射到不同的数字。例如,如果一个散列函数不管输入是什么都返回1, 它就不是好的散列函数。最理想的情况是,将不同的输入映射到不同的数字。
  • 另外,散列函数可以知道数组的大小,并返回有效的索引。如果数组包含5个元素,散列函数就不会返回无效索引100。

因此,散列函数可以用来与数组结合,形成散列表(hash table),也称哈希表,其特点是可以利用散列函数来确定元素的存储位置。同时,它也是一种包含额外逻辑数据结构。

在python中提供字典(dictionary)作为散列表的实现。

>>> book = dict()
>>> book["apple"] = 0.67  # 一个苹果的价格是67美分
>>> book["milk"] = 1.49    # 牛奶的价格为1.49美元
>>> book["avocado"] = 1.60
>>> print(book)
{'apple': 0.67, 'milk': 1.49, 'avocado': 1.6}
>>> print(book["milk"])
1.49   # 牛奶的价格

我们把散列表的输入输出称为键-值对(key-value)


5.2 应用场景

这里总结一下,散列表适合用于:

  • 模拟映射关系;
  • 防止重复;
  • 缓存/记住数据,以免服务器再通过处理来生成它们。

5.3 冲突

上面提到,散列函数总是将不同的键映射为不同的值,但实际上不是的。

当散列函数选的十分简单,如以键的首字母来分配数组位置时。如果有两个输入:appleavocado,对于这个散列函数而言,他们的值是相同的,则他们在数组中的位置也将相同!

这种情况被称作冲突(collision)。在这种情况下,后被写入的键将覆盖前一个。则以后查询apple时,将得到的是avocado的值。

对于冲突的情况,最简单的解决方法是在冲突的位置存储一个链表,拓展这个散列表。

但当冲突的输入增多,链表增长后,散列表的速度将会变得很慢。

因此,散列函数很重要。理想的情况下,散列函数将输入均匀地映射到散列表的不同位置。


5.4 性能

如果在散列表的冲突的位置,用链表进行缓解。在最糟情况下,散列表的所有操作的运行时间均为 O ( n ) O(n)

散列表平均情况 散列表最糟情况 数组 链表
查找 O ( 1 ) O(1) O ( n ) O(n) O ( 1 ) O(1) O ( n ) O(n)
插入 O ( 1 ) O(1) O ( n ) O(n) O ( n ) O(n) O ( 1 ) O(1)
删除 O ( 1 ) O(1) O ( n ) O(n) O ( n ) O(n) O ( 1 ) O(1)

(疑问——为什么散列表最糟情况的插入和删除的时间也是 O ( n ) O(n) ?)

由上表可见,在最糟情况下,散列表的各种操作的速度都很慢。因此,在使用散列表时,避开最糟情况至关重要。为此,需要避免冲突。而要避免冲突,需要有:

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

5.4.1 填装因子

散列表的填装因子的计算方式为:
{\frac {散列表包含的元素数}{位置总数}}

如,当一个散列表的数组长度为5,有3个位置已有元素时,其填装因子为0.6。

当散列表中的元素超出数组长度,填充因子大于1时。就需要为散列表中增加位置,称为调整长度(resizing)

调整长度时,首先创建一个新数组(通常将数组增长一倍),接下来通过散列函数(hash 函数)重新为各个键重新分配在新数组中的位置。

调整长度过后,填装因子将降低。填装因子越低,发生冲突的可能性越小,散列表的性能就越高。经验规则为:一旦填装因子大于0.7,就调整散列表的长度。

调整长度需要很长的时间,但平均而言,即便考虑到调整长度所需的时间,散列表操作所需的时间也为 O ( 1 ) O(1)


5.4.2 良好的散列函数

良好的散列函数使数组中的值呈均匀分布。

通常我们不需要担心如何选择散列函数,但在本书最后一章中,有对SHA函数的介绍。可以用它作为散列函数。


5.5 小结

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

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

练习

习题5.1-5.4题干:

对于同样的输入,散列表必须返回同样的输出,这一点很重要。如果不是这样的,就无法找到你在散列表中添加的元素! 请问下面哪些散列函数是一致的?

习题5.1

  • f(x) = 1​ ←(无论输入是什么,都返回1)

一致,对于同样的输入,散列表返回相同的值1。


习题5.2

  • f(x) = rand() ← (每次都返回一个随机数)

不一致,对于同样的输入,散列表可能返回不同的随机数。


习题5.3

  • f(x) = next_empty_slot() ← (返回散列表中下一个空位置的索引)

不一致,对于相同的输入,散列表返回下一个空位置的索引,而下一个空位置的索引随时可能变化。


习题5.4

  • f(x) = len(x) ← (将字符串的长度用作索引)

一致,对于相同的输入,散列表返回的始终是该输入字符串的长度。


习题5.5-5.7题干:

散列函数的结果必须是均匀分布的,这很重要。它们的映射范围必须尽可能大。最糟糕的散列函数莫过于将所有输入都映射到散列表的同一个位置。

假设你有四个处理字符串的散列函数。

A. 不管输入是什么,都返回1。

B. 将字符串的长度用作索引。

C. 将字符串的第一个字符用作索引。即将所有以a打头的字符串都映射到散列表的同一个位置,以此类推。

D. 将每个字符都映射到一个素数:a = 2,b = 3,c = 5,d = 7,e = 11,等等。对于给定的字符串,这个散列函数将其中每个字符对应的素数相加,再计算结果除以散列表长度的余数。例如,如果散列表的长度为10,字符串为bag,则索引为(3 + 2 + 17) % 10 = 22 % 10 = 2

在下面的每个示例中,上述哪个散列函数可实现均匀分布?假设散列表的长度为10。

习题5.5

  • 将姓名和电话号码分别作为键和值的电话簿,其中联系人姓名为Esther、Ben、Bob和Dan。

只有散列函数D可以实现均匀分布。


习题5.6

  • 电池尺寸到功率的映射,其中电池尺寸为A、AA、AAA和AAAA。

散列函数B、D均可实现均匀分布。


习题5.7

  • 书名到作者的映射,其中书名分别为MausFun HomeWatchmen

散列函数C、D均可实现均匀分布。


:书本后的练习答案似乎认为有两个键冲突也算是均匀分布,它提供的解答分别为CD,BD,BCD。如果有人可以告诉我为什么是这样的,希望可以留言或私信与我交流。谢谢。


猜你喜欢

转载自blog.csdn.net/hwl19951007/article/details/88627144