Lua:如何在键为表格(或对象)的表中进行查找

8

我希望能够存储一个lua表格,其中键是其他的lua表格。我知道这是可能的,但我希望能够在该表格中使用这些表格的副本进行查找。具体来说,我想要能够执行以下操作:

t = {}
key = { a = "a" }
t[key] = 4
key2 = { a = "a" }

然后我希望能够查找:
t[key2]

然后得到4。

我知道我可以将key转换为字符串并放入表格t中。我也考虑过编写自定义哈希函数或嵌套表格来实现这一点。有没有更好的方法让我获得这种功能?还有哪些其他选项可供选择?

7个回答

9

在Lua中,两个单独创建的表被视为“不同”。但是如果您创建一个表格一次,可以将其分配给任何想要的变量,并且当您比较它们时,Lua将告诉您它们相等。换句话说:

t = {}
key = { a = "a" }
t[key] = 4
key2 = key
...
t[key2] -- returns 4

那么,这就是做你想要的事情的简单清晰的方法。将key存储在某个地方,以便您可以使用它检索4。这也非常快速。

如果您真的不想这样做...好吧,有一种方法。但它有点低效和丑陋。

第一部分是创建一个比较两个不同表格的函数。如果两个表格是“相等”的,则它应该返回true,如果它们不是,则应该返回false。让我们称之为equivalent。它应该像这样工作:

equivalent({a=1},{a=1})          -- true
equivalent({a=1,b=2}, {a=1})     -- false
equivalent({a={b=1}}, {a={b=2}}) -- false

该功能必须是递归的,以处理包含表格本身的表格。如果其中一个表“包含”另一个表但具有更多元素,则它也不得被愚弄。我想出了这个实现方法;可能还有更好的方法。

local function equivalent(a,b)
  if type(a) ~= 'table' then return a == b end

  local counta, countb = 0, 0

  for k,va in pairs(a) do
    if not equivalent(va, b[k]) then return false end
    counta = counta + 1
  end

  for _,_ in pairs(b) do countb = countb + 1 end

  return counta == countb
end

我不会在这里解释该函数。我希望它足够清楚地说明它的作用。

谜题的另一个部分是使 t 在比较键时使用 equivalent 函数。这可以通过仔细的元表操作和额外的 "存储" 表来完成。

我们基本上将 t 转换成了一个冒牌货。当我们的代码告诉它在一个键下存储一个值时,它不会将其保存在自身中;相反,它将其交给额外的表(我们将其称为 store)。当代码请求 t 的值时,它会在 store 中搜索,但使用 equivalent 函数获取它。

以下是代码:

local function equivalent(a,b)
... -- same code as before
end

local store = {} -- this is the table that stores the values

t = setmetatable({}, {
  __newindex = store,
  __index = function(tbl, key)
    for k,v in pairs(store) do
      if equivalent(k,key) then return v end
    end
  end
})

使用示例:

t[{a = 1}] = 4

print(t[{a = 1}]) -- 4
print(t[{a = 1, b = 2}]) -- nil

4

kikito的回答很好,但存在一些缺陷:

  • 如果你执行t[{a=1}] = true两次,store将包含两个表(泄漏哈希表的生命周期内存)
  • 一旦存储了值,修改该值将不起作用,也无法删除它。试图更改它将导致检索潜在地返回您过去为该键分配的任何值。
  • 访问性能为O(n)(n是存储条目的数量,并假设lua从表中检索值的检索时间为O(1));结合第一个问题,这个哈希表的性能会随着使用而降低

(还要注意,如果任何表具有循环引用,则kikito的“等效”函数将导致无限循环。)

如果您永远不需要更改/删除表中的任何信息,则kikito的答案就足够了。否则,必须更改元表,以便__newindex确保表不存在:

t = setmetatable({}, {
    __newindex = function(tbl, key, value)
        for k,v in pairs(store) do
            if equivalent(k,key) then
                tbl[k] = value
                return
            end
        end
        store[key] = value
    end,
    __index = function(tbl, key)
        for k,v in pairs(store) do
            if equivalent(k, key) then return v end
        end
    end
})

正如你所建议的,一个完全不同的选项是编写自定义哈希函数。这里有一个可以利用它的HashTable:

