Как я могу эффективно извлекать повторяющиеся элементы в массиве Ruby?

У меня есть массив типа [1,1,1,2,4,6,3,3], и я хотел бы получить список повторяющихся элементов, в данном случае [1,3]. Я написал это:

my_array.select{|obj|my_array.count(obj)>1}.uniq

Но это трагически неэффективно (o(n²)). У вас есть идея получше? Если можно кратко.

Спасибо


person MiniQuark    schedule 24.04.2009    source источник


Ответы (8)


Вдохновленный ответом Ильи Хайкинсона:

def repeated(array)
  counts = Hash.new(0)
  array.each{|val|counts[val]+=1}
  counts.reject{|val,count|count==1}.keys
end
person Community    schedule 24.04.2009
comment
Да, я думаю, что это чище, чем у меня. Просто для удовольствия, вот этот метод в одной строке, при условии наличия метода tap из Ruby ›= 1.8.7. array.inject(Hash.new(0)){|counts,val|counts.tap{|c|c[val]+=1}}.reject{|val,count|count==1}.keys Я думаю но твой более читабелен. :) - person Greg Campbell; 24.04.2009
comment
Мне очень, очень нравится это решение, и мне оно нравится, потому что оно самое читабельное/понятное среди всех решений O(n). Вот однострочная модификация, просто для удовольствия: array.inject(Hash.new(0)) { |h, i| h[i] += 1; h }.reject { |v, c| c == 1 }.keys - person Marek Příhoda; 11.12.2011
comment
Спасибо! Удивительно... Я страдал от detect, find_all и т. д. - person rapcal; 18.12.2015
comment
Это нормально, но любой, кто считает, что это лучший ответ, должен ознакомиться с Set.new. Он использует хэш под капотом и отлично подходит, когда вам нужен доступ к хеш-ключу O (1), но с простотой массива. Кроме того, это способствует удобочитаемости, поскольку вся логика сводится к прекрасному очевидному dups.add(val) if seen_already.include?(val). - person Adamantish; 10.01.2019

Используя библиотеку Ruby Set:

require 'set'

ary = [1,1,1,2,4,6,3,3]
dups = Set.new
test_set = Set.new
ary.each {|val| dups.add(val) unless test_set.add?(val)}
dups.to_a # [1, 3]

Я считаю, что это должно быть O (n), потому что Set#add и Set#add? Насколько я знаю, это операции с постоянным временем.

person Greg Campbell    schedule 24.04.2009

Как насчет чего-то подобного? Он будет работать за O(n).

a = [1,1,1,2,4,6,3,3]
b = {}
a.each { |v| if b.has_key? v then b[v] = b[v]+1 else b[v]=1 end }
b.reject { |k,v| if v > 1 then false else true end }.keys
person Ilya Haykinson    schedule 24.04.2009
comment
Мне нравится идея. Вы можете украсить последнюю строку следующим образом: b.reject{|k,v| v==1}.ключи - person MiniQuark; 24.04.2009
comment
Кроме того, вы можете использовать b=Hash.new(0), и тогда у вас будет более простая 3-я строка: a.each{|v|b[v]+=1} - person MiniQuark; 24.04.2009

Решение O(n) (измените << x на + [x] и update на merge, чтобы сделать его полностью функциональным):

rs = xs.inject([[], {}]) do |(out, seen), x| 
  [(seen[x] == 1 ? (out << x) : out), seen.update(x => (seen[x] || 0)+1)]
end[0]

Гораздо более простой, но менее эффективный подход:

rs = xs.group_by { |x| x }.select { |y, ys| ys.size > 1 }.keys

Та же идея, избегающая промежуточного хеша с использованием «понимания списка»:

rs = xs.group_by { |x| x }.map { |y, ys| y if ys.size > 1 }.compact
person tokland    schedule 10.12.2011
comment
Есть проблема с этим решением. См. xs = [1,1,1]. - person Jan; 10.12.2011
comment
@ Ян, действительно, спасибо за указание. Смотрите обновление. - person tokland; 10.12.2011
comment
Не лучше ли подходит group_by? - person Andrew Grimm; 11.12.2011
comment
@Андрей. Я думал, что уже есть решение с использованием group_by, но, похоже, это было в другом вопросе. Я добавлю это. Теперь, когда Ruby упорядочил хэши, мы можем сохранить порядок исходного перечисления. Однако это менее эффективно, чем индивидуальное решение. - person tokland; 11.12.2011

Использование inject

[1,1,1,2,4,6,3,3].inject({}){ |ele, n| ele[n] = nil; ele }.keys 
# => [1, 2, 4, 6, 3] 

ОБЪЯСНЕНИЕ:

ele инициализируется {}, на каждой итерации к хэшу ele добавляется ключ с номером n и значением nil. В конце ele возвращается как:

{1=>nil, 2=>nil, 4=>nil, 6=>nil, 3=>nil}

Нам нужны только ключи, поэтому .keys заканчивает работу.

person ivanxuu    schedule 19.07.2013
comment
Спасибо, но мне нужны были только повторяющиеся элементы, как показано в примере. - person MiniQuark; 26.07.2013

Некоторые идеи: вам нужно будет выяснить правильные структуры данных библиотеки:

1 Отсортируйте массив O(nlogn), затем просмотрите массив

2 Создайте набор, найдите текущий элемент массива в наборе и, если он не найден, вставьте и продолжите для всех элементов -- снова O(nlogn).

person dirkgently    schedule 24.04.2009

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

a = [1,1,1,2,4,6,3,3]

dupes = []
a.uniq.each do |u|
  c = a.find_all {|e| e == u}.size
  dupes << [u, c] unless c == 1
end

puts dupes.inspect

# dupes = [[1, 3], [3, 2]]
# 1 appears 3 times
# 3 appears twice


# to extract just the elment a bit cleaner
dupes = a.uniq.select do |u|
  a.find_all {|e| e == u}.size != 1
end
puts dupes.inspect
# returns [1,3]
person marekj    schedule 18.12.2009

Это будет работать, если дублированные записи всегда идут подряд, как в вашем примере; в противном случае вам пришлось бы сначала сортировать. each_cons исследует скользящее окно указанного размера.

require 'set'

my_array = [1,1,1,2,4,6,3,3]
dups = Set.new
my_array.each_cons(2) {|a,b| dups.add(a) if (a == b)}
p dups.to_a
person Justin Love    schedule 18.12.2009