lua——哈希表详细学习

存储结构

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

步骤分解:

  1. 第一次调用 next(t, nil):

    • 找到第一个非空桶:桶0

    • 返回桶0的第一个节点:x=1

    • 遍历器记住:当前是x

  2. 第二次调用 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时:

  1. 节点的 value 被设为 nil
  2. 但节点的 key 还在
  3. 然而在遍历时,luaV_equalobj比较的是完整的节点
  4. 由于 value 是 nil,这个节点被认为是"空节点"
  5. 在冲突链遍历时,空节点会被跳过

如果删除的元素较多,可能触发哈希表的重建,例如以下代码:

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
相关推荐
小新同学^O^1 小时前
初步了解--> SpringCloud
java·学习·spring·spring cloud
Slow菜鸟2 小时前
Skill 学习篇(九)| 编排框架 · OpenSpec 专篇(1→10 阶段)
学习
星幻元宇VR2 小时前
VR雷霆双翼助力航空航天科普教育
科技·学习·安全·vr
知识分享小能手2 小时前
R语言入门学习教程,从入门到精通,集成开发环境RStudio(2)
开发语言·学习·r语言
大都督会赢的2 小时前
数据结构(1)--顺序表
c语言·数据结构·学习·指针
阿阳微客2 小时前
CS2饰品市场急跌,抄底时机是否已到?
笔记·学习·游戏
gz927cool3 小时前
【系统架构】可观测性设计及其应用——面向智能体开发视角
人工智能·学习·ai·系统架构
星幻元宇VR3 小时前
5D球幕飞行影院|沉浸式科技体验引领文旅与科普新方向
科技·学习·安全·虚拟现实
南境十里·墨染春水3 小时前
linux 学习进展 mysql 事务详解
linux·学习·mysql