在迭代数组表时安全地删除项目

49

这个问题类似于如何在删除键时安全地迭代Lua表,但又有所不同。

概述

给定一个 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

1
警告所有读者:table.remove()函数应该被禁止在Lua中使用!如果您想查看基准测试,展示了table.remove()的危险性,请向下滚动到我的标记为“效率!”的答案。亲爱的读者们:请忽略所有建议使用table.remove()的答案。它永远不应该用于任何事情! - Mitch McMabers
只需阅读以下内容以节省您在上面的评论中的时间:https://dev59.com/Y1rUa4cB1Zd3GeqPj3TA - VimNing
12个回答

52
遍历数组并在继续遍历的同时从中间随机删除项目的一般情况。
如果您按顺序从前到后遍历,当您删除第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 }

我们可以通过以下方式在迭代过程中移除元素:
  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个删除操作,性能是二次的(O(N^2))。如果您要删除大量元素,应该像LHF或Mitch的回答中那样自己移动这些项。

性能绝对不是指数级的。你会期望单个调用的性能为O(N),而N次删除的性能为O(N²)。 - undefined
你是对的。我以为N²被称为指数是因为指数的原因。已修正。 - undefined

32

效率!

警告:不要使用table.remove()。每次调用它删除数组条目时,该函数会导致所有后续(以下)数组索引重新索引。因此,我们最好在单个遍历中自己“压缩/重新索引”表格!

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

我分享这篇文章,因为我真的不喜欢本页上的其他答案(截至2018年10月)。它们要么错误,有bug,过于简单或过于复杂,并且大多数都超级慢。因此,我实现了一种高效,干净,超快速的单次遍历算法。只需一个循环。

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

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

最后,这是可以在你自己的代码中使用的函数,没有所有的教程打印...并且只有一些最小的注释来解释最终的算法:

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倍。

Bonus (Advanced Users): Regular users can skip reading this bonus section. It describes how to sync multiple related tables. Note that the 3rd parameter to fnKeep(t, i, j), the j, is a bonus parameter which allows your keep-function to know what index the value will be stored at whenever fnKeep answers true (to keep that value).

Example usage: Let's say you have two "linked" tables, where one is table['Mitch'] = 1; table['Rick'] = 2; (a hash-table for quick array index lookups via named strings) and the other is array[{Mitch Data...}, {Rick Data...}] (an array with numerical indices, where Mitch's data is at pos 1 and Rick's data is at pos 2, exactly as described in the hash-table). Now you decide to loop through the array and remove Mitch Data, which thereby moves Rick Data from position 2 to position 1 instead...

Your fnKeep(t, i, j) function can then easily use the j info to update the hash-table pointers to ensure they always point at the correct array offsets:

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);

Thereby removing 'Mitch' from both the lookup hash-table and the array, and moving the 'Rick' hash-table entry to point to 1 (that's the value of j) where its array data is being moved to (since i and j differed, meaning the data was being moved).

This kind of algorithm allows your related tables to stay in perfect sync, always pointing at the correct data position thanks to the j parameter.

It's just an advanced bonus for those who need that feature. Most people can simply ignore the j parameter in their fnKeep() functions!

好的,就这些了,朋友们,祝你们愉快! :-)

基准测试(也称“让我们开怀大笑...”)

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

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

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

我的ArrayRemove()算法测试结果为2.8秒(处理2000万个项目)。现在我正在等待table.remove()测试完成...到目前为止已经过去了几分钟,我有点无聊......更新:还在等待...更新:我饿了...更新:今天?!更新:Zzz...更新:还在等待...更新:............更新:好吧,内置的Lua table.remove()代码(即大多数Lua用户使用的方法)需要几天时间。我会在它完成的那一天更新。

自述:我开始在2018年11月1日GMT 04:55左右运行测试。我的ArrayRemove()算法花费了2.8秒...内置的Lua table.remove()算法到目前为止仍在运行...稍后我会更新这篇文章...;-)

更新:现在是2018年11月1日GMT 14:55,table.remove()算法仍未完成。我将中止测试的这一部分,因为Lua已经占用了我的CPU100%超过10个小时,而且我现在需要使用我的电脑。而且我的笔记本电脑的铝外壳足够热可以泡咖啡了...

这是结果:

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

当我按下Ctrl-C时,这是堆栈跟踪...它确认了Lua函数在过去的10个小时中一直在工作,哈哈:

