Python 高性能编程第4章 字典和集合

对次序未知的列表/元组的最优查询时间O(logn),字典和集合基于键的查询则可以带给我们O(1)的查询时间。除此之外,和列表/元组一样,字典和集合的插入时间O(1)。为了达到O(1)的查询时间,在底层使用的数据结构是一个开放地址散列表。然而,使用字典和集合有其代价,首先通常会占用更多的内存。同时虽然插入/查询的复杂度是O(1),但实际的速度极大取决于使用的散列函数。如果散列函数的运行速度较慢,那么在字典和集合上进行的任何操作也会相应变慢。

集合保证了它包含的键的唯一性,如果尝试添加一个已有的项,该项不会被添加进集合,这一操作的代价是O(1),而列表进行相应操作的代价是O(n)

 

对于散列表,新插入数据的位置取决于数据的两个属性:键的散列值以及该值如何跟其他对象比较。这是因为当我们插入数据时,首先需要计算键的散列值并掩码来得到一个有效的数据索引。掩码是为了保证一个可能是任意数字的散列值最终能落入分配的桶中。所以,如果我们分配了8个块的内存,而我们的散列值是28957,那么它将落入的桶的索引是28957&0b111 = 7,如果我们的字典增长到了需要512块内存,那么掩码就变成了0b11111111(此时我们会使用28957&0b11111111的桶)。现在我们检查这个桶是否已经被使用,如果是空桶,那么可以将键和值插入这一内存块。我们保存键是为了在获取时确保获得的是正确的值。如果桶已经被使用,且桶内的值和我们希望插入的值相等,说明这一键值对已经被保存,则直接返回。如果值不相等,则找一个新的位置来保存位置。

为了找到新的索引,我们用一个简单的线性函数计算出一个新的索引,这一方法称为嗅探。Pyhon的嗅探机制使用了原始散列值的高位比特(对于之前那个长度为8的散列表,由于使用的掩码mask=0b111=bin(8-1),我们只用了最后3个bit作为初始索引)。使用这些高位比特使得每一个散列值生成的下一可用散列序列都是不同的,这样就能帮助防止未来的碰撞。

当我们在查询某个键时也有一个类似的过程:给出的键会被转化为一个索引进行检索。如果和该索引指向的位置中的键符合(在插入操作时我们会保存原始的键),那么我们就会返回那个值。如果不符合,我们用同一方案继续创建新的索引,直到我们找到数据或找到一个空桶。如果我们找到一个空桶,我们就可以认为表里不存在该数据。

 

当越来越多的项目被插入列表时,表本身必须改变大小来适应。研究显示一个不超过三分之二满的表在具有最佳空间节约的同时,依然具有不错的散列碰撞避免率。因此,当一个表到达关键点时,它就会增长。为了做到这一点,需要分配一个更大的表(也就是在内存中预留更多的桶),将掩码调整为适合新的表,旧表中的所有元素被重新插入新表。这需要重新计算索引,因为改变后的掩码会改变索引计算结果。结果就是,改大散列表的代价非常大!不过因为我们只在表太小时而不是在每一次操作时进行这一操作,分摊后每一次插入的代价依然是O(1)。值得注意的是,当一个散列表变大或变小时都可能发生改变大小。也就是说,如果散列表中足够多的元素被删除,表可能会被改小,但是改变大小仅发生在插入时。

 

 

每当Python访问一个变量、函数或模块时,都有一个体系来决定它去哪里查找这些对象。首先Python查找locals()数组,其内保存了所有本地变量的条目。这是整条链上唯一一个不需要字典查询的部分。如果它不再本地变量里,那么会搜索globals()字典。最后如果对象也不在那里,则会搜索__builtin__对象。要注意locals()和globals()是显式的字典而__builtin__则是模块对象,在搜索__builtin__中的一个属性时,我们其实在搜索它的locals(字典)(对所有的模块对象和类对象都是如此)。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值