web-dev-qa-db-fra.com

Supprimer en toute sécurité des éléments d'un tableau lors de l'itération

Cette question est similaire à Comment puis-je parcourir en toute sécurité une table Lua lors du retrait des clés mais distinctement différent.

Résumé

Étant donné un tableau Lua (table avec des clés qui sont des entiers séquentiels commençant par 1), quel est le meilleur moyen de parcourir ce tableau et de supprimer certaines entrées telles qu'elles sont vues?

Exemple du monde réel

J'ai un tableau d'entrées horodatées dans une table de tableaux Lua. Les entrées sont toujours ajoutées à la fin du tableau (en utilisant table.insert).

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

Je dois parfois parcourir cette table (dans l'ordre) et traiter et supprimer certaines entrées:

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

Malheureusement, l'approche du code ci-dessus rompt l'itération en sautant certaines entrées. Existe-t-il un meilleur moyen (moins de dactylographie, mais toujours sûr) de le faire que de parcourir manuellement les index:

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
29
Phrogz

J'éviterais table.remove et traverserais le tableau une fois les entrées non désirées définies sur nil, puis traverserais à nouveau le tableau en le compactant si nécessaire.

Voici le code que j'ai en tête, en utilisant l'exemple de la réponse de 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
21
lhf

le cas général d'itération sur un tableau et de suppression d'éléments aléatoires du milieu tout en continuant l'itération

Si vous effectuez une itération de bout en bout, lorsque vous supprimez l'élément N, l'élément suivant de votre itération (N + 1) est déplacé dans cette position. Si vous incrémentez votre variable d'itération (comme le fait ipairs), vous ignorez cet élément. Il y a deux façons de gérer cela.

En utilisant cet exemple de données:

    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 }

Nous pouvons supprimer des éléments input pendant l'itération en:

  1. Itérer de l'arrière à l'avant.

    for i=#input,1,-1 do
        if remove[input[i]] then
            table.remove(input, i)
        end
    end
    
  2. Contrôler manuellement la variable de boucle afin de ne pas l'incrémenter lors de la suppression d'un élément:

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

Pour les tables autres que des tableaux, vous effectuez une itération à l'aide de next ou pairs (implémenté sous la forme de next) et définissez les éléments que vous souhaitez supprimer sur nil.

Notez que table.remove décale tous les éléments suivants à chaque appel, les performances sont donc exponentielles pour N suppressions. Si vous supprimez de nombreux éléments, vous devez les déplacer vous-même, comme dans LHF ou la réponse de Mitch.

40
Mud

Essayez cette fonction:

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

Il n'utilise pas table.remove, il devrait donc avoir la complexité O(N). Vous pouvez déplacer la fonction remove dans le générateur for pour supprimer le besoin d'une valeur supérieure, mais cela signifierait une nouvelle fermeture pour chaque élément ... et ce n'est pas une question pratique.

Exemple d'utilisation:

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, ','))

Veillez à ne pas utiliser break (ni à sortir prématurément de la boucle) car cela laisserait le tableau avec des éléments nil.

Si vous voulez que break signifie "abort" (comme dans, rien n'est supprimé), vous pouvez faire ceci:

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

Cela présente l'avantage de pouvoir annuler la totalité de la boucle sans qu'aucun élément ne soit supprimé, tout en offrant la possibilité de sauter des éléments déjà marqués comme "à supprimer". L'inconvénient est le surcoût d'une nouvelle table.

J'espère que cela vous sera utile.

4
Deco

Je déconseille d'utiliser table.remove, pour des raisons de performances (qui peuvent être plus ou moins pertinentes pour votre cas particulier).

Voici à quoi ressemble ce type de boucle pour moi:

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

Note Ceci est rapide MAIS avec deux mises en garde:

  • C'est plus rapide si vous devez supprimer relativement peu d'éléments. (Il ne fait pratiquement aucun travail pour les éléments qui devraient être conservés). 
  • Il laissera le tableau UNSORTED. Parfois, vous ne vous souciez pas d'avoir un tableau trié, et dans ce cas, c'est un "raccourci" utile.

Si vous souhaitez conserver l'ordre des éléments ou si vous prévoyez ne pas conserver la plupart des éléments, examinez la solution de Mitch. Voici une comparaison approximative entre le mien et le sien. Je l'ai exécuté sur https://www.lua.org/cgi-bin/demo et la plupart des résultats étaient similaires à ceux-ci:

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

Bien sûr, rappelez-vous que cela varie en fonction de vos données.

Voici le code pour le test:

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")
2
Srekel

Vous pouvez envisager d'utiliser une file d'attente priority au lieu d'un tableau trié . Une file d'attente prioritaire se compacte efficacement lorsque vous supprimez des entrées dans l'ordre.

Pour un exemple d'implémentation d'une file d'attente de priorités, voir le fil de discussion suivant: http://lua-users.org/lists/lua-l/2007-07/msg00482.html

1
finnw

Il me semble que, dans mon cas particulier, où je ne déplace jamais que les entrées du début de la file d'attente, je peux le faire beaucoup plus simplement via:

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

Cependant, je n'accepterai pas cela comme solution car il ne gère pas le cas général d'itération sur un tableau et de suppression d'éléments aléatoires du milieu tout en poursuivant l'itération.

0
Phrogz

Vous pouvez utiliser un foncteur pour vérifier les éléments à supprimer. Le gain supplémentaire est qu'il se termine dans O (n), car il n'utilise pas 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

Usage:

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

Dans ton cas:

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

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
0
James Penner