文章目录

  • 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冲突后,继续线性查找,指导找到下一个空闲的位置。

(图源网络,侵删)

python 字典的案例 python 字典原理_数据结构

2、拉链法

将发生hash冲突的元素放到同一位置,然后通过“指针“来串联起来。

(图源网络,侵删)

python 字典的案例 python 字典原理_dict_02

四、几点说明

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。