Come trovare e restituire un valore duplicato nell'array


170

arr è una matrice di stringhe:

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

Quale sarebbe un modo semplice ed elegante per verificare se arrha duplicati e, in tal caso, restituirne uno (indipendentemente da quale)?

Esempi:

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

arr == arr.uniqsarebbe un modo semplice ed elegante per verificare se arrha duplicati, tuttavia, non fornisce quali sono stati duplicati.
Joel AZEMAR

Risposte:


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

So che questa non è una risposta molto elegante, ma la adoro. È bellissimo un codice di copertina. E funziona perfettamente a meno che non sia necessario elaborare un set di dati di grandi dimensioni.

Cerchi una soluzione più veloce? Ecco qui!

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

È lineare, O (n), ma ora deve gestire più righe di codice, ha bisogno di casi di test, ecc.

Se hai bisogno di una soluzione ancora più veloce, prova invece C.

Ed ecco l'essenza che confronta diverse soluzioni: https://gist.github.com/naveed-ahmad/8f0b926ffccf5fbd206a1cc58ce9743e


59
Tranne quadratico per qualcosa che può essere risolto in tempo lineare.
jasonmp85,

18
Fornire soluzioni O (n ^ 2) per problemi lineari non è la strada da percorrere.
martedì

21
@ jasonmp85 - True; tuttavia, ciò sta prendendo in considerazione solo il runtime big-O. in pratica, a meno che tu non stia scrivendo questo codice per alcuni enormi dati di ridimensionamento (e in tal caso, in realtà puoi semplicemente usare C o Python), la risposta fornita è molto più elegante / leggibile e non funzionerà molto più lentamente rispetto a una soluzione temporale lineare. inoltre, in teoria, la soluzione del tempo lineare richiede spazio lineare, che potrebbe non essere disponibile
David T.

26
@Kalanamith puoi ottenere valori duplicati usando questoa.select {|e| a.count(e) > 1}.uniq
Naveed,

26
Il problema con il metodo "detect" è che si ferma quando trova il primo duplicato e non ti dà tutti i duplicati.
Jaime Bellmyer,

214

Puoi farlo in alcuni modi, con la prima opzione la più veloce:

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)

E un'opzione O (N ^ 2) (cioè meno efficiente):

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

17
I primi due sono molto più efficienti per array di grandi dimensioni. L'ultimo è O (n * n), quindi può rallentare. Avevo bisogno di usarlo per un array con ~ 20k elementi e i primi due sono tornati quasi istantaneamente. Ho dovuto annullare il terzo perché stava impiegando così tanto tempo. Grazie!!
Venkat D.

5
Solo un'osservazione, ma i primi due che terminano con .map (&: first) potrebbero semplicemente finire con .keys poiché quella parte sta semplicemente tirando le chiavi su un hash.
ingegnereDave

@engineerDave dipende dalla versione ruby ​​utilizzata. 1.8.7 richiederebbe &: first o addirittura {| k, _ | k} senza ActiveSupport.
Emirikol,

ecco alcuni benchmark gist.github.com/equivalent/3c9a4c9d07fff79062a3 in termini di prestazioni il vincitore è chiaramente group_by.select
equivalente8

6
Se stai usando Rubino> 2.1, è possibile utilizzare: ary.group_by(&:itself). :-)
Drenmi,

44

Trova semplicemente la prima istanza in cui l'indice dell'oggetto (conteggio da sinistra) non è uguale all'indice dell'oggetto (conteggio da destra).

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

Se non ci sono duplicati, il valore restituito sarà zero.

Credo che questa sia la soluzione più veloce pubblicata finora nel thread, poiché non si basa sulla creazione di oggetti aggiuntivi #indexe #rindexsono implementati in C. Il runtime big-O è N ^ 2 e quindi più lento di Sergio, ma il tempo a muro potrebbe essere molto più veloce a causa del fatto che le parti "lente" corrono in C.


5
Mi piace questa soluzione, ma restituirà solo il primo duplicato. Per trovare tutti i duplicati:arr.find_all {|e| arr.rindex(e) != arr.index(e) }.uniq
Josh

1
Né la tua risposta mostra come scoprire se ci sono triplicati o se si possono disegnare elementi dall'array per scrivere "CAT".
Cary Swoveland,

3
@ bruno077 Come è questo tempo lineare?
beauby,

4
@ Chris Grande risposta, ma penso che si può fare un po 'meglio con questo: arr.detect.with_index { |e, idx| idx != arr.rindex(e) }. L'uso with_indexdovrebbe rimuovere la necessità per la prima indexricerca.
ki4jnq,

Come lo adatteresti a un array 2D, confrontando i duplicati in una colonna?
ahnbizcad,

30

