【问题标题】:Safely remove items from an array table while iterating在迭代时安全地从数组表中删除项目
【发布时间】:2021-06-05 18:35:20
【问题描述】:

这个问题与How can I safely iterate a lua table while keys are being removed 类似,但明显不同。

总结

给定一个 Lua 数组(表的键是从 1 开始的连续整数),遍历这个数组的最佳方法是什么并删除一些看到的条目

现实世界的例子

我在 Lua 数组表中有一个带时间戳的条目数组。条目总是添加到数组的末尾(使用table.insert)。

local timestampedEvents = {}
function addEvent( data )
  table.insert( timestampedEvents, {getCurrentTime(),data} )
end

我需要偶尔(按顺序)浏览此表并处理并删除某些条目:

function processEventsBefore( timestamp )
  for i,stamp in ipairs( timestampedEvents ) do
    if stamp[1] <= timestamp then
      processEventData( stamp[2] )
      table.remove( timestampedEvents, i )
    end
  end
end

不幸的是,上述方法的代码中断了迭代,跳过了一些条目。有没有比手动遍历索引更好(打字更少,但仍然安全)的方法:

function processEventsBefore( timestamp )
  local i = 1
  while i <= #timestampedEvents do -- warning: do not cache the table length
    local stamp = timestampedEvents[i]
    if stamp[1] <= timestamp then
      processEventData( stamp[2] )
      table.remove( timestampedEvents, i )
    else
      i = i + 1
    end
  end
end

【问题讨论】:

  • 警告所有读者:table.remove() 函数应该被 Lua 禁止!向下滚动到我标记为“效率!”的答案。如果您想查看基准,显示table.remove() 是多么危险! ;-) 亲爱的读者:请忽略所有建议table.remove() 的答案。它应该从不用于任何事情

标签: lua


【解决方案1】:

遍历数组并在继续迭代的同时从中间删除随机项的一般情况

如果您从前到后迭代,当您删除元素 N 时,迭代中的下一个元素 (N+1) 将向下移动到该位置。如果您增加迭代变量(如 ipairs 所做的那样),您将跳过该元素。我们有两种方法可以解决这个问题。

