第7章 散列表
大多数编程语言都自带散列表,也叫做散列、映射、散列映射、字典、关联数组。
A=1
B=2
C=3
D=4
E=5
由此可得,ACE会转化为135, CAB会转化为312, DAB会转化为412, BAD会转化为214。将字符串转为数字串的过程就是散列,其中用于对照的密码,就是散列函数。
一个散列函数需满足以下条件才有效:每次对同一字符串调用该散列函数,返回的都应是同一数字串。如果每次都返回不一样的结果,那就无效。
现在要实现同义词字典的效果:
散列表可以看成是一行能够存储数据的格子,就像数组那样。每个格子都有对应的编号,如下所示。
现在往散列表里加入我们的第一条同义词。
thesaurus["bad"]="evil"
散列表变成了下面这样。
{"bad"=> "evil"}
再看看散列表是如何存储数据的。首先,计算机用散列函数对键进行计算。为了方便演示,这里我们依然使用之前提及的那个乘法函数。
"bad"的散列值为8,于是计算机将"evil"放到第8个格子里。
收到命令后,计算机就会进行如下两步简单的操作。
(1) 计算这个键的散列值:BAD=2×1×4=8。
(2) 由于结果是8,因此去到第8格并返回其中的值。在本例中,该值为"evil"。这下你应该明白为什么从散列表里读取数据只需要 ==O(1)==了吧,因为其过程所花的时间是恒定的。它总是先计算出键的散列值,然后根据散列值跳到对应的格子去。
处理冲突
不过,散列表也会带来一些麻烦。继续同义词典的例子:把下面这条同义词也加到表里,会发生什么呢?
thesaurus["dab"]="pat"
首先,计算散列值。
DAB=4 * 1 * 2=8
然后,将"pat"放进第8个格子。
往已被占用的格子里放东西,会造成冲突。幸好,我们有解决办法。一种经典的做法就是分离链接。当冲突发生时,我们不是将值放到格子里,而是放到该格子所关联的数组里。
因为要放入"pat"的第8格,已经存在"evil"了,于是我们将第8格的内容换成一个数组。该数组又以子数组构成,每个子数组含两个元素,第一个是被检索的词,后一个是其相应的同义词。
下面运行一遍"dab"的查找过程,执行:
thesaurus["dab"]
计算机就会按如下步骤执行。
(1) 计算散列值DAB=4×1×2=8。
(2) 读取第8格,发现其中不是一个单独的值,而是一个数组。
(3) 于是线性地在该数组中查找,检查每个子数组的索引0位置,如果碰到要找的词(“dab”),就返回该子数组的索引1的值。
如果数据都刚好存在同一个格子里,那么查找就相当于在数组上进行。因此散列表的最坏情况就是O(N)。
为了避免这种情况,散列表的设计应该尽量减少冲突,以便查找都能以==O(1)==完成。
找到平衡
使用散列表时所需要权衡的:既要避免冲突,又要节约空间。
要想解决这个问题,可参考计算机科学家研究出的黄金法则:每增加7个元素,就增加10个格子。