[     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万项数组中的第一项,它必须将其他2百万项的索引向下移动一个槽位以填补由删除引起的间隙。然后……当您删除另一个项目时……它必须再次移动所有其他2百万个项目……如此反复进行……
您永远不应该,绝对不能使用table.remove()!其性能惩罚会迅速增加。以下是具有较小数组大小的示例,以证明这一点:
  • 10个1,000项的数组(总共10,000项):ArrayRemove():0.001秒,table.remove():0.018秒(18倍慢)。
  • 10个10,000项的数组(总共100,000项):ArrayRemove():0.014秒,table.remove():1.573秒(112.4倍慢)。
  • 10个100,000项的数组(总共1m项):ArrayRemove():0.142秒,table.remove():3分钟48秒(1605.6倍慢)。
  • 10个2,000,000项的数组(总共20m项):ArrayRemove():2.802秒,table.remove():我在10小时后决定放弃测试,所以我们可能永远不知道需要多长时间。;-) 但是,在当前时间点(甚至没有完成),它比ArrayRemove()慢了12847.9倍……但是最终的table.remove()结果,如果让它完成,可能会慢30-40倍。
正如你所看到的,table.remove()的增长并不是线性的(因为如果是线性的话,那么我们的100万项测试只会比0.1万(10万)项测试多花费10倍的时间,但实际上我们看到的是1.573秒对比3分48秒!)。因此,我们不能通过一个较低的测试(例如10k项)并将其乘以 10百万 项来知道我中途放弃的测试需要花费多长时间......因此,如果有人真正好奇最终结果,您将需要运行测试并在几天后发布评论,当table.remove()完成时......

但是,我们现在可以做的是,根据我们迄今为止的基准测试,说table.remove()很差!;-)

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

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

我还使用LuaJIT最后再次运行了“10万项”测试,以便我们可以看到table.remove()在LuaJIT中的执行情况,并查看它是否比常规Lua好:

  • [LUAJIT] 10个包含100,000项(总计1m项)的数组:ArrayRemove():0.005秒,table.remove():20.783秒(比ArrayRemove()慢4156.6倍......但是这个LuaJIT结果实际上是一个更差的比率,因为对于相同的测试,常规Lua的table.remove()只比我的算法慢1605.6倍......因此,如果您使用LuaJIT,则性能比率更加倾向于我的算法!)
最后,你可能会想:“如果我们只想删除一个项目,table.remove()是否会更快,因为它是原生函数?” 如果使用LuaJIT,则该问题的答案是:否。在LuaJIT中,即使要删除一个项目,ArrayRemove()也比table.remove()更快。而且,谁不使用LuaJIT呢?对于所有Lua代码,LuaJIT可以轻松加速约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(完整的测试结果在文件末尾列出)。
总之,无论任何原因都不要在任何地方使用table.remove()
希望你喜欢ArrayRemove()...祝大家玩得开心! :-)

1
注意。这不是迭代和删除表的好方法。它仅适用于数字序列数组。非序列数组将失败,任何其他类型的表哈希也将失败 - 我通过使用它发现了这一点。 我在一个序列数组上运行了这个函数,并与table.remove进行了比较 - 在luajit中它是相同的。这是在处理一个300MB文件时进行了超过700万次调用。 这里的声明有些夸张,所以请注意,我在这里发表评论,以便其他人不会在这上浪费太多时间。先测试一下。 - David Lannan
1
@DavidLannan 非数字序列数组不就是被称为表格吗? - younyokel
1
@DavidLannan 对不起直言,但是您浪费了自己的时间,因为您简单地误解了Lua中的数组,所以您不能抱怨我的函数完全按照它应该做的。您说“警告:此函数仅适用于数值索引的Lua数组”。那么,先生,这正是Lua数组的定义。请参阅Lua文档:https://www.lua.org/pil/11.1.html(数组:仅限数字键,并且所有键必须连续(没有间隙)),以及https://www.lua.org/pil/2.5.html(表:可以使用任何键,包括数字,数字之间的间隙也是允许的)。 - Mitch McMabers
1
@DavidLannan 话虽如此,我完全赞成您帮助澄清这一点,以便新手了解这个 ArrayRemove() 函数是为 LUA ARRAYS 设计的,而不是 lua tables。 :-) 具有非数字键的表格是表格。 具有数字键但存在间隙的表格是表格。 具有按顺序排列的数字键(没有间隙!)的“表格”是数组。 希望这能澄清问题。否则,请查看我在先前评论中链接到的Lua文档。 :-) - Mitch McMabers
@DavidLannan 我想我最后再提一件事情,以便更加清楚:带有间隔(非连续数字)的数字索引表格不是数组,并且也将被Lua自己内置的处理数组的库函数拒绝。Lua自己的函数从索引1开始计数,一旦它们遇到一个nil(缺失)数字键,它们就会停止,因为它们已经到达了“数组”的末尾。因此,具有键[1] [2] [4]的表格不是数组,Lua的数组函数将处理[1]和[2],然后停止,因为它们已经到达了顺序键的末尾! - Mitch McMabers
显示剩余5条评论

