Як знайти і повернути повторне значення в масиві


170

arr це масив рядків:

["hello", "world", "stack", "overflow", "hello", "again"]

Який був би простий та елегантний спосіб перевірити, чи arrє дублікати, і якщо так, повернути один із них (незалежно від того, який)?

Приклади:

["A", "B", "C", "B", "A"]    # => "A" or "B"
["A", "B", "C"]              # => nil

arr == arr.uniqЦе був би простий і елегантний спосіб перевірити наявність arrдублікатів, однак він не передбачає, які були дублюються.
Джоель АЗЕМАР

Відповіді:


249
a = ["A", "B", "C", "B", "A"]
a.detect{ |e| a.count(e) > 1 }

Я знаю, що це не дуже елегантна відповідь, але я люблю це. Це прекрасний код одного вкладиша. І працює чудово, якщо вам не потрібно обробляти величезний набір даних.

Шукаєте швидшого рішення? Ось ви йдете!

def find_one_using_hash_map(array)
  map = {}
  dup = nil
  array.each do |v|
    map[v] = (map[v] || 0 ) + 1

    if map[v] > 1
      dup = v
      break
    end
  end

  return dup
end

Це лінійно, O (n), але тепер потрібно керувати кількома рядками коду, потрібні тестові випадки тощо.

Якщо вам потрібно ще швидше рішення, можливо, спробуйте натомість C.

І ось суть порівняння різних рішень: https://gist.github.com/naveed-ahmad/8f0b926ffccf5fbd206a1cc58ce9743e


59
За винятком квадратичного для чогось, що можна вирішити за лінійний час.
jasonmp85

18
Надання рішення O (n ^ 2) для лінійних задач - це не шлях.
tdgs

21
@ jasonmp85 - Правда; однак, це враховує лише велике виконання. на практиці, якщо ви не пишете цей код для величезних даних про масштабування (і якщо так, ви можете фактично просто використовувати C або Python), надана відповідь набагато більш елегантна / читабельна і не буде працювати так набагато повільніше порівняно до лінійного часового рішення. крім того, теоретично для лінійного часового рішення потрібен лінійний простір, який може бути недоступним
Девід Т.

26
@Kalanamith ви можете отримати скопійовані значення за допомогою цьогоa.select {|e| a.count(e) > 1}.uniq
Naveed

26
Проблема методу "виявити" полягає в тому, що він зупиняється, коли знаходить перший дублікат, і не дає вам усіх дуплів.
Хайме Беллм'єр

214

Це можна зробити кількома способами, причому перший варіант є найшвидшим:

ary = ["A", "B", "C", "B", "A"]

ary.group_by{ |e| e }.select { |k, v| v.size > 1 }.map(&:first)

ary.sort.chunk{ |e| e }.select { |e, chunk| chunk.size > 1 }.map(&:first)

І варіант O (N ^ 2) (тобто менш ефективний):

ary.select{ |e| ary.count(e) > 1 }.uniq

17
Перші два набагато ефективніші для великих масивів. Останнє - O (n * n), щоб воно могло повільно. Мені потрібно було використовувати це для масиву з елементами ~ 20k, і перші два повернулися майже миттєво. Третьому довелося скасувати третій, бо це зайняло так багато часу. Дякую!!
Венкат Д.

5
Просто спостереження, але перші два, що закінчуються .map (&: first), можуть просто закінчитися .keys, оскільки ця частина просто тягне ключі на хеш.
engineerDave

@engineerDave, що залежить від використовуваної рубінової версії. 1.8.7 вимагатиме &: спочатку або навіть {| k, _ | k} без ActiveSupport.
Емірікол

Ось кілька орієнтирів gist.github.com/equivalent/3c9a4c9d07fff79062a3 у виконанні переможець однозначно group_by.select
еквівалентно8

6
Якщо ви використовуєте рубін> 2.1, ви можете використовувати: ary.group_by(&:itself). :-)
Дренмі

44

Просто знайдіть перший екземпляр, коли індекс об’єкта (рахуючи зліва) не дорівнює індексу об’єкта (рахуючи справа).

arr.detect {|e| arr.rindex(e) != arr.index(e) }

Якщо дублікатів немає, значення повернення буде нульовим.

Я вважаю , що це найшвидше вирішення розміщена в потоці до сих пір, а також, оскільки він не спирається на створення додаткових об'єктів, а також #indexі #rindexреалізуються в C. Середовище виконання великий-O є N ^ 2 і , отже , повільніше , ніж Серхіо, але час на стіні міг бути набагато швидшим через те, що "повільні" частини працюють у С.