detecttrova solo un duplicato. find_allli troverò tutti:

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

3
La domanda è molto specifica che deve essere restituito un solo duplicato. Imo, mostrare come trovare tutti i duplicati va bene, ma solo a parte una risposta che risponde alla domanda posta, che non hai fatto. tra l'altro, è stranamente inefficiente invocare countper ogni elemento dell'array. (Un hash di conteggio, per esempio, è molto più efficiente; ad esempio, costruisci h = {"A"=>2, "B"=>2, "C"=> 1 }quindi h.select { |k,v| v > 1 }.keys #=> ["A", "B"].
Cary Swoveland,

24

Ecco altri due modi per trovare un duplicato.

Usa un set

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" 

Utilizzare selectal posto di findper restituire una matrice di tutti i duplicati.

Uso 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" 

Rilascia .firstper restituire un array di tutti i duplicati.

Entrambi i metodi restituiscono nilse non ci sono duplicati.

Ho proposto diArray#difference aggiungerlo al nucleo di Ruby. Maggiori informazioni sono nella mia risposta qui .

Prova delle prestazioni

Confrontiamo i metodi suggeriti. Innanzitutto, abbiamo bisogno di un array per i test:

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

e un metodo per eseguire i benchmark per diversi array di test:

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

Non ho incluso la risposta di @ JjP perché deve essere restituito un solo duplicato e quando la sua risposta viene modificata per farlo è la stessa della risposta precedente di @ Naveed. Né ho incluso la risposta di @ Marin, che, sebbene pubblicata prima della risposta di @ Naveed, ha restituito tutti i duplicati anziché solo uno (un punto minore ma non ha senso valutare entrambi, poiché sono identici quando restituiscono un solo duplicato).

Ho anche modificato altre risposte che hanno restituito tutti i duplicati per restituire solo il primo trovato, ma che non dovrebbero avere alcun effetto sulle prestazioni, poiché hanno calcolato tutti i duplicati prima di selezionarne uno.

I risultati per ciascun benchmark sono elencati dal più veloce al più lento:

Supponiamo innanzitutto che l'array contenga 100 elementi:

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

Ora considera un array con 10.000 elementi:

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

Nota che find_a_dup_using_difference(arr)sarebbe molto più efficiente se Array#differencefosse implementato in C, il che sarebbe il caso se fosse aggiunto al core Ruby.

Conclusione

Molte delle risposte sono ragionevoli ma l' utilizzo di un set è la scelta migliore . È il più veloce nei casi medio-duri, il più veloce nei casi più difficili e solo in casi banali dal punto di vista computazionale - quando la tua scelta non conta comunque - può essere battuto.

L'unico caso molto particolare in cui potresti scegliere la soluzione di Chris sarebbe se vuoi usare il metodo per de-duplicare separatamente migliaia di piccoli array e prevedi di trovare un duplicato in genere inferiore a 10 elementi. Questo sarà un po 'più veloce in quanto evita il piccolo sovraccarico aggiuntivo della creazione del set.


1
Soluzione eccellente All'inizio non è così ovvio come alcuni dei metodi, ma dovrebbe funzionare in un tempo veramente lineare, a scapito di un po 'di memoria.
Chris Heald,

Con find_a_dup_using_set, ottengo il set back, invece di uno dei duplicati. Inoltre non riesco a trovare "find.with_object" nei documenti di Ruby da nessuna parte.
ScottJ,

@Scottj, grazie per la cattura! È interessante che nessuno l'abbia scoperto prima d'ora. L'ho riparato. Questo è Enumerable # find concatenato a Enumerator # with_object . Aggiornerò i parametri di riferimento, aggiungendo la tua soluzione e altri.
Cary Swoveland,

1
Eccellente confronto @CarySwoveland
Naveed il

19

Purtroppo la maggior parte delle risposte lo sono O(n^2).

Ecco una O(n)soluzione,

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

Qual è la complessità di questo?

  • Corre O(n)e si rompe al primo incontro
  • Usa la O(n)memoria, ma solo la quantità minima

Ora, a seconda della frequenza con cui i duplicati sono nel tuo array, questi runtime potrebbero effettivamente migliorare. Ad esempio, se l'array di dimensioni O(n)è stato campionato da una popolazione di k << nelementi diversi O(k), diventa solo la complessità sia per il runtime che per lo spazio , tuttavia è più probabile che il poster originale convalida l'input e desideri assicurarsi che non vi siano duplicati. In tal caso sia la complessità di runtime che quella di memoria O(n)poiché prevediamo che gli elementi non abbiano ripetizioni per la maggior parte degli input.


15

Rubino oggetti Array hanno una grande metodo select.

select {|item| block }  new_ary
select  an_enumerator

La prima forma è ciò che ti interessa qui. Ti consente di selezionare oggetti che superano un test.

Rubino oggetti Array hanno un altro metodo, count.

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

In questo caso, siete interessati ai duplicati (oggetti che compaiono più di una volta nell'array). Il test appropriato è a.count(obj) > 1.

Se a = ["A", "B", "C", "B", "A"]allora

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

Dichiari di volere un solo oggetto. Quindi scegline uno.


1
Mi piace molto questo, ma devi lanciare un uniq alla fine o otterrai["A", "B", "B", "A"]
Joeyjoejoejr

1
Bella risposta. Questo e 'esattamente quello che stavo cercando. Come ha sottolineato @Joeyjoejoejr. Ho inviato una modifica da mettere .uniqin campo.
Surya,

Questo è estremamente inefficiente. Non solo trovi tutti i duplicati e poi butti via tutti tranne uno, ma invochi countper ogni elemento dell'array, che è dispendioso e inutile. Vedi il mio commento sulla risposta di JjP.
Cary Swoveland,

Grazie per aver eseguito i benchmark. È utile vedere come si confrontano le diverse soluzioni in tempo di esecuzione. Le risposte eleganti sono leggibili ma spesso non sono le più efficienti.
Martin Velez,

9

find_all () restituisce un arraycontenente tutti gli elementi di enumcui blocknon lo è false.

Per ottenere duplicateelementi

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

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

O uniqelementi duplicati

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

7

Qualcosa del genere funzionerà

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 }

Cioè, metti tutti i valori in un hash dove key è l'elemento dell'array e value è il numero di occorrenze. Quindi selezionare tutti gli elementi che si verificano più di una volta. Facile.


7

So che questo thread riguarda specificamente Ruby, ma sono arrivato qui alla ricerca di come farlo nel contesto di Ruby on Rails con ActiveRecord e ho pensato di condividere anche la mia soluzione.

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

Quanto sopra restituisce un array di tutti gli indirizzi e-mail duplicati nella tabella del database di questo esempio (che in Rails sarebbe "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

Questa è una O(n)procedura

In alternativa puoi eseguire una delle seguenti righe. Anche O (n) ma solo una iterazione

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

Ecco la mia opinione su un grande insieme di dati - come una tabella dBase legacy per trovare parti duplicate

# 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 È tuo amico!

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

Questo codice restituirà un elenco di valori duplicati. Le chiavi hash sono usate come un modo efficace per verificare quali valori sono già stati visti. In base alla visualizzazione del valore, l'array originale aryè partizionato in 2 array: il primo contiene valori univoci e il secondo contiene duplicati.

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

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

=> ["hello"]

Puoi accorciarlo ulteriormente - anche se a un costo di sintassi leggermente più complessa - in questo modulo:

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

0
a = ["A", "B", "C", "B", "A"]
b = a.select {|e| a.count(e) > 1}.uniq
c = a - b
d = b + c

risultati

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

0

Se si stanno confrontando due array diversi (anziché uno con se stesso), un modo molto veloce è utilizzare l'operatore intersect &fornito dalla classe Array di Ruby .

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

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

1
Ciò trova gli elementi che esistono in entrambi gli array, non i duplicati in un array.
Kimmo Lehto,

Grazie per la segnalazione. Ho modificato il testo nella mia risposta. Lascio qui perché è già stato utile per alcune persone che provengono dalla ricerca.
IAmNaN

0

Avevo bisogno di scoprire quanti duplicati c'erano e quali erano, quindi ho scritto una funzione basandosi su ciò che Naveed aveva pubblicato in precedenza:

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. Creiamo un metodo di duplicazione che prende array di elementi come input
  2. Nel corpo del metodo, creiamo 2 nuovi oggetti array uno è visto e un altro è duplicato
  3. consente infine di scorrere ogni oggetto in un determinato array e per ogni iterazione troviamo che l'oggetto esiste in un array visto.
  4. se l'oggetto esisteva in seen_array, allora viene considerato come oggetto duplicato e invia l'oggetto in duplication_array
  5. se l'oggetto non esiste nel visto, allora viene considerato come oggetto unico e spinge quell'oggetto in seen_array

dimostriamo in Implementazione del codice

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

Ora chiama il metodo di duplicazione e il risultato di ritorno dell'output -

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

Le risposte solo al codice sono generalmente disapprovate su questo sito. Potresti modificare la tua risposta per includere alcuni commenti o una spiegazione del tuo codice? Le spiegazioni dovrebbero rispondere a domande come: cosa fa? Come lo fa? Dove va? Come risolve il problema di OP? Vedi: Come rispondere . Grazie!
Eduardo Baitello,

-4

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

Si noti che quanto sopra è distruttivo


questo non restituisce valori duplicati
andriy-baran,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.