【问题标题】:Lua: How to look up in a table where the keys are tables (or objects)Lua:如何在键是表(或对象)的表中查找
【发布时间】:2021-02-23 02:17:17
【问题描述】:

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

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

然后我希望能够查找:

t[key2]

得到 4。

我知道我可以将key 变成一个字符串并放入表t 中。我还考虑过编写自定义哈希函数或通过嵌套表来实现。我有没有获得这种功能的最佳方法?我还有什么其他选择?

【问题讨论】:

    标签: lua hashtable lua-table


    【解决方案1】:

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

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

    所以,这就是做你想做的事情的简单、干净的方式。将key 存储在某处,以便您可以使用它检索4。这也非常快。

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

    第一部分是制作一个比较两个单独表格的函数。如果两个表“等效”,它应该返回 true,否则返回 false。让我们称它为等价物。它应该像这样工作:

    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
    

    【讨论】:

      【解决方案2】:

      kikito 的回答不错,但有一些缺陷:

      • 如果您执行两次t[{a=1}] = truestore 将包含两个表(散列表的生命周期内存泄漏)
      • 一旦你已经存储了值,修改它就不起作用,你也不能删除它。尝试更改它可能会导致检索返回您过去分配给该键的任何值。
      • 访问性能是 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'
      

      当然,您会希望获得更好的 Hash/Equals 函数。

      只要你的键的哈希很少发生冲突,这个类的性能应该是 O(1)。

      (注意:我会将此答案的上半部分作为对 kikito 的评论,但我目前没有这样做的声誉。)

      【讨论】:

        【解决方案3】:

        这在 Lua 中是不可能的。如果您使用表作为键,则键是表的特定“实例”。即使你用相同的内容制作不同的表,实例也是不同的,因此它是不同的键。

        如果你想做这样的事情,你可以创建一种哈希函数,它遍历表作为键(如果需要,甚至可以递归)并构造表内容的字符串表示。它不需要是人类可读的,只要它对于不同的内容是不同的并且对于具有相同内容的表来说是相等的。除了使用pairs() 遍历表之外,您还需要将键插入表中并使用table.sort() 对它们进行排序,因为pairs() 以任意顺序返回它们,并且您希望“等于”的字符串相同" 表格。

        一旦你构建了这样的字符串,你就可以将它用作键:

        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 的新手,我正在尝试看看是否有类似的
        • 这样的哈希函数很难编写,因为表的遍历顺序取决于它的创建方式,因此具有相同条目的表可能有不同的遍历。
        • 这就是为什么我说将键收集到一个表中并对其进行排序以确保键顺序一致。
        【解决方案4】:

        我不太了解语言处理以及您希望通过程序达到的目标,但是像这样收集令牌怎么样:使用嵌套表结构,例如索引表仅存储由 first 索引的表短语标记,然后每个子表包含由第二个短语标记索引的值......等等......直到你到达一个短语最终标记,将索引一个与他出现的短语相对应的数值。

        如果你有以下两个短语,也许举个例子会更清楚:

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

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

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

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

        【讨论】:

        • 我真正想要的是:如果我有 a = { "I", "Like", "Banana" } 和 b = { "I", "Like", "Banana " } 我写 t[a] = "zoo",我想要一个恒定的时间方案,其中 t[b] == "zoo"。
        • 正如之前所说的不可能做到这一点,您必须在某些时候通过迭代表值来进行手动比较。
        • 但是如果他除了“我喜欢辣妹”之外还有“我喜欢辣”这句话怎么办?他会将其“= 1”存储在哪里?
        【解决方案5】:

        我不确定你能做到这一点。您可以使用元表定义表的相等性,但无法定义散列函数,我怀疑单独定义相等性会满足您的需要。您显然可以定义相等,然后使用pairs() 遍历表并自己比较键,但这会将应该是O(1) 查找变成O(n)

        【讨论】:

          【解决方案6】:

          kikito's answer 有一个解决方案的开始,但正如chess123mate's answer 所指出的,它是只写的(以及其他缺陷)。这个解决方案并不能解决所有问题,但它是一个开始。 (它也非常非常慢。)

          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 用于复制和粘贴到 Playground,如果您的浏览器讨厌我最后一个)。

          【讨论】:

            猜你喜欢
            • 2012-04-03
            • 2020-04-06
            • 1970-01-01
            • 1970-01-01
            • 2021-04-16
            • 2017-07-18
            • 1970-01-01
            • 1970-01-01
            • 2018-12-19
            相关资源
            最近更新 更多