2016-07-18 2 views
4

Я пытаюсь удалить все 7 S из массива:Ruby не зацикливал все элементы?

a = [1,2,3,4,5,7,7,7,7,7,7,7,7,7,9,10,11,12,13] 

я сделал:

a.each_with_index do |item, index| 
    if item == 7 
    a.delete_at(index) 
    end 
end 
a # => [1, 2, 3, 4, 5, 7, 7, 7, 7, 9, 10, 11, 12, 13] 

Как это случилось?

+3

потому что вы удаляете во время цикла – dnit13

+0

@ dnit13, поэтому правильный способ удалить все 7 в массиве? – NamNamNam

+2

http://ruby-doc.org/core-2.2.0/Array.html#method-i-delete – dnit13

ответ

7

Тот факт, что только около половины (5/9) ваших предметов исчезли, является мертвой распродажей, которая устраняет проблему при повторном выполнении коллекции.

Итерация будет обрабатывать индексы 1, 2, 3, 4 и так далее. Если вы удалите индекс 2 во время его обработки, это приведет к смещению всех последующих индексов на один.

Итак, когда вы переходите к индексу 3 в следующей итерации, вы пропустите оригинальный индекс 3, потому что будут сдвинуты вниз к индексу 2.

Другими словами, давайте начнем с более простой пример с двумя соседними элементами для удаления:

index | 0 | 1 | 2 | 3 | 
value | 1 | 7 | 7 | 9 | 

вы проверяете первый индекс и значение 1 так что вы ничего не делаете. Затем проверьте второй индекс и значение 7 так вы удалите его, давая:

index | 0 | 1 | 2 | 
value | 1 | 7 | 9 | 

Вы затем проверить третий индекс и значение 9 так что вы ничего не делаете. Вы также дошли до конца, чтобы он остановился.

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

Как правило, каждая полная пара смежных элементов будет иметь только одну из пары, удаленной, когда элемент сам по себе (за которым не следует другое значение) будет удален. Вот почему только 5/9 вашего 7 s удалены, по одному для каждой из четырех пар и окончательного отдельного.

Правильный путь (в Ruby), чтобы удалить все элементы из одного заданного значения является использование array delete метода:

a.delete(7) 

Вы также можете использовать conditional delete для более сложных условий, таких как удаление всех больше 7 :

a.delete_if {|val| val > 7} 

И, если вы действительно хотите сделать это вам (как учебное упражнение), вам просто нужно понять, что проблема заключается в том, что вы обрабатываете массив в обратном порядке - когда вы это делаете, изменения за пределами того места, где вы удаляете, могут вызывать проблемы.

Если вы хотите найти способ обработки массива обратным образом, эта проблема не возникнет. К счастью, Ruby имеет такой зверь:

a.to_enum.with_index.reverse_each do |item, index| 

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

Я по-прежнему гарантирую, что delete и delete_if - это правильный путь, поскольку они уже запечены в Ruby, и поэтому невероятно маловероятно, чтобы были ошибки.

+0

Спасибо за объяснение, очень полезно. Что делать, если я просто хочу loop & delete? Я пишу эту небольшую программу, потому что я пытаюсь удалить элемент из большого массива JSON, и дело не в том, что я исключаю. Я пишу эту попытку, чтобы понять, как цикл и удаление работают в Ruby – NamNamNam

+2

плюс один .. действительно хороший объяснение. –

+1

@NamNamNam вышеупомянутый 'Array # delete_if' делает под контуром капота и удаляет. – mudasobwa

3

Это происходит потому, что на каждой итерации вы обновляете и удаляете тот же массив, который вы просматриваете.

Для этой цели, вы должны использовать этот delete метод:

a = [1,2,3,4,5,7,7,7,7,7,7,7,7,7,9,10,11,12,13] 
a.delete(7) 
# [1, 2, 3, 4, 5, 9, 10, 11, 12, 13] 

Вы можете увидеть проблему, запустив эту программу:

a = [1,2,3,4,5,6] 
a.each_with_index do |item, index| 
    puts "#{index} : #{item}" 
    if item == 4 
     a.delete_at(index) 
    end 
end 

Выход:

0 : 1 
1 : 2 
2 : 3 
3 : 4 # made delete here 
4 : 6 # See the problem ! 

Надеется, что это помогает:

2

В общем, удаление из массива, которое вы повторяете, на любом языке, станет странным. Чтобы узнать, почему, посмотрите на код для Array#each_index.

VALUE rb_ary_each_index(VALUE ary) 
{ 
    long i; 
    RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length); 

    for (i=0; i<RARRAY_LEN(ary); i++) { 
     rb_yield(LONG2NUM(i)); 
    } 
    return ary; 
} 

Обратите внимание, что это действительно for цикл от 0 до длины массива. Цикл будет просто отсчитываться от 0 до оригинала длины массива.

При удалении элемента из массива, все после того, как он перемещается над 1. Если i = 5 и вы называете a.delete_at(i), что означает a[6] теперь a[5]. a[7] сейчас a[6]. И так далее. Следующая итерация будет иметь i = 6, что означает, что вы фактически пропустили элемент.

Для иллюстрации, и если вы хотите удалить 2.

i = 0 
a = [1,2,2,2,3,4,5] 
    ^
------------------- 
i = 1 
a = [1,2,2,2,3,4,5] 
    ^
a.delete_at(i) 
a = [1,2,2,3,4,5] 
------------------- 
i = 2 
a = [1,2,2,3,4,5] 
     ^
a.delete_at(i) 
a = [1,2,3,4,5] 
------------------- 
i = 3 
a = [1,2,3,4,5] 
     ^
------------------- 
i = 4 
a = [1,2,3,4,5] 
      ^
------------------- 
i = 5 
a = [1,2,3,4,5] 
------------------- 
i = 6 
a = [1,2,3,4,5] 

Обратите внимание, что последние две итерации прошлись от массива, так как массив теперь два элемента меньше, чем это было раньше.

+2

Спасибо за этот длинный ответ и исходный код Ruby! здорово! – NamNamNam

Смежные вопросы