Подтвердить что ты не робот

Безопасное удаление элементов из таблицы массивов во время итерации

Этот вопрос похож на Как я могу безопасно перебирать таблицу 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
4b9b3361

Ответ 1

общий случай итерации по массиву и удаление случайных элементов из середины, продолжая итерацию

Просто используйте table.remove. Однако, если вы повторяете фронт-к-назад, когда вы удаляете элемент 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 во время итерации:

  • Итерация с обратной стороны.

    for i=#input,1,-1 do
        if remove[input[i]] then
            table.remove(input, i)
        end
    end
    
  • Управление переменной цикла вручную, поэтому мы можем пропустить ее при удалении элемента:

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

Для таблиц без массива вы выполняете итерацию с использованием next или pairs (которая реализована в терминах next) и устанавливайте элементы, которые вы хотите удалить, на nil.

Ответ 2

Я бы избегал table.remove и проходил массив после установки ненужных записей в nil, а затем пересекал массив, снова уплотняя его, если это необходимо.

Вот код, который я имею в виду, используя пример из Mud answer:

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

Ответ 3

Попробуйте эту функцию:

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 в генератор-генератор, чтобы удалить потребность в 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

Это имеет то преимущество, что можно отменить весь цикл без удаления элементов, а также предоставить возможность пропускать элементы, уже отмеченные как "подлежащие удалению". Недостатком является накладные расходы новой таблицы.

Я надеюсь, что они вам полезны.

Ответ 4

Вместо сортированного массива вы можете использовать приоритетную очередь. Очередь приоритетов будет эффективно сжиматься при удалении записей в порядке.

Пример реализации очереди приоритетов см. в этом списке рассылки: http://lua-users.org/lists/lua-l/2007-07/msg00482.html

Ответ 5

Я также рекомендую не использовать 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

Обратите внимание, что это изменит порядок элементов в массиве.

Если вы хотите сохранить порядок элементов, вы можете использовать table.remove, и я делаю это следующим образом:

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

Ответ 6

Мне кажется, что для моего особого случая, где я только когда-либо перемещаю записи из передней части очереди, я могу сделать это гораздо проще:

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

Однако я не буду принимать это как ответ, потому что он не обрабатывает общий случай итерации по массиву и удаление случайных элементов из середины, продолжая итерацию.

Ответ 7

Вы можете использовать функтор для проверки элементов, которые необходимо удалить. Дополнительный коэффициент усиления заключается в том, что он завершается в 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)