Самый эффективный способ подсчета дублированных элементов между двумя массивами

Как часть очень простой программы, которую я пишу в Ruby, я пытаюсь найти общее количество общих элементов между двумя массивами равной длины, но Мне нужно включить повторы.

Мой текущий примерный код для этой ситуации выглядит следующим образом:

array_a = ["B","A","A","A","B"]
array_b = ["A","B","A","B","B"]
counter = 0
array_a.each_index do |i|
  array_a.sort[i] == array_b.sort[i]
    counter += 1
  end
end
puts counter

Я хочу, чтобы возвращаемое значение этого сравнения в этом экземпляре равнялось 4, а не 2, так как два массива имеют два повторяющихся символа ( "А" два раза и "В" два раза). Это похоже на работу, но мне интересно, есть ли более эффективные решения для этой проблемы. В частности, есть ли какие-либо методы, которые вы хотели бы изучить. Я говорил с кем-то, кто предложил другой метод, inject, но я действительно не понимаю, как это применимо и хотелось бы понять. Я довольно много читал об использовании для него, и мне все еще не ясно, насколько это уместно. Спасибо.

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

Ответ 1

Это идеальная работа для Enumerable#zip и Enumerable#count:

array_a.zip(array_b).count do |a, b|
  a == b
end
# => 2

Метод zip объединяет элементы, "застегивая их на молнию" вместе, а метод count может принимать блок относительно того, следует ли считать элемент.

Метод inject очень мощный, но он также самый низкий уровень. Практически каждый другой метод Enumerable может быть создан с помощью inject, если вы работаете над ним, поэтому он достаточно гибкий, но обычно более подходящий метод лучше подходит. Он по-прежнему является полезным инструментом, если он применяется правильно.

В этом случае zip и count выполняют гораздо лучшую работу, и если вы знаете, что делают эти методы, этот код сам объясняет.

Обновление

Если вам нужно подсчитать все перекрывающиеся буквы независимо от порядка, вам нужно сделать некоторую группировку на них. Ruby on Rails предоставляет удобный метод group_by в ActiveSupport, но в чистом Ruby вам нужно сделать свой собственный.

Здесь используется подход, который подсчитывает все уникальные буквы, группируя их с помощью chunk:

# Convert each array into a map like { "A" => 2, "B" => 3 }
# with a default count of 0.
counts = [ array_a, array_b ].collect do |a|
  Hash.new(0).merge(
    Hash[a.sort.chunk { |v| v }.collect { |k, a| [ k, a.length ] }]
  )
end

# Iterate over one of the maps key by key and count the minimum
# overlap between the two.
counts[0].keys.inject(0) do |sum, key|
  sum + [ counts[0][key], counts[1][key] ].min
end

Ответ 2

Если я правильно понял вопрос, вы можете сделать следующее.

код

def count_shared(arr1, arr2)
  arr1.group_by(&:itself).
       merge(arr2.group_by(&:itself)) { |_,ov,nv| [ov.size, nv.size].min }.
       values.
       reduce(0) { |t,o| (o.is_a? Array) ? t : t + o }
end

<сильные > Примеры

arr1 = ["B","A","A","A","B"]
arr2 = ["A","B","A","B","B"]

count_shared(arr1, arr2)
  #=> 4 (2 A + 2 B's)

arr1 = ["B", "A", "C", "C", "A", "A", "B", "D", "E", "A"]
arr2 = ["C", "D", "F", "F", "A", "B", "A", "B", "B", "G"]

count_shared(arr1, arr2)
  #=> 6 (2 A + 2 B + 1 C + 1 D + 0 E + 0 F + 0 G's)

Объяснение

Следующие шаги для слегка измененной версии первого примера.

arr1 = ["B","A","A","A","B","C","C"]
arr2 = ["A","B","A","B","B","D"]

Сначала примените Enumerable # group_by к arr1 и arr2:

