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

时间:2012-09-12 19:18:50

标签: lua

此问题与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

11 个答案:

答案 0 :(得分:40)

  

迭代数组并从中间删除随机项而继续迭代的一般情况

如果你从前到后迭代,当你删除元素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
    
  3. 对于非数组表,您使用nextpairs(根据next实现)进行迭代,并将要删除的项目设置为nil。< / p>

    请注意table.remove每次调用后都会移动所有后续元素,因此N删除的性能是指数级的。如果您要删除大量元素,则应自行移动项目,如LHF或Mitch的答案。

答案 1 :(得分: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

答案 2 :(得分:5)

效率!

警告:请勿使用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)) thenif (t[i] == 'deleteme') then ...这样一来,您还可以摆脱函数调用/回调的开销,并进一步提高速度!

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

  

奖励(高级用户):普通用户可以跳过阅读此奖励部分。它描述了如何同步多个相关表。请注意,fnKeep(t, i, j)的第三个参数j是一个奖励参数,它使您的keep函数可以知道该值的索引   将存储在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的值)将其数组数据移动到的位置   到(因为i和j不同,这意味着数据正在移动)。

     

这种算法可使您的相关表保持完美同步,   j总是指向正确的数据位置   参数。

     

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

好吧,伙计们!

享受! :-)

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

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

要执行此测试,我使用了以下test.lua文件:https://pastebin.com/aCAdNXVh

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

嗯...我的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()算法尚未完成。我将中止该部分测试,因为Lua在过去的 10个小时中一直在使用100%的CPU,现在我需要我的计算机。而且足够热,可以在笔记本电脑的铝制外壳上煮咖啡...

结果如下:

  • 处理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个项目(共1万个项目):ArrayRemove():0.001秒,table.remove():0.018秒(慢18倍)。
  • 10个10,000个项目的数组(共10万个项目):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个项目的数组(共计2000万个项目):ArrayRemove():2.802秒,table.remove():我决定在10个小时后中止测试,因此我们可能永远不会花多长时间。 ;-)但是,在当前时间点(甚至还没有完成),它花费的时间比ArrayRemove()长12847.9倍...但是如果我让它完成,最终的table.remove()结果可能大约是30慢了四万倍。

如您所见,table.remove()的时间增长不是线性的(因为如果是这样的话,那么我们的100万项测试只用了10万(10万)项测试的10倍,但是相反,我们看到的是1.573s和3m48s!)。因此,我们不能接受较低的测试(例如1万个项目),而只能将其乘以 1000万个项目,以了解我中止了 的测试要花多长时间...因此,如果有人真的对最终结果感到好奇,那么table.remove()结束几天后,您就必须自己进行测试并发表评论...

但是,根据目前为止的基准测试,我们现在可以做的是说这些话:F-ck table.remove()! ;-)

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

顺便说一句,上面的测试都是使用Lua 5.3.4执行的,因为这是大多数人使用的标准运行时。我决定使用LuaJIT 2.0.5JIT: 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] :10个数组,每100,000个项目(共100万个项目):ArrayRemove():0.005秒,table.remove():20.783秒(比{{1}慢4156.6倍} ...但是这个LuaJIT结果实际上是比常规Lua的 WORSE 比率,后者的ArrayRemove()“仅”比我的算法慢了 1605.6x 测试...因此,如果您使用LuaJIT,则性能比甚至更高(支持我的算法!)

最后,您可能会想“如果我们只想删除一个项,因为它是一个本机函数,table.remove()会更快吗?”。如果您使用LuaJIT,则该问题的答案是:否。在LuaJIT中,table.remove()ArrayRemove()更快,即使删除了一个项目。而谁是' t 使用LuaJIT?使用LuaJIT,与常规Lua相比,所有Lua代码的速度轻松提高了约30倍。结果如下:table.remove()。这是“只是删除1-6个项目”测试的pastebin:https://pastebin.com/wfM7cXtU(文件末尾列出了完整的测试结果)。

TL; DR:出于任何原因,请勿在任何地方使用[mitch] elapsed time (deleting 1 items): 0.008, [table.remove] elapsed time (deleting 1 items): 0.011

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

答案 3 :(得分: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

注意这很快但有两点需要注意:

  • 如果您需要删除相对较少的元素,速度会更快。 (对于应该保留的元素,它几乎没有任何作用)。
  • 它将使数组保持UNSORTED状态。有时你不关心有一个排序数组,在​​这种情况下,这是一个有用的“快捷方式”。

如果您想保留元素的顺序,或者您希望不保留大部分元素,那么请查看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")

答案 4 :(得分:1)

您可以考虑使用priority queue而不是排序数组。 当您按顺序删除条目时,优先级队列将自动压缩。

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

答案 5 :(得分: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

答案 6 :(得分:0)

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

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

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

答案 7 :(得分:0)

您可以使用仿函数来检查需要删除的元素。额外的好处是它在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)

答案 8 :(得分:0)

简单..

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

答案 9 :(得分:0)

首先,绝对阅读@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;

结束

答案 10 :(得分:0)

效率!更! )

关于米奇的变体。它有一些废物分配为零,这是具有相同想法的优化版本:

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) 不需要,因为它执行了很多次,但在第一次删除后为“真”。我认为 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 吗?