そういう仕様だった
The array is changed instantly every time the block is called, not after the iteration is over.
http://www.ruby-doc.org/core-2.1.2/Array.html#method-i-delete_if
ようするに条件ブロックが true を返したら次に条件ブロックが実行されるときには、配列は完全な状態で要素数は1つ減っているということだ。使いやすさ優先ということなのだろう。
この制約を緩めると O(n) の delete_if を書くことが出来る。
class Array # 走査が完了するまで配列を不完全な状態に置くが、 # 空間効率 O(1)、時間効率 O(n) で走る delete_if。 def lite_delete_if fail ArgumentError, 'no block given' unless block_given? read_ptr = 0 writ_ptr = 0 while read_ptr < size item = self[read_ptr] if yield(item) read_ptr += 1 next elsif read_ptr != writ_ptr self[writ_ptr] = item end read_ptr += 1 writ_ptr += 1 end self ensure while read_ptr < size self[writ_ptr] = self[read_ptr] read_ptr += 1 writ_ptr += 1 end self[writ_ptr..-1] = [] # truncate end end
標準の delete_if との比較はこんな感じ。
user system total real delete all 10.070000 0.690000 10.760000 ( 10.802066) delete half 5.050000 0.360000 5.410000 ( 5.418306) delete none 0.010000 0.000000 0.010000 ( 0.011556) lite all 0.020000 0.000000 0.020000 ( 0.026159) lite half 0.040000 0.000000 0.040000 ( 0.038867) lite none 0.030000 0.010000 0.040000 ( 0.033636)
配列への書き込み回数が一番多くなる、全要素の半分を削除するケースが一番遅くなっているのが面白い。
空間効率を気にしなければ*1もう少し速くできる。こちらのバージョンはブロックから break した場合、何も削除されない点でまたセマンティクスが異なる。
class Array # 走査が完了するまで配列を全く変更しないが、 # 空間効率 O(n)、時間効率 O(n) で走る delete_if。 def quick_delete_if(&test) fail ArgumentError, 'no block given' unless test replace(reject(&test)) end end
user system total real quick all 0.010000 0.000000 0.010000 ( 0.012533) quick half 0.020000 0.000000 0.020000 ( 0.014838) quick none 0.010000 0.000000 0.010000 ( 0.022953)
# -*- coding: utf-8 -*- require 'benchmark' # class Array ... をここに。 n = 250_000 ints = [*1..n] Benchmark.bm(11) do |bm| ar = ints.dup bm.report('delete all') do ar.delete_if { true } end fail unless ar.empty? ar = ints.dup bm.report('delete half') do ar.delete_if(&:odd?) end half = ar fail unless ar.size == 125_000 ar = ints.dup bm.report('delete none') do ar.delete_if { false } end fail unless ar == ints ar = ints.dup bm.report('lite all') do ar.lite_delete_if { true } end fail unless ar.empty? ar = ints.dup bm.report('lite half') do ar.lite_delete_if(&:odd?) end fail unless ar == half ar = ints.dup bm.report('lite none') do ar.lite_delete_if { false } end fail unless ar == ints ar = ints.dup bm.report('quick all') do ar.quick_delete_if { true } end fail unless ar.empty? ar = ints.dup bm.report('quick half') do ar.quick_delete_if(&:odd?) end fail unless ar == half ar = ints.dup bm.report('quick none') do ar.quick_delete_if { false } end fail unless ar == ints end