h0 = arr1.group_by(&:itself)
  #=> {"B"=>["B", "B"], "A"=>["A", "A", "A"], "C"=>["C", "C"]} 
h1 = arr2.group_by(&:itself)
  #=> {"A"=>["A", "A"], "B"=>["B", "B", "B"], "D"=>["D"]} 

До Ruby v.2.2, когда был введен Object # сам, вам нужно будет написать:

arr.group_by { |e| e }

Продолжение,

h2 = h0.merge(h1) { |_,ov,nv| [ov.size, nv.size].min }
  #=> {"B"=>2, "A"=>2, "C"=>["C", "C"], "D"=>["D"]} 

Я скоро вернусь, чтобы объяснить приведенный выше расчет.

a = h2.values
  #=> [2, 2, ["C", "C"], ["D"]] 
a.reduce(0) { |t,o| (o.is_a? Array) ? t : t + o }
  #=> 4

Здесь Enumerable # reduce (aka inject) просто суммирует значения a, которые не являются массивами. Массивы соответствуют элементам arr1, которые не отображаются в arr2 или наоборот.

Как и было обещано, я теперь объясню, как вычисляется h2. Я использовал форму Hash # merge, в которой используется блок (здесь { |k,ov,nv| [ov.size, nv.size].min }), чтобы вычислить значения ключей, которые присутствуют в оба хеша объединяются. Например, когда первая пара ключ-значение h1 ("A"=>["A", "A"]) объединяется в h0, так как h0 также имеет ключ "A", массив

["A", ["A", "A", "A"], ["A", "A"]]

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

k, ov, nv = ["A", ["A", "A", "A"], ["A", "A"]]

поэтому имеем

k  #=> "A" 
ov #=> ["A", "A", "A"] 
nv #=> ["A", "A"] 

k является ключом, ov ( "старое значение" ) является значением "A" в h0 и nv ( "новое значение" ) является значением "A" в h1. Расчет блока

[ov.size, nv.size].min
  #=> [3,2].min = 2

поэтому значение "A" теперь 2.

Обратите внимание, что ключ, k, не используется при вычислении блока (что очень часто встречается при использовании этой формы merge). По этой причине я изменил блок-переменную от k до _ (допустимая локальная переменная), чтобы уменьшить вероятность появления ошибки и сообщить читателю, что ключ не используется в блоке. Остальные элементы h2, которые используют этот блок, вычисляются аналогично.

Другой способ

Было бы довольно просто, если бы у нас был доступный Array метод Я предложил добавить в ядро ​​Ruby:

array_a = ["B","A","A","A","B"]
array_b = ["A","B","A","B","B"]

array_a.size - (array_a.difference(array_b)).size
  #=> 4

или

array_a.size - (array_b.difference(array_a)).size
  #=> 4

Я привел другие приложения в своем ответе здесь.

Ответ 3

Позвольте мне повторить и объяснить, что, на мой взгляд, оригинальное намерение OP:

Указанные массивы равного размера

array_a = ["B","A","A","A","B"]
array_b = ["A","B","A","B","B"]

Нам нужно показать общее количество совпадающих пар элементов между двумя массивами. Другими словами, каждый B в array_a будет "использовать" B в array_b, и то же самое будет верно для каждого A. Поскольку в array_a есть два B в array_a и три в array_b, это оставляет нас со счетом 2 для B и по той же логике 2 для A, для суммы 4.

(array_a & array_b).map { |e| [array_a.count(e), array_b.count(e)].min }.reduce(:+)

Если мы получим пересечение массивов с &, результатом будет список значений, существующих в обоих массивах. Затем мы перебираем каждое совпадение и выбираем минимальное количество раз, когда элемент существует в любом массиве --- это самое большее количество раз, которое элемент может быть "использован". Осталось только общее число парных элементов: reduce(:+)

Изменение array_a на ["B", "A", "A", "B", "B"] приводит к сумме 5, так как в настоящее время достаточно B для выключения питания B в array_b.