21

我会避免使用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
15
因为它会在只需要移动一个项目时多次移动项目。例如,如果您从数组中删除每个其他条目,则如果使用table.remove,将执行二次方数量的移动操作。 - lhf
1
甚至可以做得更好:第一个循环中的几行代码使第二个循环(从j+1到n)变得过时。这可以节省多达N步。(考虑一下第一个循环后数组只包含最后一个元素以外的所有nil的情况)。关键是不仅要写入input[j],而且如果i ~= j,则将其与input[i]交换。 - garek
这里向任何转录此代码的人提供提示:您必须在顶部获取数组的长度(即您不能仅在底部for循环中获取它)。在Lua中,数组只是表格,因此它们不存储长度的元数据。相反,长度意味着“检查从1开始的每个索引,并且第一个返回nil的索引告诉您长度”。当您在第一个for循环中将中间的任意元素设置为nil时,这会导致问题。 - tennessee_jed

5

试试这个函数:

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-生成器中,以消除对于 upvalue 的需求,但这意味着每个元素都需要一个新的闭包...这并不是一个实际问题。
示例用法:
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

这样做的优点是可以取消整个循环而不删除任何元素,并提供跳过已标记为“待删除”的元素的选项。缺点是会产生一个新表的开销。

希望这些对你有所帮助。


2

出于性能原因(这可能与您的特定情况更或少相关),我建议不要使用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]经过时间:0.004,[mitch]经过时间:0.004,[srekel]经过时间:0.003,[mitch]经过时间:0.004...如果我将每个数组的项目数从100k提高到100万(for j=1, 1000000 do),以使测试运行足够长的时间来测量任何差异,我得到以下结果:$ luajit test2.lua,[srekel]经过时间:0.038,[mitch]经过时间:0.041,[srekel]经过时间:0.033,[mitch]经过时间:0.041... - Mitch McMabers
此外,您的算法在实际删除某些内容时会执行其繁重的工作(将数据复制到不同的单元格),而在不删除任何内容时几乎不会执行任何工作(只是增加循环计数器)。并且您用于确定何时随机放置数字“13”到数组中的常量(这是将被删除的数字),通过 j%15373 仅生成7个将被删除的索引... :-O 因此,在此测试中仅删除7个项目。这主要是从“1”到“#n”的循环测试!无论如何,当不需要排序时,您的算法非常好和有趣! - Mitch McMabers
1
你说得对,当你要删除的元素很少时,我的速度更快;而当你想要删除许多项时,你的速度更快。我现在在本地运行了测试,并尝试了几个不同的配置。对我来说,临界点大约是20-25%,即如果您希望删除约五分之一或更少的元素,则我的速度更快。但是这个值只是一个估计值,当然会因情况而异。 - Srekel

2

2

Simple..

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

1
虽然我欣赏那些注重性能的回答,但很高兴看到有人实际上回答了这个问题... - nathanfranke
答案是错误的。应该按相反的顺序删除要删除的索引:table.insert(rem_key, 1, i) - jiyongdong

1
您可以使用函数对象来检查需要删除的元素。另一个好处是它在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)

1

效率!更高效!

关于Mitch的变体。它有一些将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吗?


1
这基本上是以非功能风格重申其他解决方案;我发现这样更易于理解(也更难出错):
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

1
这个答案不起作用!一开始看起来似乎是一个简单的解决方案,但是看看下面的例子:使用数组{"A", "B", "C", "D"}尝试删除元素A和D时,此函数错误地输出了{"C", "B"}而不是{"B", "C"}。花了我一段时间才找出bug藏在哪里... - jonasstr

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