存储结构
c
typedef union TKey {
struct {
TValuefields;
int next; // 下一个哈希冲突节点的索引
} nk;
TValue tvk;
} TKey;
typedef struct Node {
TValue i_val; // 值
TKey i_key; // 键
} Node;
存储示例
lua
local t = {
"array1", -- 在数组部分,索引1
"array2", -- 在数组部分,索引2
a = "hash1", -- 在哈希表部分
b = "hash2", -- 在哈希表部分
c = "hash3", -- 在哈希表部分
[10] = "array10", -- 在数组部分,索引10
}
在内存中大致是这样:
数组部分: [0]:nil [1]:"array1" [2]:"array2" [3-9]:nil [10]:"array10"
哈希表部分: 桶1: a="hash1" → b="hash2" (a和b哈希冲突)
桶2: c="hash3"
遍历table的原理
遍历字典通常是使用pairs,用一段伪代码讲述一下其运行的大概逻辑
lua
function pairs(table)
local last_key = nil
return function()
-- 获取下一个键
local next_key = find_next_key(table, last_key)
last_key = next_key
return next_key, table[next_key]
end
end
其中find_next_key是其关键,本质就是next完成寻找下一个key的工作,接下来就详细讲解下next的工作机制
next工作机制
c
// 伪代码简化版
int luaH_next(Table *t, const TValue *key) {
if (key == NULL) {
// 第一次调用,从数组部分开始
for (int i = 0; i < t->sizearray; i++) {
if (!ttisnil(&t->array[i])) {
push_key(i+1); // Lua索引从1开始
push_value(t->array[i]);
return 1;
}
}
// 数组部分没有,从哈希表第一个非空桶开始
return find_first_hash_entry(t);
}
// 不是第一次调用
if (ttisinteger(key)) {
// 键是整数,可能在数组部分
int idx = ivalue(key);
if (idx >= 1 && idx <= t->sizearray) {
// 继续在数组部分找下一个
for (int i = idx; i < t->sizearray; i++) {
if (!ttisnil(&t->array[i])) {
push_key(i+1);
push_value(t->array[i]);
return 1;
}
}
}
}
// 在哈希表部分查找
return find_next_hash_entry(t, key);
}
由上面的逻辑,我们可以看出其是通过获取上一次查询的键,提高遍历的效率,不需要每次都从头查找某个只在哪。其中最重要的接口就是find_next_hash_entry
find_next_hash_entry
c
static int find_next_hash_entry(Table *t, const TValue *key) {
// 1. 找到key的主位置
Node *mp = luaH_mainposition(t, key);
Node *n = mp;
// 2. 在冲突链中查找这个key
while (n != NULL && !luaV_equalobj(NULL, key, &n->i_key.tvk)) {
n = gnext(n); // 沿着next指针找
}
if (n != NULL) {
// 3. 找到了key,返回冲突链中的下一个节点
Node *next = gnext(n);
if (next != NULL) {
push_key_from_node(next);
push_value_from_node(next);
return 1;
}
}
// 4. 没找到key,或者key是冲突链的最后一个节点
// 从key的主位置的下一个桶开始找
int i = mp - t->node; // 当前桶的索引
for (i = i + 1; i <= t->sizemask; i++) {
Node *node = &t->node[i];
if (!ttisnil(gval(node))) {
push_key_from_node(node);
push_value_from_node(node);
return 1;
}
}
return 0; // 没有更多元素
}
遍历过程中不要删除元素
删除导致漏掉的具体过程
lua
local t = {}
t.x = 1 -- 哈希到桶0
t.y = 2 -- 和x冲突,在桶0的链表中
t.z = 3 -- 哈希到桶1
内存结构:
桶0: [x=1] → [y=2]
桶1: [z=3]
桶2: 空
桶3: 空
lua
for k, v in pairs(t) do
print(k, v)
if k == "x" then
t[k] = nil -- 删除x
end
end
步骤分解:
-
第一次调用 next(t, nil):
-
找到第一个非空桶:桶0
-
返回桶0的第一个节点:x=1
-
遍历器记住:当前是x
-
-
第二次调用 next(t, "x"):
c
// 在find_next_hash_entry中:
// 1. 找到x的主位置:桶0
// 2. 在桶0的链表中找x
// 3. 但是!x已经被删除了!
// 4. 找不到x,进入第4步逻辑
// 第4步:从桶0的下一个桶开始找
for (i = 0 + 1; i <= sizemask; i++) {
// 检查桶1,找到z=3
// 返回z=3
}
-
结果:y=2 被完全跳过了!
-
输出变成了:x, z
-
y 消失了
为什么找不到被删除的节点?
当执行 t.x = nil时:
- 节点的 value 被设为 nil
- 但节点的 key 还在
- 然而在遍历时,luaV_equalobj比较的是完整的节点
- 由于 value 是 nil,这个节点被认为是"空节点"
- 在冲突链遍历时,空节点会被跳过
如果删除的元素较多,可能触发哈希表的重建,例如以下代码:
lua
local t = {}
-- 填充很多元素
for i = 1, 1000 do
t["key"..i] = i
end
for k, v in pairs(t) do
if v % 2 == 0 then
t[k] = nil
-- 删除可能触发rehash
-- 哈希表结构完全改变!
end
end
如何正确删除table中的元素
lua
function safe_pairs(t)
local keys = {}
for k in pairs(t) do
table.insert(keys, k)
end
local i = 0
return function()
i = i + 1
local k = keys[i]
if k then
return k, t[k]
end
end
end