Python列表和字典的本质和区别
“选择正确的数据结构并坚持使用它!虽然对于某个特定操作来说也许还存在更高效的数据结构,但是在这些数据结构之间进行转换的代价可能会抵消效率上的增益。” 摘自《Python高性能编程》
Python中的列表本质是动态的数组,它与数组的区别在于:1)数组定义好之后就无法扩容了,而列表在定义好之后可以扩容;2)数组只能同时存储一种类型的数据,而列表可以同时存储不同类型的数据。列表为什么没有数组这样的限制呢?
我们知道数组底层的存储结构是顺序存储结构,这样的结构有这样一些优点:逻辑上相邻的节点在物理位置上也是相邻的,可以节省空间,并且可以实现随机存取(也称直接访问)。创建一个数组时,会在内存中开辟一块固定长度的区域用于直接存储元素,扩容要考虑这块区域的后面是否有存储其他对象,所以数组在定义好之后就无法扩容了。而且在查询时,是根据索引和元素存储大小去计算地址偏移量的,如果元素类型不一致,所占内存空间不相同,就不能实现随机存储,所以数组不能同时存储不同类型的数据;而列表就不同了,它存储的是每个元素在内存中的地址(即引用),当列表中空白占位低于1/3时,会在内存中开辟一块更大的空间,并将旧列表中存储的地址复制到新列表中,旧列表则被销毁,这样就实现了扩容。因为列表存储的是元素的引用这个特性,而引用所占的内存空间是相同的,这样便可以同时存放不同类型的数据了。
Python中的字典底层是通过散列表(哈希表)来实现的, “哈希表是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。”
字典本质也是一个数组,但其索引是键经过散列函数处理后得到的散列值,散列函数的目的是使键均匀地分布在散列表中,并且可以在内存中以O(1)的时间复杂度进行寻址,从而实现快速查找和修改。散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组),散列表里的单元通常叫作表元。在字典的散列表当中,每个键值对都占用一个表元,每个表元都有两个部分,一个是对键的引用,另一个是对值的引用。散列表中散列函数的设计困难在于将数据均匀分布在散列表中,从而尽量减少散列碰撞和冲突(散列冲突指的是在查询过程中通过索引定位到有值的表元时,发现该位置的key和查询的key不相等。高级的散列函数能够使冲突数目最小化。散列冲突在这里不过多描述)。
字典数据的添加和查询过程如下:1)添加:把key通过散列函数转换成一个整型数字(即散列值),把这个值最低的几位数字当作数组存储value的下标,最后再存储value到数组中;2)查询:使用散列函数将key转换为数组的下标,并定位到数组对应位置获取value。
字典为什么是无序的?字典的无序是由两方面构成的:1)上面有说过Python中列表会设法保证大概还有1/3的表元是空的,所以快要达到这个阀值时,原有的散列表会被复制到一个更大的空间中去。在这个过程中会重新对字典的键进行散列化(散列表的大小增加了,那散列值所占位数和用作索引的位数都会随之增加,这样做的目的是为了减少发生散列冲突的概率),生成新的散列值,此时由于散列值的不同,则可能导致键的次序不同;2)另一方面就是就是散列冲突,当添加新键时发生散列冲突(即新键通过散列函数处理得到的散列值和字典中其他键的散列值相同,则需要在散列值中另外再取几位,然后用特殊的方法处理一下,把新得到的数字当作索引来寻找空的表元),新键可能会被安排到另一个位置,于是新添加的元素可能就跑到前面去了。
最后再说一点,因为字典的存储使用了散列表,为了减少散列冲突发生概率,散列表必须是稀疏的,所以字典在内存上的开销是很大的。
Python中列表和字典在时间复杂度的比较
|
list
|
dict
|
查询
|
查找某个元素的索引:O(1)
搜索某个元素:O(n)
|
获取某个元素: O(1)
根据键搜索:O(1)
|
添加
|
尾部添加:O(1)
任意位置添加:O(n)
|
键添加:O(1)
|
删除
|
尾部删除:O(1)
|
根据键来删除:O(1)
|
修改
|
索引赋值:O(1)
|
键赋值:O(1)
|