local function HashTable(Hash, Equals)
    --Hash is an optional function that takes in any key and returns a key that lua can use (string or number). If you return false/nil, it will be assumed that you don't know how to hash that value.
    --    If Hash is not provided, table-keys should have a GetHash function or a .Hash field
    --Equals is an optional function that takes two keys and specify whether they are equal or not. This will be used when the same hash is returned from two keys.
    --    If Equals is not provided, items should have a Equals function; items are in this case assumed to not be equal if they are different types.
    local items = {} --Dict<hash, Dict<key, value>>
    local function GetHash(item)
        return Hash and Hash(item) or type(item) == "table" and (item.GetHash and item:GetHash() or item.Hash) or item
    end
    local function GetEquals(item1, item2)
        if Equals then return Equals(item1, item2) end
        local t1, t2 = type(item1), type(item2)
        if t1 ~= t2 then return false end
        if t1 == "table" and item1.Equals then
            return item1:Equals(item2)
        elseif t2 == "table" and item2.Equals then
            return item2:Equals(item1)
        end
        return false
    end
    return setmetatable({}, {
        __newindex = function(_, key, value)
            local hash = GetHash(key)
            local dict = items[hash]
            if not dict then
                if value ~= nil then --Only generate a table if it will be non-empty after assignment
                    items[hash] = {[key] = value}
                end
                return
            end
            for k, v in pairs(dict) do
                if GetEquals(key, k) then --Found the entry; update it
                    dict[k] = value
                    if value == nil then --check to see if dict is empty
                        if next(dict) == nil then
                            items[hash] = nil
                        end
                    end
                    return
                end
            end
            --This is a unique entry
            dict[key] = value
        end,
        __index = function(_, key)
            local hash = GetHash(key)
            local dict = items[hash]
            if not dict then return nil end
            for k, v in pairs(dict) do
                if GetEquals(key, k) then
                    return v
                end
            end
        end
    })
end

使用示例:

local h = HashTable(
    function(t) return t.a or 0 end, --Hash
    function(t1, t2) return t1.a == t2.a end) --Equals
h[{a=1}] = 1
print(h[{a=1}]) -- 1
h[{a=1}] = 2
print(h[{a=1}]) -- 2
print(h[{a=1,b=2}]) -- 2 because Hash/Equals only look at 'a'

自然而然,您会想要获得更好的哈希/相等函数。
只要键的哈希很少冲突,这个类的性能应该是O(1)。
(注:我本来会把这个答案的前半部分作为对kikito的评论,但是目前我没有足够的声望去这样做。)

2
这在Lua中是不可能的。如果您使用表作为键,那么该键就是该特定表的“实例”。即使您使用相同内容创建了另一个表,实例也不同,因此它是不同的键。
如果您想要做类似的事情,可以创建一种哈希函数,遍历表以用作键(如果需要,甚至可以递归遍历),并构造表内容的字符串表示形式。它不需要人类可读性,只要对于不同的内容是不同的,并且对于具有相同内容的表是相等的即可。除了使用遍历表之外,您还需要将键插入到表中并使用table.sort()对其进行排序,因为以任意顺序返回它们,而您希望对于“相等”的表返回相同的字符串。
一旦构造出这样的字符串,您就可以将其用作键:
function hash(t) ... end
t = {}
key1 = { a = "a", b = "b" }
t[hash(key1)] = 4
key2 = { a = "a", b = "b" }
print(t[hash(key2)]) -- should print "4" if the hash function works correctly

在我看来,这一切对于索引的简单任务来说都太过复杂了,你可能需要重新考虑使用表格副本进行索引的愿望。为什么你想要这样的功能呢?
更新
如果你只需要处理短语,我认为将它们连接起来比创建这样通用的哈希函数更容易。如果你需要处理短语序列,你实际上并不需要迭代表格并排序键,只需从每个短语中收集主要信息即可。你仍然需要使用一个辅助函数,它可以为你创建一个适当的键:
function pkey(...)
    local n, args = select('#', ...), { ... }
    for i=1,n do args[i] = args[i].value end -- extract your info here
    return table.concat(args, ' ') -- space or other separator, such as ':'          
end
tab[pkey(phrase1, phrase2, phrase3)] = "value"

