Techioz Blog

delete_if と拒否の予期しない動作!メソッド

概要

Rubyに要素の配列があります

nums = [1,1,1,2,2,3]

タスクは、要素が配列内に 2 回以上出現する場合に、その場で重複を削除することです。

次のコードを書きました。

nums = [1,1,1,2,2,3]

def remove_elements(nums)
  nums.delete_if{ |num| nums.count(num) > 2 } #reject! returns the same result
end

だから私はこの結果を予想していました

[2,2,3]

しかし、得た

[2,3]

ただし、reject メソッドは期待される配列を返します

def remove_elements(nums)
  nums.reject{ |num| nums.count(num) > 2 } # returns [2,2,3]
end

解決策

これは delete_if /拒否の方法によるものです。 (そしてそれに対応する keep_if / select!) は内部的に動作します。各ステップで数値を出力しながら偶数を削除する別の例を見てみましょう。

nums = [0, 1, 2, 3, 4, 5, 6]

def remove_elements(nums)
  nums.reject! do |num|
    p nums: nums
    num.even?
  end
end

p result: remove_elements(nums)

出力:

{:nums=>[0, 1, 2, 3, 4, 5, 6]}
{:nums=>[0, 1, 2, 3, 4, 5, 6]}
{:nums=>[1, 1, 2, 3, 4, 5, 6]}
{:nums=>[1, 1, 2, 3, 4, 5, 6]}
{:nums=>[1, 3, 2, 3, 4, 5, 6]}
{:nums=>[1, 3, 2, 3, 4, 5, 6]}
{:nums=>[1, 3, 5, 3, 4, 5, 6]}
{:result=>[1, 3, 5]}

ご覧のとおり、nums は一見奇妙な方法で変更されています。しかし、ここで正確には何が起こるのでしょうか?

Ruby の実装では、一時配列を作成する代わりに、既存の配列を再利用して結果を保存します。配列を走査する際に、保持する必要がある (ブロックが偽の結果を返す) 各要素を先頭にコピーし、既存の要素を上書きします。最終的に、配列は最終的なサイズに切り詰められます: (ASCII アートが先)

[0, 1, 2, 3, 4, 5, 6]
 ┌──┘
[1, 1, 2, 3, 4, 5, 6]
    ┌─────┘
[1, 3, 2, 3, 4, 5, 6]
       ┌────────┘
[1, 3, 5, 3, 4, 5, 6]

[1, 3, 5]
 ───┬───
  result

1、3、5 が配列の途中で 2 回出現することに注意してください。これが、nums.count が期待どおりに機能しない理由です。配列が再構築されている間に要素をカウントしていることになります。

この問題を解決するには、事前に要素を数えることができます。集計経由:

nums = [1, 1, 1, 2, 2, 3]

counts = nums.tally
#=> {1=>3, 2=>2, 3=>1}

nums.delete_if { |num| counts[num] > 2 }
#=> [2, 2, 3]

これは、要素を数えるために各要素の配列全体を再度走査する必要がないため、高速でもあります。