关于python3.6中dict如何保证有序

描述:python3.6中dict有序,有序是指遍历时的输出顺序与输入顺序相同;

相关参考文档:

[Python-Dev] More compact dictionaries with faster iteration

morepypy.blogspot.hk/20

github.com/python/cpyth

PHP’s new hashtable implementation

关于哈希表


1. 散列表概念
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
2. 哈希函数
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
(相关:是不是可以这样理解,数组可以通过下标进行访问,时间复杂度是O(1),对于不连续存储的数据结构,如果知道下标也可以直接进行访问,所以可以通过哈希函数将key映射成数组下标,进行访问)
3. 冲突
不同的key经过hash函数运行后得到相同的值,产生冲突;
4. 冲突解决方式
* 开放寻址:线性探测、二次探测、伪随机数序列(python的dict解决冲突用的这个,具体的策略没有看太明白)
* 再哈希法:将哈希值再哈希,然后存储;
* 链地址法:hash过后值相同的存储在链表里;
* 公共溢出区

python实现dict无序到有序:

  • 存储结构(只有布局的不同,其他完全相同(哈希算法、冲突策略等))
    这里需要先看一下dict的Cpython对象模型(CPython对象模型:Dict - w0mTea - 博客园
    基础补充
    dict中把一个(key, value)对称为一个slot(严格来说一个slot是指一个PyDictEntry类型的变量)。 python中的slot存在4种不同的状态:
    • Unused:表示此slot尚未使用,所有slot都会初始化成该状态;
    • Active:表示此slot正在使用中;
    • Dummy:表示此slot已被删除;
    • Pending:表示此slot尚未被插入到dict中或尚未被删除。


为什么会存在表示已删除的状态呢? dict采用了开放地址法, 存在冲突时会依次寻找新的地址,直到找到空slot(Unused状态) 或者需要查找的key。这样的查找过程,实际上形成了一个查找链, 查找链的结尾是一个Unused或Active状态的slot。 如果查找链中间某一个slot被删除, 并重新回到了Unused状态,那么这个查找链就会被截断, 导致后面的部分不能被找到。 因此Dummy存在的意义就在于维持查找链的完整。

Example d = {'timmy': 'red', 'barry': 'green', 'guido': 'blue'}

从下图中可以看出:
Stack Overflow 2016年度 20个最佳Python问题(二)
1. 原先的内存布局entries为哈希表,表中直接存储PyDictKeyEntry(hash、key、value),也就是说当当前位置为空的时候存的是(0, null, null)浪费了大量内存;
2. python3.6: indices充当哈希表,存储的entries的index,使用index去访问存有PyDictKeyEntry的数组;

python3.6内存布局详解:
- dk_indices: 保存字典条目(类型为PyDictKeyEntry)的插入顺序。保留顺序是通过只允许附加到数组最后来实现,该数组的新元素总会插入到末端(插入顺序)

- dk_entries:保存dk_entries数组的索引(即,指示dk_entries中相关条目的位置)。此数组充当**哈希表**。当key被哈希时,就能得到存储在dk_indices中的一个索引,进而通过索引获得dk_entries中的相应条目。由于只保留索引,该数组的类型取决于字典的整体大小(范围从int8_t(1字节)到32/64位构建的int32_t / int64_t(4/8字节))

- 将存储PyDictKeyEntry的稀疏数组更改为存储int的稀疏数组;

  • 数据结构:
    (增加)variableint:sparsearray,根据sparsearray中的值去访问dictentry;


    1. 节省存储空间:将存储PyDictKeyEntry的稀疏数组更改为存储int的稀疏数组;
    2. 之前的dict_entry是稀疏表,经压缩后在密集表上循环,使用更少的内存;
    3. 调整大小更快,并且触及更少的内存。 目前,每一个散列/键/值条目在一个过程中被移动或复制调整。 在新的布局中,只有索引是更新。 大多数情况下,散列/键/值条目从不移动(除了偶尔交换填充删除留下的空洞)。

编辑于 2018-04-27