python3.6字典的底层原理
在Python 3.6以后,字典的底层数据结构发生了变化,现在当你初始化一个空的字典以后,它在底层是这样的:
my_dict = {}
'''
此时的内存示意图
indices = [None, None, None, None, None, None, None, None]
entries = []
'''
当你初始化一个字典以后,Python单独生成了一个长度为8的一维数组。然后又生成了一个空的二维数组。
现在,我们往字典里面添加一个键值对:
my_dict['name'] = 'jkc'
'''
此时的内存示意图
indices = [None, 0, None, None, None, None, None, None]
entries = [[-5954193068542476671, 指向name的指针, 执行jkc的指针]]
'''
为什么内存会变成这个样子呢?我们来一步一步地看:
在当前运行时,name这个字符串的hash值为-5954193068542476671,这个值对8取余数是1:
>>> hash('name')
-5954193068542476671
>>> hash('name') % 8
1
所以,我们把indices这个一维数组里面,下标为1的位置修改为0。
这里的0是什么意思呢?0是二位数组entries的索引。现在entries里面只有一行,就是我们刚刚添加的这个键值对的三个数据:name的hash值、指向name的指针和指向jkc的指针。所以indices里面填写的数字0,就是刚刚我们插入的这个键值对的数据在二位数组里面的行索引。
好,现在我们再来插入两条数据:
my_dict['address'] = 'xxx'
my_dict['salary'] = 999999
'''
此时的内存示意图
indices = [1, 0, None, None, None, None, 2, None]
entries = [
[-5954193068542476671, 指向name的指针, 执行jkc的指针],
[9043074951938101872, 指向address的指针,指向xxx的指针],
[7324055671294268046, 指向salary的指针, 指向999999的指针]
]
'''
现在如果我要读取数据怎么办呢?假如我要读取salary的值,那么首先计算salary的hash值,以及这个值对8的余数:
>>> hash('salary')
7324055671294268046
>>> hash('salary') % 8
6
那么我就去读indices下标为6的这个值。这个值为2.
然后再去读entries里面,下标为2的这一行的数据,也就是salary对应的数据了。
新的这种方式,当我要插入新的数据的时候,始终只是往entries的后面添加数据,这样就能保证插入的顺序。当我们要遍历字典的Keys和Values的时候,直接遍历entries即可,里面每一行都是有用的数据,不存在跳过的情况,减少了遍历的个数。
老的方式,当二维数组有8行的时候,即使有效数据只有3行,但它占用的内存空间还是 8 * 24 = 192 byte。但使用新的方式,如果只有三行有效数据,那么entries也就只有3行,占用的空间为3 * 24 =72 byte,而indices由于只是一个一维的数组,只占用8 byte,所以一共占用 80 byte。内存占用只有原来的41%。
字典的用法总结
-
1.键必须可散列
-
(1) 数字、字符串、元组,都是可散列的。
-
(2) 自定义对象需要支持下面三点:
- ①支持 hash()函数
- ②支持通过__eq__()方法检测相等性。
- ③若 a==b 为真,则
hash(a)==hash(b)也为真。
-
-
2.字典在内存中开销巨大,典型的空间换时间。
-
3.键查询速度很快
-
4.往字典里面添加新建可能导致扩容,导致散列表中键的次序变化。因此,不要在遍历字 典的同时进行字典的修改。
如果遇到碰撞:
ython 字典处理哈希碰撞主要使用开放定址法(Open Addressing),具体来说是采用了一种叫做探查(Probing)的策略。以下是 Python 字典处理哈希碰撞的详细原理和方法。
开放定址法(Open Addressing)
在开放定址法中,当发生哈希碰撞时,不是将冲突的键值对存储在链表或其他结构中,而是通过某种探查策略寻找哈希表中其他空闲的位置来存储。
1. 线性探查(Linear Probing)
一种简单的探查方法是线性探查,当发生碰撞时,从冲突位置开始,依次检查后续位置,直到找到一个空闲位置。
2. 二次探查(Quadratic Probing)
线性探查可能会导致“主堆积”(Primary Clustering)问题,即冲突的元素集中在一起。二次探查通过更改步长(通常是二次方)来减少这种问题。
3. 双重散列(Double Hashing)
双重散列通过两个独立的哈希函数来计算位置,如果第一个哈希函数发生碰撞,就使用第二个哈希函数来计算新的步长。
Python 字典的哈希碰撞解决方法
Python 字典实现了一种称为二次探查(Quadratic Probing)的探查策略。具体步骤如下:
- 计算初始位置: 使用哈希函数计算键的初始哈希位置。
- 检查冲突: 如果计算出的初始位置已经有其他键存在(即发生碰撞),则计算新的探查位置。
- 二次探查: 通过一个增量函数计算下一个探查位置。增量函数通常是与探查次数相关的二次方函数,以减少堆积问题。
- 插入键值对: 找到空闲位置后,将新的键值对插入到该位置。
当然还有一些其他的优化方法: 在 Python 的实际实现中(CPython),字典不仅使用二次探查,还进行了许多优化以提高性能和减少内存消耗。以下是一些关键点:
- 预分配和动态扩展: 字典会预分配一定数量的存储桶,并根据需要动态扩展,以保持低装载因子,减少碰撞的可能性。
- 小表优化: 对于小字典,Python 使用一种优化方案,以减少内存消耗和提高访问速度。
- 缓存优化: Python 字典会缓存最近使用的条目,以提高访问性能。
通过这些方法,Python 字典在大多数情况下能够提供高效、快速的键值对存储和访问。