感谢回复。我想要这个的原因是为了进行NLP任务。我提取短语,将它们存储为lua表格(短语中的每个标记作为一个值,使用table.insert映射到索引),并且我想计算短语的频率。我知道有其他方法可以实现我的目的(例如连接短语并使用该连接字符串作为键),但这需要额外的实现步骤,而且不够简洁。我相信你可以用Java做到我想要的事情,而我作为新手正在尝试看看是否有类似的东西可以用lua实现。 - akobre01
1
这样的哈希函数很难编写,因为表格遍历的顺序取决于它是如何创建的,因此具有相同条目的表格可能具有不同的遍历顺序。 - lhf
这就是为什么我提到将键收集到表中并进行排序以确保键的顺序一致。 - Michal Kottman

1

我对语言处理和您想要通过程序实现的目标了解不多,但是您可以考虑像这样收集令牌:使用嵌套表结构,使索引表仅存储由第一个短语令牌索引的表,然后每个子表都包含由第二个短语令牌索引的值...等等,直到达到短语最终令牌,将索引与该短语出现次数相对应的数字值。

如果您有以下两个短语:

  • 我喜欢香蕉。
  • 我喜欢辣妹。

您的索引将具有以下结构:

index["I"] = {
    ["like"] = {
        ["banana"] = 1,
        ["hot"] = {
            ["chick"] = 1
        }
    }    
}

以这种方式,您可以在单个遍历步骤中计算频率,并在索引同时计算出现次数,但正如我之前所说,这取决于您的目标,并且它将意味着重新拆分短语以通过您的索引查找出现次数。

我真正想要的是:如果我有a = {“I”,“Like”,“Banana”}和b = {“I”,“Like”,“Banana”},并且我写t [a] =“zoo”,我希望有一个常数时间方案,使得t [b] ==“zoo”。 - akobre01
正如之前所说,这是不可能做到的,您必须通过迭代表值来手动比较。 - Faylixe
1
但是如果他除了“我喜欢辣妹”之外还有“我喜欢辣”的短语呢?他应该把它的“= 1”存储在哪里? - Niccolo M.

0

kikito的回答提供了一个解决方案的开端,但正如chess123mate的回答所指出的那样,它是只写不读的(还有其他缺陷)。这个解决方案并没有修复所有问题,但它是一个开始。(它也非常非常慢。)

local function equivalent(stack)
    local a, b = stack.a, stack.b

    if type(a) ~= 'table' then return a == b end
    if a == b then return true end

    local top = stack.next
    while top ~= nil do
        if stack.a == a and stack.b == b then return true end
        top = stack.next
    end

    local counta, countb = 0, 0

    for k,va in pairs(a) do
        if not equivalent({a = va, b = b[k], next = stack}) then return false end
        counta = counta + 1
    end

    for _,_ in pairs(b) do countb = countb + 1 end

    return counta == countb
end

t = setmetatable({}, {
    __newindex = function(tbl, key, value)
        for k, _ in pairs(tbl) do
            if equivalent({a = k, b = key}) then rawset(tbl, k, value); return end
        end
        rawset(tbl, key, value)
    end,
    __index = function(tbl, key)
        for k, v in pairs(tbl) do
            if equivalent({a = k, b = key}) then return v end
        end
    end
})

Lua Playground link(或GitHub Gist link,如果您的浏览器对此最后一个链接感到不适,请使用它进行复制和粘贴)。


0

我不确定你能做到这一点。您可以使用元表为表定义相等性,但无法定义哈希函数,而且我怀疑仅定义相等性是否能满足您的需求。您可以显然定义相等性,然后使用pairs()迭代表并自己比较键,但这将把本应该是O(1)查找变成O(n)


0
我认为做这样的事情最简单的方法是拥有一个tableKey工厂,它返回输入表的readonly表。
您希望分配一个__key(或类似)值作为键。类似于:
_cachedTableKeys = {}
function tableKey (t)
  if(not t.__key) t.__key = calculateKey(t)
  local existing = _cachedTableKeys[t.__key]
  if(existing) return existing
  existing = readOnly(t) -- https://www.lua.org/pil/13.4.5.html
  _cachedTableKeys[t.__key] = existing
  return existing 
end

这将确保作为键使用的表是只读的,并且如果相等,它的id将始终匹配。
注意:calculateKey函数会对表键进行递归哈希函数计算。您需要使用足够大的哈希(比如16字节左右),以确保几乎不可能发生碰撞。在按键查找表时,您还可以实现某种碰撞算法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接