字典是否是有序
在Python3.6之前,字典是无序的,但是Python3.7+,字典是有序的。
在3.6中,字典有序是一个implementation detail,在3.7才正式成为语言特性,因此3.6中无法确保100%有序。
字典的本质就是 hash 表,hash 表就是通过 key 找到其 value ,平均情况下你只需要花费 O(1) 的时间复杂度即可以完成对一个元素的查找,字典是否有序,并不是指字典能否按照键或者值进行排序,而是字典能否按照插入键值的顺序输出对应的键值。
比如,对于一个无序字典,插入顺序和遍历的顺序是不一致的:
>>> my_dict = dict()
>>> my_dict["name"] = "lowman"
>>> my_dict["age"] = 26
>>> my_dict["girl"] = "Tailand"
>>> my_dict["money"] = 80
>>> my_dict["hourse"] = None
>>> for key,value in my_dict.items():
... print(key,value)
...
money 80
girl Tailand
age 26
hourse None
而一个有序字典的输出是这样的:
name lowman
age 26
girl Tailand
money 80
hourse None
先从 Python3.6 之前说起。在 Python 3.6 之前,其数据结构如下图所示:
由于不同键的哈希值不一样,哈希表(entries)中的顺序是按照哈希值大小排序的,遍历时从前往后遍历并不能输出键值插入的顺序,其表现起来就是无序的。
此外,这种方式还有一个缺点,就是如果以稀疏的哈希表存储时,会浪费较多的内存空间,Python3.6 之后,对其进行了优化,哈希索引和真正的键值对分开存放,数据结构如下所示:
indices 指向了一列索引,entries 指向了原本的存储哈希表内容的结构。
你可以把 indices 理解成新的简化版的哈希表,entries 理解成一个数组,数组中的每个元素是原本应该存储的哈希结果:键和值。
查找或者插入一个元素的时候,根据键的哈希值结果取模 indices 的长度,就能得到对应的数组下标,再根据对应的数组下标到 entries 中获取到对应的结果,比如 hash("key2") % 8 的结果是 3,那么 indices[3] 的值是 1,这时候到 entries 中找到对应的 entries[1] 既为所求的结果:
这么做的好处是空间利用率得到了较大的提升,我们以 64 位操作系统为例,每个指针的长度为 8 字节,则原本需要 8 * 3 * 8 为 192
现在变成了 8 * 3 * 3 + 1 * 8 为 80,节省了 58% 左右的内存空间,如下图所示:
此外,由于 entries 是按照插入顺序进行插入的数组,对字典进行遍历时能按照插入顺序进行遍历,这也是为什么 Python3.6 以后的版本字典对象是有序的原因。
字典的查询、添加、删除的时间复杂度
字典的查询、添加、删除的平均时间复杂度都是O(1),相比列表与元祖,性能更优。
字典的实现原理
Python3.6之前的无序字典
字典底层是维护一张哈希表,可以把哈希表看成一个列表,哈希表中的每一个元素又存储了哈希值(hash)、键(key)、值(value)3个元素。
enteies = [
['--', '--', '--'],
[hash, key, value],
['--', '--', '--'],
['--', '--', '--'],
[hash, key, value],
]
带入具体的数值来介绍
# 给字典添加一个值,key为hello,value为word
# my_dict['hello'] = 'word'
# hash表初始如下
enteies = [
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
]
hash_value = hash('hello') # 假设值为 12343543
index = hash_value & ( len(enteies) - 1) # 假设index值计算后等于3
# 下面会将值存在enteies中
enteies = [
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
[12343543, 'hello', 'word'], # index=3
['--', '--', '--'],
]
# 继续向字典中添加值
# my_dict['color'] = 'green'
hash_value = hash('color') # 假设值为 同样为12343543
index = hash_value & ( len(enteies) - 1) # 假设index值计算后同样等于3
# 下面会将值存在enteies中
enteies = [
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
[12343543, 'hello', 'word'], # 由于index=3的位置已经被占用,且key不一样,所以判定为hash冲突,继续向下寻找
[12343543, 'color', 'green'], # 找到空余位置,则保存
]
enteies表是稀疏的,随着我们插入的值不同,enteies表会越来越稀疏(enteies也是一个会动态扩展长度的,每一此扩展长度,都会重新计算所有key的hash值),所以新的字典实现就随之出现。
Python3.7+后的新的实现方式
Python3.7+带入数据演示
# 给字典添加一个值,key为hello,value为word
# my_dict['hello'] = 'word'
# 假设是一个空列表,hash表初始如下
indices = [None, None, None, None, None, None]
enteies = []
hash_value = hash('hello') # 假设值为 12343543
index = hash_value & ( len(indices) - 1) # 假设index值计算后等于3
# 会找到indices的index为3的位置
indices = [None, None, None, 0, None, None]
# 此时enteies会插入第一个元素
enteies = [
[12343543, 'hello', 'word']
]
# 我们继续向字典中添加值
my_dict['haimeimei'] = 'lihua'
hash_value = hash('haimeimei') # 假设值为 34323545
index = hash_value & ( len(indices) - 1) # 假设index值计算后等于 0
# 会找到indices的index为0的位置
indices = [1, None, None, 0, None, None]
# 此时enteies会插入第一个元素
enteies = [
[12343543, 'hello', 'word'],
[34323545, 'haimeimei', 'lihua']
]
查询字典
# 下面是一个字典与字典的存储
more_dict = {'name': '张三', 'sex': '男', 'age': 10, 'birth': '2019-01-01'}
# 数据实际存储
indices = [None, 2, None, 0, None, None, 1, None, 3]
enteies = [
[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] # 数据在enteies的位置是2
value = enteies[entey_index] # 所以找到值为 enteies[2]
时间复杂度
字典的平均时间复杂度是O(1),因为字典是通过哈希算法来实现的,哈希算法不可避免的问题就是hash冲突,Python字典发生哈希冲突时,会向下寻找空余位置,直到找到位置。如果在计算key的hash值时,如果一直找不到空余位置,则字典的时间复杂度就变成了O(n)了。
常见的哈希冲突解决方法:
1 开放寻址法(open addressing)
开放寻址法中,所有的元素都存放在散列表里,当产生哈希冲突时,通过一个探测函数计算出下一个候选位置,如果下一个获选位置还是有冲突,那么不断通过探测函数往下找,直到找个一个空槽来存放待插入元素。
2 再哈希法
这个方法是按顺序规定多个哈希函数,每次查询的时候按顺序调用哈希函数,调用到第一个为空的时候返回不存在,调用到此键的时候返回其值。
3 链地址法
将所有关键字哈希值相同的记录都存在同一线性链表中,这样不需要占用其他的哈希地址,相同的哈希值在一条链表上,按顺序遍历就可以找到。
4 公共溢出区
其基本思想是:所有关键字和基本表中关键字为相同哈希值的记录,不管他们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。