字典和集合的效率高,和他背后的散列表是绕不开的。
散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)。散列表的单元叫做表元bucket。在dict的散列表中,每个键值对都占用一个表元,每个表元都有两个部分,一个是对键的引用,一个是对于值的引用。因为表元的大小一致,所以可以通过偏移量来读取表元。python会保证大概有三分之一的表元是空的,快要达到这个阈值的时候,原有的散列表会被复制到一个更大的空间里面。
散列值的相等:
内置的hash()方法是调用对象的__hash__。如果两个对象比较的时候是相等的,那么他的散列值必须相等吗,也就是他们的__hash__相等。
比如如果1 == 1.0 为真,那么hash(1) == hash(1.0) 也要为真。
散列表的算法:
为了获取一个字典的值dict[key],首先python会调用hash(key)来计算key的散列值,把这个值的最后几位数字(具体多少位,要看散列表的大小)当做偏移量,在散列表中查找表元。若插到的表元是空的,则会抛出KeyError异常。若不为空,则表元里面会有一对found_key:found_value。这时候python校验key==found_key是否为真,如果相等的话,就会返回found_value。 字典的查找就结束了。
以上有一种情况是key和found_key不匹配的时候,这种叫做【散列冲突】,发生这种情况的原因是散列表把随机的元素映射到了几位的数字上,而散列表本身的索引有只依赖于这个数字的一部分。为了解决散列冲突,算法会在散列值中另外再多取几位,然后处理一下,用新得到的数字当做索引再次寻找表元,重复以上步骤:如果得到是空的抛出KeyError异常,如果匹配则返回值,吐过又发生了散列冲突,重复本步骤。。
以上是字典取值的算法,新增值,修改值,也是几乎一样的操作。
dict的实现和结果
- 键必须是可散列的
一个可散列对象必须满足:支持hash()函数,通过__hash__()得到的散列值是不变的,支持__eq__()方法来测试相等性,若a==b为真,则hash(a)==hash(b)也为真
所有用户自定义的对象默认都是可散列的,因为他们的散列值是由id()获取的,而且他们都是不相等的。
- 字典的内存开销巨大
由于字典是使用了散列表,散列表又必须是稀疏数组,这就是导致了在空间的效率低下,dict是典型的空间换时间。如果想存储大量的数据,元祖是更好的选择。
- 键查询很快
字典类型有着巨大的内存开销,也就无视数据量的快速访问。数据量大对字典的查询速度影响很小。
- 键的次序取决于添加的顺序
当往dict添加新键又发生了散列冲突的时候,新建可能会被安排到另一个位置。这就产生了不同的添加次序,产生的字典顺序很可能不一样。
- 新增键可能改变已有的顺序
无论何时在字典添加新建,python都可能做出为字典扩容的决定。扩容就会新增一个更大的散列表,把已有的元素放到新的散列表中,这个过程就可能产生新的散列冲突,导致新的字典顺序变化。
set的实现和结果
set和frozenset的实现也一样依赖散列表,但是散列表中指存放了元素的引用(就像字典中一样只放了键,没有放值)
set的特点和dict一样,也是有上面的1到5点。