使用此示例数据:

    input = { 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p' }
    remove = { f=true, g=true, j=true, n=true, o=true, p=true }

我们可以在迭代过程中删除input元素:

  1. 从后向前迭代。

    for i=#input,1,-1 do
        if remove[input[i]] then
            table.remove(input, i)
        end
    end
    
  2. 手动控制循环变量,因此我们可以在删除元素时跳过递增它:

    local i=1
    while i <= #input do
        if remove[input[i]] then
            table.remove(input, i)
        else
            i = i + 1
        end
    end
    

对于非数组表,您可以使用nextpairs(根据next 实现)进行迭代,并将要删除的项目设置为nil

请注意,table.remove 每次调用时都会移动所有后续元素,因此对于 N 次删除,性能是指数级的。如果您要删除很多元素,您应该像 LHF 或 Mitch 的回答那样自己移动这些项目。

【讨论】:

    【解决方案2】:

    效率!

    警告:不要使用 table.remove()。每次调用该函数以删除数组条目时,该函数都会重新索引所有后续(后续)数组索引。因此,在单次直通 OURSELVES 中“压缩/重新索引”表要快得多!

    最好的技术很简单:向上计数 (i) 通过所有数组条目,同时跟踪我们应该将下一个“保留”值放入 (j) 的位置。任何未保留的内容(或从i 移动到j)都设置为nil,这会告诉Lua 我们已经删除了该值。

    我正在分享这个,因为我真的不喜欢这个页面上的其他答案(截至 2018 年 10 月)。它们要么是错误的,要么是错误的,要么是过于简单的,要么是过于复杂的,而且大多数都是超慢的。所以我实现了一个高效、干净、超快的一次性算法。使用 SINGLE 循环。

    这是一个完整注释的示例(本文末尾有一个较短的非教程版本):

    function ArrayShow(t)
        for i=1,#t do
            print('total:'..#t, 'i:'..i, 'v:'..t[i]);
        end
    end
    
    function ArrayRemove(t, fnKeep)
        print('before:');
        ArrayShow(t);
        print('---');
        local j, n = 1, #t;
        for i=1,n do
            print('i:'..i, 'j:'..j);
            if (fnKeep(t, i, j)) then
                if (i ~= j) then
                    print('keeping:'..i, 'moving to:'..j);
                    -- Keep i's value, move it to j's pos.
                    t[j] = t[i];
                    t[i] = nil;
                else
                    -- Keep i's value, already at j's pos.
                    print('keeping:'..i, 'already at:'..j);
                end
                j = j + 1;
            else
                t[i] = nil;
            end
        end
        print('---');
        print('after:');
        ArrayShow(t);
        return t;
    end
    
    local t = {
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'
    };
    
    ArrayRemove(t, function(t, i, j)
        -- Return true to keep the value, or false to discard it.
        local v = t[i];
        return (v == 'a' or v == 'b' or v == 'f' or v == 'h');
    end);
    

    输出,沿途显示其逻辑,如何移动事物等...

    before:
    total:9 i:1 v:a
    total:9 i:2 v:b
    total:9 i:3 v:c
    total:9 i:4 v:d
    total:9 i:5 v:e
    total:9 i:6 v:f
    total:9 i:7 v:g
    total:9 i:8 v:h
    total:9 i:9 v:i
    ---
    i:1 j:1
    keeping:1   already at:1
    i:2 j:2
    keeping:2   already at:2
    i:3 j:3
    i:4 j:3
    i:5 j:3
    i:6 j:3
    keeping:6   moving to:3
    i:7 j:4
    i:8 j:4
    keeping:8   moving to:4
    i:9 j:5
    ---
    after:
    total:4 i:1 v:a
    total:4 i:2 v:b
    total:4 i:3 v:f
    total:4 i:4 v:h
    

    最后,这是在您自己的代码中使用的函数,没有所有的教程打印......并且只有几个最小的 cmets 来解释最终算法:

    function ArrayRemove(t, fnKeep)
        local j, n = 1, #t;
    
        for i=1,n do
            if (fnKeep(t, i, j)) then
                -- Move i's kept value to j's position, if it's not already there.
                if (i ~= j) then
                    t[j] = t[i];
                    t[i] = nil;
                end
                j = j + 1; -- Increment position of where we'll place the next kept value.
            else
                t[i] = nil;
            end
        end
    
        return t;
    end
    

    就是这样!

    如果你不想使用整个“可重用回调/函数”设计,你可以简单地将ArrayRemove()的内部代码复制到你的项目中,并将if (fnKeep(t, i, j)) then行更改为if (t[i] == 'deleteme') then ...这样你也摆脱了函数调用/回调的开销,而且速度更快!

    就我个人而言,我使用可重复使用的回调系统,因为它仍然比 table.remove() 快 100-1000 倍以上。

    奖励(高级用户):普通用户可以跳过阅读此奖励部分。它描述了如何同步多个相关表。请注意,fnKeep(t, i, j) 的第三个参数j 是一个额外参数,它允许您的保持函数知道该值的索引 将存储在 fnKeep 回答 true 时(以保持 值)。

    示例用法:假设您有两个“链接”表, 其中一个是table['Mitch'] = 1; table['Rick'] = 2;(哈希表 通过命名字符串进行快速数组索引查找),另一个是 array[{Mitch Data...}, {Rick Data...}](带有数字索引的数组, 其中 Mitch 的数据位于 pos 1 而 Rick 的数据位于 pos 2, 完全按照哈希表中的描述)。现在你决定循环 通过array 并删除Mitch Data,从而将Rick Data 从位置2 移动到位置1...

    然后您的fnKeep(t, i, j) 函数可以轻松地使用j 信息来更新哈希表 确保它们始终指向正确的数组偏移量的指针:

    local hData = {['Mitch'] = 1, ['Rick'] = 2};
    local aData = {
        {['name'] = 'Mitch', ['age'] = 33}, -- [1]
        {['name'] = 'Rick', ['age'] = 45}, -- [2]
    };
    
    ArrayRemove(aData, function(t, i, j)
        local v = t[i];
        if (v['name'] == 'Rick') then -- Keep "Rick".
            if (i ~= j) then -- i and j differing means its data offset will be moved if kept.
                hData[v['name']] = j; -- Point Rick's hash table entry at its new array location.
            end
            return true; -- Keep.
        else
            hData[v['name']] = nil; -- Delete this name from the lookup hash-table.
            return false; -- Remove from array.
        end
    end);
    

    从而从查找哈希表和数组中删除“Mitch”,并将“Rick”哈希表条目移动到指向 到1(这是j的值),其数组数据正在被移动 to(因为 i 和 j 不同,意味着数据正在被移动)。

    这种算法可以让您的相关表保持完美同步, 由于j,始终指向正确的数据位置 参数。

    对于有需要的人来说,这只是一个高级奖励 特征。大多数人可以简单地忽略他们的j 参数 fnKeep()函数!

    好了,伙计们!

    享受吧! :-)

    基准(又名“让我们开怀大笑……”)

    我决定将此算法与 99.9% 的 Lua 用户都在使用的标准“向后循环并使用 table.remove()”方法进行基准测试。

    为了做这个测试,我使用了下面的 test.lua 文件:https://pastebin.com/aCAdNXVh

    每个被测试的算法都有 10 个测试数组,每个数组包含 200 万个项目(每个算法测试总共包含 2000 万个项目)。所有数组中的项目都是相同的(以确保测试的公平性):每5个项目是数字“13”(将被删除),所有其他项目都是数字“100”(将被保留)。

    嗯...我的ArrayRemove() 算法的测试在 2.8 秒内结束(处理 2000 万个项目)。我现在正在等待table.remove() 测试完成......到目前为止已经过了几分钟,我越来越无聊......更新:还在等待......更新:我饿了..更新:你好……今天?!更新:Zzz...更新:还在等待...更新:............更新:好的,table.remove() 代码(这是大多数 Lua 用户正在使用的方法)正在运行需要几天。我会在它完成的那一天更新。

    自我说明:我于 2018 年 11 月 1 日格林威治标准时间 ~04:55 开始运行测试。我的 ArrayRemove() 算法在 2.8 秒内完成...内置 Lua table.remove() 算法仍在运行现在......我稍后会更新这篇文章......;-)

    更新:现在是 2018 年 11 月 1 日格林威治标准时间 14:55,table.remove() 算法仍未完成。我将中止那部分测试,因为过去 10 小时 Lua 一直在使用我的 CPU 的 100%,我现在需要我的计算机。而且它的温度足以在笔记本电脑的铝制外壳上煮咖啡......

    结果如下:

    • 处理 10 个数组,包含 200 万个项目(总共 2000 万个项目):
    • 我的ArrayRemove() 函数:2.8 秒。
    • 普通 Lua table.remove():我决定在 Lua 使用 100% CPU 10 小时退出测试。因为我现在需要使用我的笔记本电脑! ;-)

    这是我按下 Ctrl-C 时的堆栈跟踪...确认了我的 CPU 在过去 10 小时内一直在处理什么 Lua 函数,哈哈:

    [     mitch] elapsed time: 2.802
    
    ^Clua: test.lua:4: interrupted!
    stack traceback:
        [C]: in function 'table.remove'
        test.lua:4: in function 'test_tableremove'
        test.lua:43: in function 'time_func'
        test.lua:50: in main chunk
        [C]: in ?
    

    如果我让table.remove() 测试运行完成,可能需要几天时间...欢迎任何不介意浪费大量电力的人重新运行此测试(文件位于上方pastebin) 并让我们都知道花了多长时间。

    为什么table.remove() 这么慢?仅仅是因为每个对该函数的调用都必须重复重新索引 每个存在于我们之后的表项告诉它删除!因此,要删除 200 万项数组中的第一项,它必须将所有其他 200 万项的索引向下移动 1 个插槽以填补删除造成的空白。然后......当你移除另一个项目时......它必须再次移动所有其他 200 万个项目......它一遍又一遍地这样做......

    你不应该,永远使用table.remove()!它的性能损失迅速增长。下面是一个较小数组大小的示例,以证明这一点:

    • 10 个包含 1,000 项的数组(总共 10k 项):ArrayRemove():0.001 秒,table.remove():0.018 秒(慢 18 倍)。
    • 10 个数组,包含 10,000 个项目(总共 100k 个项目):ArrayRemove():0.014 秒,table.remove():1.573 秒(慢 112.4 倍)。
    • 100,000 个项目的 10 个数组(总共 100 万个项目):ArrayRemove():0.142 秒,table.remove():3 分 48 秒(慢 1605.6 倍)。
    • 10 个包含 2,000,000 个项目的数组(总共 2000 万个项目):ArrayRemove():2.802 秒,table.remove():我决定在 10 小时后中止测试,所以我们可能永远不会知道需要多长时间。 ;-) 但是在当前时间点(甚至还没有完成),它比ArrayRemove() 花费了 12847.9 倍...但是最终的table.remove() 结果,如果我让它完成,可能会在 30-40 千次左右慢一点。

    如您所见,table.remove() 的时间增长不是线性的(因为如果是这样,那么我们的 100 万项测试将只花费 10 万(100k)项测试的 10 倍,但相反,我们见 1.573 秒与 3 分 48 秒!)。因此,我们不能进行较低的测试(例如 10k 个项目)并简单地将其乘以 1000 万 个项目,以知道我中止的测试将花费多长时间 ...因此,如果有人真的对最终结果感到好奇,您必须自己运行测试并在 table.remove() 完成几天后发表评论......

    但是在这一点上,我们可以做的是,根据我们迄今为止的基准,说这些话:F-ck table.remove()! ;-)

    没有理由调用该函数。 永远。因为如果你想从表中删除项目,只需使用t['something'] = nil;。如果要从数组(具有数字索引的表)中删除项目,请使用 ArrayRemove()

    顺便说一句,上面的测试都是使用Lua 5.3.4 执行的,因为这是大多数人使用的标准运行时。我决定使用LuaJIT 2.0.5 (JIT: ON CMOV SSE2 SSE3 SSE4.1 fold cse dce fwd dse narrow loop abc sink fuse) 快速运行主要的“2000 万项”测试,它比标准 Lua 运行时更快。 ArrayRemove() 的 2000 万个项目的结果是:Lua 中为 2.802 秒,LuaJIT 中为 0.092 秒。这意味着如果您的代码/项目在 LuaJIT 上运行,您可以期待我的算法更快的性能! :-)

    我还使用 LuaJIT 最后一次重新运行了“100k 个项目”测试,以便我们可以看到 table.remove() 在 LuaJIT 中的表现如何,看看它是否比常规 Lua 更好:

    • [LUAJIT] 100,000 项的 10 个数组(总共 1m 项):ArrayRemove():0.005 秒,table.remove():20.783 秒(比 ArrayRemove() 慢 4156.6 倍...但是这个 LuaJIT 结果是实际上比普通 Lua 的 WORSE 比率,其 table.remove() 比我的算法“仅”1605.6x 在相同的测试中......所以如果你使用的是 LuaJIT ,性能比甚至更多有利于我的算法!)

    最后,您可能想知道“如果我们只想删除 一个 项,table.remove() 会更快吗,因为它是一个原生函数?”。如果您使用 LuaJIT,那么该问题的答案是:不。在 LuaJIT 中,ArrayRemove()table.remove() 更快,即使删除 ONE ITEM。不是 使用 LuaJIT?使用 LuaJIT,与常规 Lua 相比,所有 Lua 代码的速度都轻松提高了 30 倍左右。结果如下:[mitch] elapsed time (deleting 1 items): 0.008, [table.remove] elapsed time (deleting 1 items): 0.011。这是“仅删除 1-6 项”测试的 pastebin:https://pastebin.com/wfM7cXtU(文件末尾列出了完整的测试结果)。

    TL;DR:不要使用table.remove()anywhere,出于任何任何原因!

    希望大家喜欢ArrayRemove()...并玩得开心! :-)

    【讨论】:

    • 评论不用于扩展讨论;这个对话是moved to chat
    • 当心。这不是迭代和删除表的好方法。它适用于数字序列数组。非序列数组将失败,任何其他类型的表哈希也会失败 - 通过使用它来找出困难的方法。我在一个有序数组上运行这个函数并与 table.remove 进行比较——它在 luajit 中是相同的。这是在处理 300MB 文件时进行的超过 700 万次调用。这里的说法有点过头了,所以请注意,我在这里发表评论,以免其他人在这方面浪费太多时间。先测试一下。
    • @DavidLannan 不是仅称为表的非数字序列数组吗?
    • @younyokel 是的。两者都是桌子。我想明确用法(作为数组)。
    • @DavidLannan 请原谅我这么说,但是你浪费了自己的时间,因为你只是误解了 Lua ARRAYS 是什么,所以你可以不要抱怨我的函数完全按照它应该做的。 你说“警告:这个函数只适用于数字索引 Lua 数组”。好吧,先生,这正是 Lua ARRAY 的含义。请参阅 Lua 文档:lua.org/pil/11.1.html(数组:仅限数字键,并且所有键必须是顺序的(无间隙))和 lua.org/pil/2.5.html (表格:可以使用任何键,包括数字,并且允许数字中有空格。)
    【解决方案3】:

    我会避免 table.remove 并在将不需要的条目设置为 nil 后遍历数组,然后在必要时再次遍历数组以压缩它。

    这是我想到的代码,使用 Mud 回答中的示例:

    local input = { 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p' }
    local remove = { f=true, g=true, j=true, n=true, o=true, p=true }
    
    local n=#input
    
    for i=1,n do
            if remove[input[i]] then
                    input[i]=nil
            end
    end
    
    local j=0
    for i=1,n do
            if input[i]~=nil then
                    j=j+1
                    input[j]=input[i]
            end
    end
    for i=j+1,n do
            input[i]=nil
    end
    

    【讨论】:

    • 你能提供任何避免table.remove的理由,或者使用它的后果吗?
    • @Phrogz 因为它会在只需要一个时多次移动项目。例如,如果您删除数组中的所有其他条目,那么如果您使用 table.remove,则会执行 二次 次移动。
    • 一个可以做得更好:第一个循环中的几行使第二个 (j+1 to n) 过时了。它最多可以保存N个步骤。 (考虑在第一个循环之后数组包含除最后一个元素之外的所有 nil 的情况)。关键是不仅要写入 input[j],还要在 i ~= j 时将其与 input[i] 交换
    • 这个答案很糟糕。 它遍历数组一次,将所有“坏”条目设置为零。然后它再次循环遍历数组,将所有不需要的条目从i 移动到j(如果ij 相等,则将数组项复制到自身中会浪费时间......呃)。然后它循环最后一次,将所有剩余的条目设置为零。这一切都比table.remove()(最慢)要好,但是这个答案仍然是三个循环,并且是一种缓慢、浪费和非常愚蠢的方法,没有人应该使用!相反,向下滚动到我的答案标记为“效率!”。
    • 对于任何转录此代码的人来说,这是一个提醒:您必须在顶部捕获数组的长度(即,您不能只在底部的 for 循环中抓取它)。数组只是 Lua 中的表,因此它们不存储长度元数据。相反,长度意味着“从 1 开始检查每个索引,返回 nil 的第一个告诉你长度”。当您在第一个 for 循环中将中间的任意元素设置为 nil 时,这会导致问题。
    【解决方案4】:

    试试这个功能:

    function ripairs(t)
        -- Try not to use break when using this function;
        -- it may cause the array to be left with empty slots
        local ci = 0
        local remove = function()
            t[ci] = nil
        end
        return function(t, i)
            --print("I", table.concat(array, ','))
            i = i+1
            ci = i
            local v = t[i]
            if v == nil then
                local rj = 0
                for ri = 1, i-1 do
                    if t[ri] ~= nil then
                        rj = rj+1
                        t[rj] = t[ri]
                        --print("R", table.concat(array, ','))
                    end
                end
                for ri = rj+1, i do
                    t[ri] = nil
                end
                return
            end
            return i, v, remove
        end, t, ci
    end
    

    它不使用table.remove,因此它应该具有O(N) 复杂性。您可以将 remove 函数移动到 for 生成器中,以消除对上值的需要,但这意味着每个元素都有一个新的闭包......这不是一个实际问题。

    示例用法:

    function math.isprime(n)
        for i = 2, n^(1/2) do
            if (n % i) == 0 then
                return false
            end
        end
        return true
    end
    
    array = {}
    for i = 1, 500 do array[i] = i+10 end
    print("S", table.concat(array, ','))
    for i, v, remove in ripairs(array) do
        if not math.isprime(v) then
            remove()
        end
    end
    print("E", table.concat(array, ','))
    

    注意不要使用break(或者过早退出循环),因为它会留下带有nil元素的数组。

    如果您希望 break 表示“中止”(如,没有删除任何内容),您可以这样做:

    function rtipairs(t, skip_marked)
        local ci = 0
        local tbr = {} -- "to be removed"
        local remove = function(i)
            tbr[i or ci] = true
        end
        return function(t, i)
            --print("I", table.concat(array, ','))
            local v
            repeat
                i = i+1
                v = t[i]
            until not v or not (skip_marked and tbr[i])
            ci = i
            if v == nil then
                local rj = 0
                for ri = 1, i-1 do
                    if not tbr[ri] then
                        rj = rj+1
                        t[rj] = t[ri]
                        --print("R", table.concat(array, ','))
                    end
                end
                for ri = rj+1, i do
                    t[ri] = nil
                end
                return
            end
            return i, v, remove
        end, t, ci
    end
    

    这样做的好处是能够取消整个循环而不删除任何元素,并提供跳过已标记为“要删除”的元素的选项。缺点是新表的开销。

    希望这些对你有帮助。

    【讨论】:

    • 那是不必要的复杂和意大利面条代码,它使用浪费的额外表格等......相反,向下滚动到我的答案标记为“效率!”。
    【解决方案5】:

    您可以考虑使用priority queue 代替排序数组。 当您按顺序删除条目时,优先级队列将有效地压缩自身。

    有关优先级队列实现的示例,请参阅此邮件列表线程:http://lua-users.org/lists/lua-l/2007-07/msg00482.html

    【讨论】:

      【解决方案6】:

      简单..

      values = {'a', 'b', 'c', 'd', 'e', 'f'}
      rem_key = {}
      
      for i,v in pairs(values) do
      if remove_value() then
      table.insert(rem_key, i)
      end
      end
      
      for i,v in pairs(rem_key) do
      table.remove(values, v)
      end
      

      【讨论】:

      • 虽然我很欣赏以性能为中心的答案,但很高兴看到有人真正回答了这个问题......
      【解决方案7】:

      出于性能原因(这可能或多或少与您的特定情况相关),我建议不要使用 table.remove

      对我来说,这种类型的循环通常是这样的:

      local mylist_size = #mylist
      local i = 1
      while i <= mylist_size do
          local value = mylist[i]
          if value == 123 then
              mylist[i] = mylist[mylist_size]
              mylist[mylist_size] = nil
              mylist_size = mylist_size - 1
          else
              i = i + 1
          end
      end
      

      注意这很快,但有两个警告:

      • 如果您需要删除相对较少的元素,它会更快。 (它实际上对应该保留的元素不起作用)。
      • 它将使数组未排序。有时您并不关心排序后的数组,在这种情况下,这是一个有用的“捷径”。

      如果您想保留元素的顺序,或者如果您希望不保留大部分元素,请查看 Mitch 的解决方案。这是我和他的粗略比较。我在https://www.lua.org/cgi-bin/demo 上运行它,大多数结果与此类似:

      [    srekel] elapsed time: 0.020
      [     mitch] elapsed time: 0.040
      [    srekel] elapsed time: 0.020
      [     mitch] elapsed time: 0.040
      

      当然,请记住,这取决于您的特定数据。

      下面是测试代码:

      function test_srekel(mylist)
          local mylist_size = #mylist
          local i = 1
          while i <= mylist_size do
              local value = mylist[i]
              if value == 13 then
                  mylist[i] = mylist[mylist_size]
                  mylist[mylist_size] = nil
                  mylist_size = mylist_size - 1
              else
                  i = i + 1
              end
          end
      
      end -- func
      
      function test_mitch(mylist)
          local j, n = 1, #mylist;
      
          for i=1,n do
              local value = mylist[i]
              if value ~= 13 then
                  -- Move i's kept value to j's position, if it's not already there.
                  if (i ~= j) then
                      mylist[j] = mylist[i];
                      mylist[i] = nil;
                  end
                  j = j + 1; -- Increment position of where we'll place the next kept value.
              else
                  mylist[i] = nil;
              end
          end
      end
      
      function build_tables()
          local tables = {}
          for i=1, 10 do
            tables[i] = {}
            for j=1, 100000 do
              tables[i][j] = j % 15373
            end
          end
      
          return tables
      end
      
      function time_func(func, name)
          local tables = build_tables()
          time0 = os.clock()
          for i=1, #tables do
              func(tables[i])
          end
          time1 = os.clock()
          print(string.format("[%10s] elapsed time: %.3f\n", name, time1 - time0))
      end
      
      time_func(test_srekel, "srekel")
      time_func(test_mitch, "mitch")
      time_func(test_srekel, "srekel")
      time_func(test_mitch, "mitch")
      

      【讨论】:

      • Lua.org 的演示页面不是基准测试工具。 它速度慢而且超载!我在LuaJIT 2.0.5 上本地运行了您的基准代码。结果:$ luajit test.lua, [srekel] elapsed time: 0.004, [mitch] elapsed time: 0.004, [srekel] elapsed time: 0.003, [mitch] elapsed time: 0.004...如果我将每个数组的项目数从 100k 提高到 100 万(for j=1, 1000000 do),以使测试运行足够长的时间来测量任何差异,我得到以下结果:$ luajit test2.lua, [srekel] elapsed time: 0.038, [mitch] elapsed time: 0.041, [srekel] elapsed time: 0.033, [mitch] elapsed time: 0.041 ...
      • 此外,您的算法在实际删除某些内容时会进行艰苦的工作(将数据复制到不同的单元格),而在不删除时(只是增加循环计数器)则工作很少。您用来确定何时通过j % 15373 将数字“13”随机放入数组(将被删除的数字)的常量仅生成 7 个将被删除的索引...: -O 所以在这个测试中只删除了 7 个项目。这主要是从1 循环到#n 的测试!无论哪种方式,在不需要排序顺序的算法上都做了很好且有趣的工作!
      • 你是对的,当你要删除的元素很少时,我的速度更快,而当你要删除的元素很多时,你的速度更快。我现在在本地运行测试并尝试了一些不同的配置。对我来说,截止点约为 20-25%,即如果您希望移除大约五分之一或更少的元素,那么我的速度更快。但这个值只是一个估计值,当然会因情况而异。
      【解决方案8】:

      这基本上是在以非功能性风格重述其他解决方案;我发现这更容易理解(也更难出错):

      for i=#array,1,-1 do
        local element=array[i]
        local remove = false
        -- your code here
        if remove then
          array[i] = array[#array]
          array[#array] = nil
        end
      end
      

      【讨论】:

      • 这个答案不起作用!起初这似乎是一个简单的解决方案,但请看以下示例:使用数组 {"A", "B", "C", "D"} 在尝试删除元素 A 和 D 时,此函数错误地输出 {"C", "B"} 而不是 {"B", "C"}。我花了一段时间才弄清楚这个错误隐藏在哪里......
      【解决方案9】:

      您可以使用仿函数来检查需要删除的元素。额外的好处是它在 O(n) 内完成,因为它不使用 table.remove

      function table.iremove_if(t, f)
          local j = 0
          local i = 0
          while (i <= #f) do
              if (f(i, t[i])) then
                  j = j + 1
              else
                  i = i + 1
              end
              if (j > 0) then
                  local ij = i + j
                  if (ij > #f) then
                      t[i] = nil
                  else
                      t[i] = t[ij]
                  end
              end
          end
          return j > 0 and j or nil -- The number of deleted items, nil if 0
      end
      

      用法:

      table.iremove_if(myList, function(i,v) return v.name == name end)
      

      在你的情况下:

      table.iremove_if(timestampedEvents, function(_,stamp)
          if (stamp[1] <= timestamp) then
              processEventData(stamp[2])
              return true
          end
      end)
      

      【讨论】:

        【解决方案10】:

        我突然想到——对于我的特殊情况,我只从队列的前面移动条目——我可以通过以下方式更简单地做到这一点:

        function processEventsBefore( timestamp )
          while timestampedEvents[1] and timestampedEvents[1][1] <= timestamp do
            processEventData( timestampedEvents[1][2] )
            table.remove( timestampedEvents, 1 )
          end
        end
        

        但是,我不会接受这个作为答案,因为它不能处理迭代数组并在继续迭代的同时从中间删除随机项的一般情况。

        【讨论】:

        • 如果您的问题确实是关于“迭代数组并在继续迭代的同时从中间删除随机项的一般情况”,那么您应该将您的问题修改为关于该一般情况。因为您所说的问题实际上是关于您刚刚解决的具体案例。
        • @NicolBolas 我已经编辑了这个问题,以明确这是我正在寻找答案的一般情况。
        【解决方案11】:

        首先,肯定阅读@MitchMcCabers 详细说明table.remove() 的邪恶的帖子。

        现在我不是 lua 高手,但我尝试将他的方法与 @MartinRudat 的方法结合起来,使用从 @PiFace’s answer here. 修改的数组检测方法的辅助

        根据我的测试,结果成功地从键值表或数组中删除了一个元素。

        我希望它是正确的,到目前为止它对我有用!

        --helper function needed for remove(...)
        --I’m not super able to explain it, check the link above
        function isarray(tableT)
            for k, v in pairs(tableT) do
                if tonumber(k) ~= nil and k ~= #tableT then
                    if tableT[k+1] ~= k+1 then
                        return false
                    end
                end
            end
            return #tableT > 0 and next(tableT, #tableT) == nil
         end
        
        function remove(targetTable, removeMe)
        --check if this is an array
        if isarray(targetTable) then
            --flag for when a table needs to squish in to fill cleared space
            local shouldMoveDown = false
            --iterate over table in order
            for i = 1, #targetTable do
                --check if the value is found
                if targetTable[i] == removeMe then
                    --if so, set flag to start collapsing the table to write over it
                    shouldMoveDown = true
                end
                --if collapsing needs to happen...
                if shouldMoveDown then
                    --check if we're not at the end
                    if i ~= #targetTable then
                        --if not, copy the next value over this one
                        targetTable[i] = targetTable[i+1]
                    else
                        --if so, delete the last value
                        targetTable[#targetTable] = nil
                    end 
                end
            end
        else
            --loop over elements
            for k, v in pairs(targetTable) do
                --check for thing to remove
                if (v == removeMe) then
                    --if found, nil it
                    targetTable[k] = nil
                    break
                end
            end
        end
        return targetTable, removeMe;
        

        结束

        【讨论】:

          【解决方案12】:

          效率!更! )

          关于米奇的变种。它有一些浪费分配到 nil,这里是具有相同想法的优化版本:

          function ArrayRemove(t, fnKeep)
              local j, n = 1, #t;
              for i=1,n do
                  if (fnKeep(t, i, j)) then
                      -- Move i's kept value to j's position, if it's not already there.
                      if (i ~= j) then
                          t[j] = t[i];
                      end
                      j = j + 1; -- Increment position of where we'll place the next kept value.
                  end
              end
              table.move(t,n+1,n+n-j+1,j);
              --for i=j,n do t[i]=nil end
              return t;
          end
          

          还有更优化的方块移动版本

          对于更大的数组和更大的保留块

          function ArrayRemove(t, fnKeep)
              local i, j, n = 1, 1, #t;
              while i <= n do
                  if (fnKeep(t, i, j)) then
                      local k = i
                      repeat
                          i = i + 1;
                      until i>n or not fnKeep(t, i, j+i-k)
                      --if (k ~= j) then
                          table.move(t,k,i-1,j);
                      --end
                      j = j + i - k;
                  end
                  i = i + 1;
              end
              table.move(t,n+1,n+n-j+1,j);
              return t;
          end
          

          if (k ~= j) 不需要,因为它执行了很多次,但在第一次删除后为“true”。我认为 table.move() 无论如何都会处理索引检查。
          table.move(t,n+1,n+n-j+1,j) 相当于“for i=j,n do t[i] =nil end"。
          我是 lua 新手,不知道哪里有高效的值复制功能。在这里我们将复制 nil n-j+1 次。

          关于 table.remove()。我认为它应该利用 table.move() 在一个操作中移动元素。 C 中的一种 memcpy。所以也许它毕竟不是那么糟糕。
          @MitchMcMabers,你能更新你的基准吗?你使用 lua >= 5.3 吗?

          【讨论】:

            猜你喜欢
            • 2014-10-26
            • 2011-09-04
            • 2020-05-27
            • 2022-05-17
            • 2016-05-08
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多