文章目录
- Python进阶系列---(2)字典与集合工作原理详解
- 一、字典与集合基础
- 1、访问方式
- 字典
- 集合
- 2、增删改查
- 3、排序
- 二、工作原理
- 1、Python3.6之前字典实现方式
- 插入操作
- 查找操作
- 删除操作
- 2、Python3.7+后实现方式
- 三、哈希冲突解决方法
- 1、开放寻址法
- 2、拉链法
- 四、几点说明
- 五、Reference
Python进阶系列—(2)字典与集合工作原理详解
一、字典与集合基础
字典
:一系列由键(key)和值(value)配对组成的元素的集合。
注:自Python3.7+,字典被确定为有序。
集合
:一系列无序的、唯一的元素集合。
字典与集合常见创建方式:
d1 = {'name': 'jason', 'age': 20, 'gender': 'male'}
d2 = dict({'name': 'jason', 'age': 20, 'gender': 'male'})
d3 = dict([('name', 'jason'), ('age', 20), ('gender', 'male')])
d4 = dict(name='jason', age=20, gender='male')
d1 == d2 == d3 ==d4
True
s1 = {1, 2, 3}
s2 = set([1, 2, 3])
s1 == s2
True
1、访问方式
字典
1、直接索引键:
d['name']
2、使用get(key,default)函数索引,default为默认的返回值:
d.get('name','null)
集合
集合并不支持索引操作,因其本质是一个哈希表。
判断元素是否在集合或字典内,可用:
value in dict/set
s = {1, 2, 3}
1 in s
True
10 in s
False
d = {'name': 'jason', 'age': 20}
'name' in d
True
'location' in d
False
2、增删改查
d = {'name': 'jason', 'age': 20}
d['gender'] = 'male' # 增加元素对'gender': 'male'
d['dob'] = '1999-02-01' # 增加元素对'dob': '1999-02-01'
d
{'name': 'jason', 'age': 20, 'gender': 'male', 'dob': '1999-02-01'}
d['dob'] = '1998-01-01' # 更新键'dob'对应的值
d.pop('dob') # 删除键为'dob'的元素对
'1998-01-01'
d
{'name': 'jason', 'age': 20, 'gender': 'male'}
s = {1, 2, 3}
s.add(4) # 增加元素4到集合
s
{1, 2, 3, 4}
s.remove(4) # 从集合中删除元素4
s
{1, 2, 3}
注:集合POP()是删除集合中最后一个元素,而其本身无序,所以此操作慎用。
3、排序
依据键/值排序:
d = {'b': 1, 'a': 2, 'c': 10}
d_sorted_by_key = sorted(d.items(), key=lambda x: x[0]) # 根据字典键的升序排序
d_sorted_by_value = sorted(d.items(), key=lambda x: x[1]) # 根据字典值的升序排序
d_sorted_by_key
[('a', 2), ('b', 1), ('c', 10)]
d_sorted_by_value
[('b', 1), ('a', 2), ('c', 10)]
s={3,4,1,2,5}
sorted(s)
[1,2,3,4]
二、工作原理
1、Python3.6之前字典实现方式
哈希表存储了哈希值(hash),键和值三个元素。
entries = [
['--', '--', '--'],
[hash, key, value],
['--', '--', '--'],
['--', '--', '--'],
[hash, key, value],
]
具体示例:
# 给字典添加一个值,key为hello,value为word
my_dict['hello'] = 'word'
# 假设是一个空列表,hash表初始如下
entries = [
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
]
hash_value = hash('hello') # 假设值为 12343543 注:以下计算值不等于实际值,仅为演示使用
index = hash_value & ( len(entries) - 1) # 假设index值计算后等于3,具体的hash算法本文不做介绍
# 下面会将值存在entries中
entries = [
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
[12343543, 'hello', 'word'], # index=3
['--', '--', '--'],
]
# 我们继续向字典中添加值
my_dict['color'] = 'green'
hash_value = hash('color') # 假设值为 同样为12343543
index = hash_value & ( len(entries) - 1) # 假设index值计算后同样等于3
# 下面会将值存在entries中
entries = [
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
[12343543, 'hello', 'word'], # 由于index=3的位置已经被占用,且key不一样,所以判定为hash冲突,继续向下寻找
[12343543, 'color', 'green'], # 找到空余位置,则保存
]
插入操作
1、计算key的hash值:hash(key)
2、与mask做与操作【mask=PyDicMinSize(字典最小长度)-1)】:
hask(key)&mask
得到一个数字index,index为插入哈希表的位置。
3、判断:
- 如果index对应位置为空,则元素被插入其中。
- 如果index对应位置被占用,则比较key:
- 若key相等,表示元素已存在,若值不同,则更新value的值
- 若key不等,则表示hask冲突(key不同而key的哈希值相同),则继续向下寻找空位,直到找到空余位置。
随着哈希表的扩张,它会变得越来越稀疏。同时可以发现,不同的key计算的出的index值是不一样的,在entries中插入的位置也不一样,所以当我们遍历字典的时候,字段的顺序与我们插入的顺序是不相同的(即无序)。
查找操作
1、根据哈希值,找到其应该处于的位置;
2、比较哈希表这个位置中元素的哈希值和键,与需要查找的元素是否相等。
- 相等,则直接返回;
- 如果不等,则继续查找,直到找到空位或者抛出异常为止。
删除操作
对于删除操作,Python 会暂时对这个位置的元素,赋于一个特殊的值,等到重新调整哈希表的大小时,再将其删除。
2、Python3.7+后实现方式
从上一小节可以看出,老版本的设计结构会浪费存储空间,为了提高存储空间的利用效率,Python3.7+后除使用一张hash表外,还使用了一张Indices表:
Indices
----------------------------------------------------
None | index | None | None | index | None | index ...
----------------------------------------------------
Entries
--------------------
hash0 key0 value0
---------------------
hash1 key1 value1
---------------------
hash2 key2 value2
---------------------
...
---------------------
具体示例:
# 下面是一个字典与字典的存储
more_dict = {'name': '张三', 'sex': '男', 'age': 10, 'birth': '2019-01-01'}
# 数据实际存储
indices = [None, 2, None, 0, None, None, 1, None, 3]
entries = [
[34353243, 'name', '张三'],
[34354545, 'sex', '男'],
[23343199, 'age', 10],
[00956542, 'birth', '2019-01-01'],
]
print(more_dict['age']) # 当我们执行这句时
hash_value = hash('age') # 假设值为 23343199
index = hash_value & ( len(indices) - 1) # index = 1
entey_index = indices[1] # 数据在entries的位置是2
value = entries[entey_index] # 所以找到值为 entries[2]
与老版本的字典不同的是,新字典操作得到的index是indices中下标的位置,此下标位置存储的不是hash值,而是len(entries),表示该值在entries中的位置,如果出现hask冲突,则处理方式同老字典处理方式。
可以发现,现在的存储位置由indices来维护,空间利用率得到了很大的提高,而且数据存放是有序的。
三、哈希冲突解决方法
1、开放寻址法
即之前提到的,hash冲突后,继续线性查找,指导找到下一个空闲的位置。
(图源网络,侵删)
2、拉链法
将发生hash冲突的元素放到同一位置,然后通过“指针“来串联起来。
(图源网络,侵删)
四、几点说明
1、为了防止Hash冲突,Python字典的哈希算法,会尽量保证哈希值计算出的index是平均分布且每一个值之间有剩余位置,保证发生Hash冲突时,能很快找到空余位置。例如:
[index, None, None, None, index, None, None, None]
2、为了保证其高效性,字典和集合内的哈希表,通常会保证其至少留有 1/3 的剩余空间。随着元素的不停插入,当剩余空间小于 1/3 时,Python 会重新获取更大的内存空间,扩充哈希表。不过,这种情况下,表内所有的元素位置都会被重新排放。虽然哈希冲突和哈希表大小的调整,都会导致速度减缓,但是这种情况发生的次数极少。所以,平均情况下,这仍能保证插入、查找和删除的时间复杂度为 O(1)。
3、dict与set实现原理相同,唯一不同的在于hash函数操作的对象,对于dict,hash函数操作的是其key,而对于set是直接操作的它的元素。把实现set的方式叫做Hash Set,实现dict的方式叫做Hash Map/Table(注:map指的就是通过key来寻找value的过程)。
4、python字典的键不可为列表,因为键必须可hash,而列表是动态的,不可hash。