4-26日读书笔记——流畅的python

3.9.2 3.9.2 3.9.2 字典中的散列表

上一章最后提到了关于散列表的知识,并提到了散列表如何记录元素信息。那么python其实会设法保证有大概1/3的元素空间是空的,因此在快要达到这一饱和度的时候,原来的散列表便会被复制到一个更大的空间中去。

然而,我们在将对象放入散列表时,首先就要计算出元素键的散列值,python中用到了hash()来做这件事。

  1. 散列值和相等性

hash()方法可以运用在所有的内置的类型对象。如果自定义对象调用hash()的话,实际上运行了自定义的__hash__。我们在比较两个数时,如果是相等的,那么它们的散列值就是相等的,但是内部结构并不一定相同。

>>> hash(1) == hash(1.0)
True

2.散列表算法

为了获取my_dict[key]的值,python首先会调用hash(key)方法来得到key的散列值,所以key必须是可散列的。这样通过hash()方法所得到的散列值就是不变的。
然后根据散列值在散列表里查找元素空间,如果该空间为空,就抛出keyerror异常,否则,该空间里会存在一对键值对。就检验key与该键值对中的key是否相等,如果相等,就返回查找到的value。

实际上,散列表是把随机的元素映射到几位数字上,因此,当我们查找到的key与我们的关键字key并不想同时,我们把这样的情况叫做散列冲突。

当冲突发生时,有两类处理冲突的方法:

  1. 开放寻址法
  2. 链表法

先说链表法吧。
链表法也叫链地址法,是比较直接的一种方式用来处理冲突。
将散列到同一个表元中的元素放在同一个链表中。
我用表格来模拟一下:

0 1 2 3 4 5 6 7 8 9
/ / / / / / / / / /

假设散列表的长度为10,则可将散列表定义成一个由10个头指针组成的指针数组[0-9]。将散列地址%10,结果为i的结点,则放入到相对应为i的链表中。

如果现在这组关键字为(14,27,9,11,38,15,48,37,7),则会对应的放在下图所示的链表中,没有对应地址元素的则为空指针。

0 1 2 3 4 5 6 7 8 9
/ / / / /
链表i 元素
1 11
5 15
7 27 ——>37 ——> 7
8 38 ——>48
9 9

此时,已经不存在什么冲突换址的问题,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题。


开放寻址法分为好几种,包括:线性探查、二次探查、双重散列、随机散列。

关于具体的算法的不同 可以到这个blog看一下,讲的挺全,也比较具体。算法导论-散列表(Hash Table)-大量数据快速查找算法

但其实所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
下图是一个实例:

可以看到,当轮到36时,由于26已经将第6个位置填住了,因此往后置位挪了一个。以此类推,直到将所有的槽都填满。而多种方式无非是当冲突发生时,偏移位置的计算方式有所不同。

然而从这个例子我们也可以看到,在解决冲突的时候,还会碰到如48和37这种本来都不是同义词却需要争夺同一个空间地址的情况,我们称这种现象为堆积。很显然,堆积的出现,使得我们需要不断处理冲突,无论是存入还是査找效率都会大大降低。

3.9.3 3.9.3 3.9.3 dict的实现及其导致的结果
使用散列表给dict带来了很多优势,可同时也会有很多限制。
首先 键必须是可散列的

一个可散列的对象需要满足以几下个条件:
(1)支持hash()函数,并且通过__hash__()方法所得到的散列值是不变的
(2)支持通过__eq__()方法来检测相等性。
(3)若a == b为真,则hash(a) == hash(b)也为真。

如果我们实现了一个类的__eq__方法,并且希望它是可散列的,那么一定要有一个恰当的__hash__方法,保证在a==b的时候hash(a) == hash(b)也为真。否则就会导致由这些对象所组成的字典和集合完全失去可靠性。

当我们需要存放大量数据时,放在由元组构成的列表中会是比较好的选择。因为首先可以节省由散列表而耗费的很多空间,另外并不需要把记录中字段的名字在每个元素里都存一次。但是字典查询的速度是真的快很多。

键的次序取决于添加顺序
先来一个小示例:

>>> STUDENTS_LIST = [(11,'ming'),(7,'yi'),(29,'mei'),(13,'ma')]       
>>> d1 = dict(STUDENTS_LIST)
>>> print('d1:', d1.keys())
d1: dict_keys([11, 7, 29, 13])
>>> d2 = dict(sorted(STUDENTS_LIST))
>>> print('d2:', d2.keys())
d2: dict_keys([7, 11, 13, 29])
>>> d3 = dict(sorted(STUDENTS_LIST, key=lambda x:x[1]))
>>> print('d3:', d3.keys())
d3: dict_keys([13, 29, 11, 7])

>>> d1 == d2 == d3
True

通过检验我们可以得知这3个字典是相等的,因为它们包含着同样的键值对数据。
但是它们每次的输出却是不相同的。
也就是说,当我们往dict里添加新键而又发生了散列冲突时,新键会被安排存放在另一个位置。于是就会得到两个两个字典,但是它们又是相等的。

当我们向字典中添加新键的时候,很有可能会改变原有的键的顺序。因为当我们添加新键时,会导致python解释器可能会扩容该字典,以至于会用到新的散列表以记录这些键值对,在这个过程中很有可能会导致散列冲突。一旦我们在迭代时对字典进行了一些修改,那么很有可能就会跳过一些键。所以这个操作最好分成两部分来操作哦哦。

另外,set的实现也依赖散列表,但是就好像dict只存放“key”(元素的引用)一样。

在set加入到python之前,都是把字典加上无意义的值来当作集合用。

你可能感兴趣的:(4-26日读书笔记——流畅的python)