5
Мені подобається це рішення, але воно поверне лише перший дублікат. Щоб знайти всі дублікати:arr.find_all {|e| arr.rindex(e) != arr.index(e) }.uniq
Josh

1
Також у вашій відповіді не вказано, як знайти, чи є три примірники, чи можна намалювати елементи з масиву для написання "CAT".
Cary Swoveland

3
@ bruno077 Як цей лінійний час?
beauby

4
@ Кріс Великий відповідь, але я думаю , що ви можете зробити трохи краще з цим: arr.detect.with_index { |e, idx| idx != arr.rindex(e) }. Використання with_indexповинно усунути необхідність першого indexпошуку.
ki4jnq

Як би ви адаптували це до двовимірного масиву, порівнюючи дублікати у стовпці?
ahnbizcad

30

detectзнаходить лише один дублікат. find_allзнайде їх усіх:

a = ["A", "B", "C", "B", "A"]
a.find_all { |e| a.count(e) > 1 }

3
Питання дуже специфічне, що потрібно повернути лише один дублікат. Imo, показувати, як знайти всі дублікати, це добре, але лише як осторонь відповіді, яка відповідає на поставлене запитання, чого ви ще не зробили. btw, агоністично неефективно викликати countкожен елемент масиву. (Наприклад, підрахунок хешу набагато ефективніший; наприклад, h = {"A"=>2, "B"=>2, "C"=> 1 }тоді h.select { |k,v| v > 1 }.keys #=> ["A", "B"]
будуйте

24

Ось ще два способи пошуку дубліката.

Використовуйте набір

require 'set'

def find_a_dup_using_set(arr)
  s = Set.new
  arr.find { |e| !s.add?(e) }
end

find_a_dup_using_set arr
  #=> "hello" 

Використовуйте selectзамість, findщоб повернути масив усіх дублікатів.

Використовуйте Array#difference

class Array
  def difference(other)
    h = other.each_with_object(Hash.new(0)) { |e,h| h[e] += 1 }
    reject { |e| h[e] > 0 && h[e] -= 1 }
  end
end

def find_a_dup_using_difference(arr)
  arr.difference(arr.uniq).first
end

find_a_dup_using_difference arr
  #=> "hello" 

Drop, .firstщоб повернути масив усіх дублікатів.

Обидва способи повертаються, nilякщо немає дублікатів.

Я запропонувавArray#difference додати його до ядра Ruby. Більше інформації - у моїй відповіді тут .

Орієнтир

Порівняємо запропоновані методи. Спочатку нам потрібен масив для тестування:

CAPS = ('AAA'..'ZZZ').to_a.first(10_000)
def test_array(nelements, ndups)
  arr = CAPS[0, nelements-ndups]
  arr = arr.concat(arr[0,ndups]).shuffle
end

і метод запуску орієнтирів для різних тестових масивів:

require 'fruity'

def benchmark(nelements, ndups)
  arr = test_array nelements, ndups
  puts "\n#{ndups} duplicates\n"    
  compare(
    Naveed:    -> {arr.detect{|e| arr.count(e) > 1}},
    Sergio:    -> {(arr.inject(Hash.new(0)) {|h,e| h[e] += 1; h}.find {|k,v| v > 1} ||
                     [nil]).first },
    Ryan:      -> {(arr.group_by{|e| e}.find {|k,v| v.size > 1} ||
                     [nil]).first},
    Chris:     -> {arr.detect {|e| arr.rindex(e) != arr.index(e)} },
    Cary_set:  -> {find_a_dup_using_set(arr)},
    Cary_diff: -> {find_a_dup_using_difference(arr)}
  )
end

Я не включив відповідь @ JjP, тому що потрібно повернути лише один дублікат, і коли його / її відповідь буде змінено, щоб це зробити, це те саме, що і попередня відповідь @ Naveed. Я також не включив відповідь @ Маріна, яка, опублікована перед відповіддю @ Naveed, повертала всі дублікати, а не лише один (другорядний пункт, але немає жодної точки, що оцінює обидва, оскільки вони ідентичні, коли повертаються лише один дублікат).

Я також змінив інші відповіді, які повертали всі дублікати, щоб повернути лише перший знайдений, але це не мало по суті ніякого впливу на продуктивність, оскільки вони обчислювали всі дублікати перед вибором одного.

Результати для кожного еталону перераховані від найшвидшого до найповільнішого:

Спочатку припустимо, що масив містить 100 елементів:

benchmark(100, 0)
0 duplicates
Running each test 64 times. Test will take about 2 seconds.
Cary_set is similar to Cary_diff
Cary_diff is similar to Ryan
Ryan is similar to Sergio
Sergio is faster than Chris by 4x ± 1.0
Chris is faster than Naveed by 2x ± 1.0

benchmark(100, 1)
1 duplicates
Running each test 128 times. Test will take about 2 seconds.
Cary_set is similar to Cary_diff
Cary_diff is faster than Ryan by 2x ± 1.0
Ryan is similar to Sergio
Sergio is faster than Chris by 2x ± 1.0
Chris is faster than Naveed by 2x ± 1.0

benchmark(100, 10)
10 duplicates
Running each test 1024 times. Test will take about 3 seconds.
Chris is faster than Naveed by 2x ± 1.0
Naveed is faster than Cary_diff by 2x ± 1.0 (results differ: AAC vs AAF)
Cary_diff is similar to Cary_set
Cary_set is faster than Sergio by 3x ± 1.0 (results differ: AAF vs AAC)
Sergio is similar to Ryan

Тепер розглянемо масив із 10000 елементів:

benchmark(10000, 0)
0 duplicates
Running each test once. Test will take about 4 minutes.
Ryan is similar to Sergio
Sergio is similar to Cary_set
Cary_set is similar to Cary_diff
Cary_diff is faster than Chris by 400x ± 100.0
Chris is faster than Naveed by 3x ± 0.1

benchmark(10000, 1)
1 duplicates
Running each test once. Test will take about 1 second.
Cary_set is similar to Cary_diff
Cary_diff is similar to Sergio
Sergio is similar to Ryan
Ryan is faster than Chris by 2x ± 1.0
Chris is faster than Naveed by 2x ± 1.0

benchmark(10000, 10)
10 duplicates
Running each test once. Test will take about 11 seconds.
Cary_set is similar to Cary_diff
Cary_diff is faster than Sergio by 3x ± 1.0 (results differ: AAE vs AAA)
Sergio is similar to Ryan
Ryan is faster than Chris by 20x ± 10.0
Chris is faster than Naveed by 3x ± 1.0

benchmark(10000, 100)
100 duplicates
Cary_set is similar to Cary_diff
Cary_diff is faster than Sergio by 11x ± 10.0 (results differ: ADG vs ACL)
Sergio is similar to Ryan
Ryan is similar to Chris
Chris is faster than Naveed by 3x ± 1.0

Зауважте, що find_a_dup_using_difference(arr)було б набагато ефективніше, якби вони Array#differenceбули реалізовані в C, що було б у випадку, якщо б його було додано до ядра Ruby.

Висновок

Багато відповідей є розумними, але використання набору - це найкращий вибір . Це найшвидше у випадках із середньою жорсткістю, найшвидший суглоб у найважчих і лише у обчислювально тривіальних випадках - коли ваш вибір все одно не має значення - чи можна його побити.

Єдиним особливим випадком, коли ви можете вибрати рішення Кріса, буде, якщо ви хочете скористатися методом для окремого видалення копій тисяч малих масивів і очікуєте, що вони знайдуть дублікат, як правило, менше 10 елементів. Це буде трохи швидше оскільки це дозволяє уникнути невеликих додаткових витрат на створення набору.


1
Відмінне рішення. Це не так очевидно, що відбувається спочатку, як деякі методи, але він повинен працювати в дійсно лінійний час, за рахунок трохи пам'яті.
Кріс Хілд

За допомогою find_a_dup_using_set я повертаю Set назад замість одного з дублікатів. Також я не можу ніде знайти "find.with_object" в документах Ruby.
ScottJ

@Scottj, дякую за улов! Цікаво, що раніше цього ніхто не зловив. Я полагодив це. Це безліч # знаків, прикутих до Enumerator # with_object . Я оновлю орієнтири, додаю ваше рішення та інші.
Cary Swoveland

1
Відмінне порівняння @CarySwoveland
Naveed

19

На жаль, більшість відповідей є O(n^2).

Ось O(n)рішення,

a = %w{the quick brown fox jumps over the lazy dog}
h = Hash.new(0)
a.find { |each| (h[each] += 1) == 2 } # => 'the"

У чому полягає складність цього?

  • Забігає O(n)та перерветься на першому матчі
  • Використовує O(n)пам'ять, але лише мінімальну кількість

Тепер, залежно від того, наскільки часті копії є у ​​вашому масиві, ці умови виконання можуть стати ще кращими. Наприклад, якщо масив розмірів O(n)був відібраний з групи k << nрізних елементів, стає лише складність і для виконання, і для простору O(k), однак, більш імовірно, що оригінальний плакат перевіряє вхідні дані і хоче переконатися, що немає дублікатів. У цьому випадку як час виконання, так і складність пам'яті, O(n)оскільки ми очікуємо, що елементи не мають повторень для більшості входів.


15

Об'єкти Рубінові масивів мають великий метод, select.

select {|item| block }  new_ary
select  an_enumerator

Перша форма - це те, що вас тут цікавить. Це дозволяє вибрати об'єкти, які проходять тест.

Об'єкти Ruby - масиви мають інший метод, count.

count  int
count(obj)  int
count { |item| block }  int

У цьому випадку вас цікавлять дублікати (об’єкти, які з’являються не один раз у масиві). Відповідний тест є a.count(obj) > 1.

Якщо a = ["A", "B", "C", "B", "A"], то

a.select{|item| a.count(item) > 1}.uniq
=> ["A", "B"]

Ви заявляєте, що хочете лише один об’єкт. Тож виберіть один.


1
Мені це дуже подобається, але вам доведеться кинути ["A", "B", "B", "A"]
uniq

1
Чудова відповідь. Це саме те, що я шукав. Як вказував @Joeyjoejoejr Я надіслав редагування для розміщення .uniqмасиву.
Суря

Це надзвичайно неефективно. Ви не тільки знайдете всі дублікати, а потім викинете всі, крім одного, ви посилаєтесь countна кожен елемент масиву, що марно і непотрібно. Дивіться мій коментар до відповіді JjP.
Cary Swoveland

Дякуємо за тестування тестів. Корисно побачити, як різні рішення порівнюються за час роботи. Елегантні відповіді читаються, але часто не є найбільш ефективними.
Мартін Велес

9

find_all () повертає, що arrayмістить усі елементи, enumдля яких blockце не так false.

Щоб отримати duplicateелементи

>> arr = ["A", "B", "C", "B", "A"]
>> arr.find_all { |x| arr.count(x) > 1 }

=> ["A", "B", "B", "A"]

Або дублювати uniqелементи

>> arr.find_all { |x| arr.count(x) > 1 }.uniq
=> ["A", "B"] 

7

Щось подібне спрацює

arr = ["A", "B", "C", "B", "A"]
arr.inject(Hash.new(0)) { |h,e| h[e] += 1; h }.
    select { |k,v| v > 1 }.
    collect { |x| x.first }

Тобто, поставте всі значення в хеш, де ключовим є елемент масиву, а значення - кількість випадків. Потім виберіть усі елементи, які трапляються не один раз. Легко.


7

Я знаю, що ця тема спеціально стосується Ruby, але я приземлився тут, шукаючи, як це зробити в контексті Ruby on Rails з ActiveRecord, і думав, що теж поділюсь своїм рішенням.

class ActiveRecordClass < ActiveRecord::Base
  #has two columns, a primary key (id) and an email_address (string)
end

ActiveRecordClass.group(:email_address).having("count(*) > 1").count.keys

Наведене повертає масив усіх адрес електронної пошти, які дублюються в таблиці бази даних цього прикладу (що в Rails було б "active_record_classes").


6
a = ["A", "B", "C", "B", "A"]
a.each_with_object(Hash.new(0)) {|i,hash| hash[i] += 1}.select{|_, count| count > 1}.keys

Це O(n)процедура.

Або ви можете виконати будь-який із наступних рядків. Також O (n), але лише одна ітерація

a.each_with_object(Hash.new(0).merge dup: []){|x,h| h[:dup] << x if (h[x] += 1) == 2}[:dup]

a.inject(Hash.new(0).merge dup: []){|h,x| h[:dup] << x if (h[x] += 1) == 2;h}[:dup]

2

Ось мій погляд на великий набір даних - наприклад, застаріла таблиця dBase для пошуку повторюваних частин

# Assuming ps is an array of 20000 part numbers & we want to find duplicates
# actually had to it recently.
# having a result hash with part number and number of times part is 
# duplicated is much more convenient in the real world application
# Takes about 6  seconds to run on my data set
# - not too bad for an export script handling 20000 parts

h = {};

# or for readability

h = {} # result hash
ps.select{ |e| 
  ct = ps.count(e) 
  h[e] = ct if ct > 1
}; nil # so that the huge result of select doesn't print in the console

2
r = [1, 2, 3, 5, 1, 2, 3, 1, 2, 1]

r.group_by(&:itself).map { |k, v| v.size > 1 ? [k] + [v.size] : nil }.compact.sort_by(&:last).map(&:first)

1

each_with_object твій друг!

input = [:bla,:blubb,:bleh,:bla,:bleh,:bla,:blubb,:brrr]

# to get the counts of the elements in the array:
> input.each_with_object({}){|x,h| h[x] ||= 0; h[x] += 1}
=> {:bla=>3, :blubb=>2, :bleh=>2, :brrr=>1}

# to get only the counts of the non-unique elements in the array:
> input.each_with_object({}){|x,h| h[x] ||= 0; h[x] += 1}.reject{|k,v| v < 2}
=> {:bla=>3, :blubb=>2, :bleh=>2}

1

Цей код поверне список дублюваних значень. Клавіші хешу використовуються як ефективний спосіб перевірити, які значення вже були помічені. На основі того, чи було видно значення, вихідний масив aryрозділений на 2 масиви: перший містить унікальні значення, а другий містить дублікати.

ary = ["hello", "world", "stack", "overflow", "hello", "again"]

hash={}
arr.partition { |v| hash.has_key?(v) ? false : hash[v]=0 }.last.uniq

=> ["hello"]

Ви можете також скоротити його - хоча ціною трохи складнішого синтаксису - до такої форми:

hash={}
arr.partition { |v| !hash.has_key?(v) && hash[v]=0 }.last.uniq


0

Якщо ви порівнюєте два різних масиви (замість одного проти самого себе), дуже швидким способом є використання оператора пересічення, &наданого класом Ruby's Array .

# Given
a = ['a', 'b', 'c', 'd']
b = ['e', 'f', 'c', 'd']

# Then this...
a & b # => ['c', 'd']

1
Це знаходить елементи, які існують в обох масивах, а не дублікати в одному масиві.
Кіммо Лехто

Дякуємо, що вказали на це. Я змінив формулювання у своїй відповіді. Я залишу його тут, оскільки це вже виявилося корисним для деяких людей, які приходять із пошуку.
IAmNaN

0

Мені потрібно було дізнатися, скільки було дублікатів і якими вони були, тому я написав функцію, що залежить від того, що Naveed розмістив раніше:

def print_duplicates(array)
  puts "Array count: #{array.count}"
  map = {}
  total_dups = 0
  array.each do |v|
    map[v] = (map[v] || 0 ) + 1
  end

  map.each do |k, v|
    if v != 1
      puts "#{k} appears #{v} times"
      total_dups += 1
    end
  end
  puts "Total items that are duplicated: #{total_dups}"
end

-1
  1. Створимо метод дублювання, який приймає масив елементів як вхідний
  2. У тілі методу давайте створимо 2 нові об’єкти масиву, один бачить, а інший - дублікат
  3. нарешті, дозволяє перебирати кожен об'єкт у заданому масиві, і для кожної ітерації можна знайти, що об'єкт існував у баченому масиві.
  4. якщо об'єкт існував у saw_array, він вважається дублікатом об'єкта і підштовхує його до duplication_array
  5. якщо об'єкт не існував у побаченому, то він розглядається як унікальний об'єкт і підштовхує цей об’єкт до saw_array

Давайте продемонструємо у впровадженні коду

def duplication given_array
  seen_objects = []
  duplication_objects = []

  given_array.each do |element|
    duplication_objects << element if seen_objects.include?(element)
    seen_objects << element
  end

  duplication_objects
end

Тепер виклик методу дублювання та результат повернення виводу -

dup_elements = duplication [1,2,3,4,4,5,6,6]
puts dup_elements.inspect

Відповіді, що стосуються лише коду, зазвичай нахмурені на цьому веб-сайті. Чи можете ви відредагувати свою відповідь, щоб включити коментарі чи пояснення свого коду? Пояснення повинні відповідати на запитання на кшталт: Що це робить? Як це робиться? Куди воно йде? Як це вирішує проблему ОП? Див.: Як відповісти . Дякую!
Едуардо Байєлло

-4

[1,2,3].uniq!.nil? => true [1,2,3,3].uniq!.nil? => false

Зауважте, що вищезгадане є руйнівним


це не повертає дублюваних значень
andriy-baran
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.