python中大部分的运行机制当中都采用了dict来进行数据的管理,所以为了最高效的查询性能,
dict底层选择了基于哈希表实现。其中在python3.6之前,dict的实现里将数据存在了hash表里,
所以之前的dict是无序且十分消耗内存的,而在python3.6以后,官方对dict进行了很大地改进
(主要是将数据和hash部分分离),包括从无序变为有序,在一定程度上减少了内存的消耗,以及
部分操作性能的提升。
链接1
链接2
3.6版本前和3.6后对比
3.6前
用一个数组存储combined
1 | \[(hash, key, value), (hash, key, value), (hash, key, value)] |
dict状态
- 1.Unused:此时key为null,即还未使用当前位置,此时索引值被设为-1
- 2.Active:如果当前位置设置了键值对,那么就会进入该状态,表示正在使用中,此时索引值就是所在位置的索引
- 3.Dummy:当被使用的key删除时,将会进入该状态,用于避免探测链断开而导致索引出错的问题
3.6后
用两个数组存储,一个为entries存储数据的顺序,另一个为原来的
1 | # entries |
dict状态
- 1.Unused:此时key为null,即还未使用当前位置,此时索引值被设为-1
- 2.Active:如果当前位置设置了键值对,那么就会进入该状态,表示正在使用中,此时索引值就是所在位置的索引
- 3.Pending:同样是删除键值对以后进入的状态,此时索引不变,但是对应索引存储的value将会被删除,从而等待新的对应索引的key插入
3.6后源码
PyDictObject
1 | typedef struct { |
key集合
1 | typedef struct _dictkeysobject PyDictKeysObject; |
entries集合
1 | typedef struct { |
dict基本配置和缓冲池
基本配置
1
2
3
4
5
6
7
8// 哈希表的起始尺寸为8
#define PyDict_MINSIZE 8
// 哈希表相对于索引数量的尺寸
#define ESTIMATE_SIZE(n) (((n)*3+1) >> 1)
// 当需要更新dict尺寸时,尺寸大小为索引数*3
#define GROWTH_RATE(d) ((d)->ma_used*3)缓冲池
1
2
3
4
5
6
7
8
9// dict缓冲池大小,尺寸和list缓冲池一样是80
#ifndef PyDict_MAXFREELIST
#define PyDict_MAXFREELIST 80
#endif
// dict对象和dict中的key集合对象都有进行缓冲池操作
static PyDictObject *free_list[PyDict_MAXFREELIST];
static int numfree = 0;
static PyDictKeysObject *keys_free_list[PyDict_MAXFREELIST];
static int numfreekeys = 0;
创建dict
1 | PyObject * |
添加元素
1 | int |
static int
insertdict(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value)
{
PyObject *old_value;
PyDictKeyEntry *ep;
Py_INCREF(key);
Py_INCREF(value);
if (mp->ma_values != NULL && !PyUnicode_CheckExact(key)) {
if (insertion_resize(mp) < 0)
goto Fail;
}
// 寻找对应key的索引
Py_ssize_t ix = mp->ma_keys->dk_lookup(mp, key, hash, &old_value);
// 异常情况处理
if (ix == DKIX_ERROR)
goto Fail;
assert(PyUnicode_CheckExact(key) || mp->ma_keys->dk_lookup == lookdict);
MAINTAIN_TRACKING(mp, key, value);
if (_PyDict_HasSplitTable(mp) &&
((ix >= 0 && old_value == NULL && mp->ma_used != ix) ||
(ix == DKIX_EMPTY && mp->ma_used != mp->ma_keys->dk_nentries))) {
if (insertion_resize(mp) < 0)
goto Fail;
ix = DKIX_EMPTY;
}
// 如果对应的key的映射为空,则代表添加数据
if (ix == DKIX_EMPTY) {
assert(old_value == NULL);
if (mp->ma_keys->dk_usable <= 0) {
if (insertion_resize(mp) < 0)
goto Fail;
}
// 寻找哈希表中当前key的插入位置
Py_ssize_t hashpos = find_empty_slot(mp->ma_keys, hash);
// ep指向entries的最后一位(索引为dk_nentries处)
ep = &DK_ENTRIES(mp->ma_keys)[mp->ma_keys->dk_nentries];
// 设置indices中对应entries的索引,可以看到设置的entries索引就是entries的数量,因为键值对每次都是往entries的最后一位添加的
dictkeys_set_index(mp->ma_keys, hashpos, mp->ma_keys->dk_nentries);
// 设置新加入entries的键值对数据
ep->me_key = key;
ep->me_hash = hash;
if (mp->ma_values) {
assert (mp->ma_values[mp->ma_keys->dk_nentries] == NULL);
mp->ma_values[mp->ma_keys->dk_nentries] = value;
}
else {
ep->me_value = value;
}
// 添加成功操作
mp->ma_used++;
mp->ma_version_tag = DICT_NEXT_VERSION();
mp->ma_keys->dk_usable--;
mp->ma_keys->dk_nentries++;
assert(mp->ma_keys->dk_usable >= 0);
ASSERT_CONSISTENT(mp);
return 0;
}
// 如果不是添加操作,并且指定entries的value和当前传入的value不同,则需要更新value
if (old_value != value) {
// split型表的操作
if (_PyDict_HasSplitTable(mp)) {
// 直接更新entries中对应索引的value
mp->ma_values[ix] = value;
// 当前位置属于pending状态(已删除状态),则说明之前没有使用,现在使用了,所以使用数+1
if (old_value == NULL) {
assert(ix == mp->ma_used);
mp->ma_used++;
}
}
// combined型表的操作
else {
assert(old_value != NULL);
// 由于key、value都是存在哈希表中,直接修改哈希表对应位置的value
DK_ENTRIES(mp->ma_keys)[ix].me_value = value;
}
mp->ma_version_tag = DICT_NEXT_VERSION();
}
Py_XDECREF(old_value);
ASSERT_CONSISTENT(mp);
Py_DECREF(key);
return 0;
Fail:
Py_DECREF(value);
Py_DECREF(key);
return -1;
}
1 |
|
f(key) = (f(key)+di) MOD m (di = 1^2, -1^2, 2^2, -2^2,……, q^2, -q^2, q <= m/2)
1 | - 注:1^2 表示是 1的平方 |
设哈希表长m=14,哈希函数H(key)=key%11。
表中已有4个结点:addr(15)=4,addr(38)=5,addr(61)=6,addr(84)=7,
其余地址为空。如果用二次探测再散列处理冲突,关键字为49的结点的地址是?
因为 f(49) = 5 与 f(38) 冲突
所以需要采用二次探测再散列来处理冲突
(f(49) + di) MOD 14(哈希表长m=14)
第一次 di = 1^2
(5 + 1)MOD 14 = 6 与addr(61)=6冲突
第二次 di = -1^2
(5 - 1)MOD 14 = 4 与addr(15)=4 冲突
第三次 di = 2^2
(5 + 4)MOD 14 = 9 没有冲突
所以 addr(49)=9
```
链地址法
冲突生成链表,链表一直往下。java使用此方法作为首要解决hash冲突的方法,
jdk1.7完全采用单链表来存储同义词,jdk1.8则采用了一种混合模式,对于链表
长度大于8的,会转换为红黑树存储。
再hash
同时构造多个不同的哈希函数
Hi = RHi(key) i= 1,2,3 … k;
当H1 = RH1(key) 发生冲突时,再用H2 = RH2(key) 进行计算,
直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间
###dict的hash冲突解决
使用开放寻址的二次探查,如果使用的容量超过数组大小的2/3,
就申请更大的容量。数组大小较小的时候resize为*4,
较大的时候resize*2