Cómo encontrar y devolver un valor duplicado en la matriz

170

arr es una variedad de cadenas:

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

¿Cuál sería una manera fácil y elegante de verificar si arrtiene duplicados y, de ser así, devolver uno de ellos (sin importar cuál)?

Ejemplos:

["A", "B", "C", "B", "A"]    # => "A" or "B"
["A", "B", "C"]              # => nil
Misha Moroshko
fuente
arr == arr.uniqsería una manera fácil y elegante de verificar si arrtiene duplicados, sin embargo, no proporciona cuáles fueron duplicados.
Joel AZEMAR

Respuestas:

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

Sé que esta no es una respuesta muy elegante, pero me encanta. Es hermoso un código de línea. Y funciona perfectamente bien a menos que necesite procesar un gran conjunto de datos.

¿Busca una solución más rápida? ¡Aqui tienes!

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

Es lineal, O (n), pero ahora necesita administrar múltiples líneas de código, necesita casos de prueba, etc.

Si necesita una solución aún más rápida, pruebe con C en su lugar.

Y aquí está la esencia de comparar diferentes soluciones: https://gist.github.com/naveed-ahmad/8f0b926ffccf5fbd206a1cc58ce9743e

Naveed
fuente
59
Excepto cuadrático para algo que se puede resolver en tiempo lineal.
jasonmp85
18
Proporcionar soluciones O (n ^ 2) para problemas lineales no es el camino a seguir.
tdgs
21
@ jasonmp85: verdadero; sin embargo, eso solo está considerando el tiempo de ejecución big-O. en la práctica, a menos que esté escribiendo este código para algunos datos de escalado enormes (y si es así, en realidad solo puede usar C o Python), la respuesta proporcionada es mucho más elegante / legible, y no va a funcionar mucho más lento en comparación a una solución de tiempo lineal. Además, en teoría, la solución de tiempo lineal requiere un espacio lineal, que puede no estar disponible
David T.
26
@Kalanamith puedes obtener valores duplicados usando estoa.select {|e| a.count(e) > 1}.uniq
Naveed
26
El problema con el método "detectar" es que se detiene cuando encuentra el primer duplicado y no le da todos los duplicados.
Jaime Bellmyer
214

Puede hacerlo de varias maneras, siendo la primera opción la más rápida:

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)

Y una opción O (N ^ 2) (es decir, menos eficiente):

ary.select{ |e| ary.count(e) > 1 }.uniq
Ryan LeCompte
fuente
17
Los dos primeros son mucho más eficientes para matrices grandes. El último es O (n * n), por lo que puede ser lento. Necesitaba usar esto para una matriz con ~ 20k elementos y los dos primeros regresaron casi instantáneamente. Tuve que cancelar el tercero porque me estaba tomando mucho tiempo. ¡¡Gracias!!
Venkat D.
55
Solo una observación, pero los dos primeros que terminan con .map (&: first) podrían terminar con .keys, ya que esa parte solo está presionando las teclas en un hash.
ingenieroDave
@engineerDave que depende de la versión de ruby ​​que se utilice. 1.8.7 requeriría &: primero o incluso {| k, _ | k} sin ActiveSupport.
Emirikol
Aquí hay algunos puntos de referencia gist.github.com/equivalent/3c9a4c9d07fff79062a3 en rendimiento el ganador es claramente group_by.select
equivalente8
66
Si está utilizando Rubí> 2.1, puede utilizar: ary.group_by(&:itself). :-)
Drenmi
44

Simplemente encuentre la primera instancia donde el índice del objeto (contando desde la izquierda) no es igual al índice del objeto (contando desde la derecha).

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

Si no hay duplicados, el valor de retorno será nulo.

Creo que esta es la solución más rápida publicada en el hilo hasta ahora, ya que no se basa en la creación de objetos adicionales #indexy #rindexse implementa en C. El tiempo de ejecución de Big-O es N ^ 2 y, por lo tanto, más lento que Sergio, pero el tiempo de la pared podría ser mucho más rápido debido al hecho de que las partes "lentas" corren en C.

Chris Heald
fuente
55
Me gusta esta solución, pero solo devolverá el primer duplicado. Para encontrar todos los duplicados:arr.find_all {|e| arr.rindex(e) != arr.index(e) }.uniq
Josh
1
Su respuesta tampoco muestra cómo encontrar si hay triplicados, o si uno puede dibujar elementos de la matriz para deletrear "CAT".
Cary Swoveland
3
@ bruno077 ¿Cómo es este tiempo lineal?
beauby
44
@ Chris Gran respuesta, pero creo que se puede hacer un poco mejor con esto: arr.detect.with_index { |e, idx| idx != arr.rindex(e) }. El uso with_indexdebería eliminar la necesidad de la primera indexbúsqueda.
ki4jnq
¿Cómo adaptarías esto a una matriz 2D, comparando duplicados en una columna?
ahnbizcad
30

detectsolo encuentra un duplicado. find_alllos encontrará a todos:

a = ["A", "B", "C", "B", "A"]
a.find_all { |e| a.count(e) > 1 }
JjP
fuente
3
La pregunta es muy específica de que solo se devolverá un duplicado. Imo, mostrar cómo encontrar todos los duplicados está bien, pero solo como respuesta a una respuesta que responde a la pregunta formulada, lo que no ha hecho. por cierto, es agonizante ineficiente invocar countpara cada elemento de la matriz. (Un hash de conteo, por ejemplo, es mucho más eficiente; por ejemplo, construir h = {"A"=>2, "B"=>2, "C"=> 1 }entonces h.select { |k,v| v > 1 }.keys #=> ["A", "B"].
Cary Swoveland
24

Aquí hay dos formas más de encontrar un duplicado.

Usar un conjunto

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" 

Usar selecten lugar defind para devolver una matriz de todos los duplicados.

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

soltar .first para devolver una matriz de todos los duplicados.

Ambos métodos regresan nil si no hay duplicados.

I propuse queArray#difference ser añadido al núcleo Ruby. Más información está en mi respuesta aquí .

Punto de referencia

Comparemos los métodos sugeridos. Primero, necesitamos una matriz para probar:

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

y un método para ejecutar los puntos de referencia para diferentes matrices de prueba:

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

No incluí la respuesta de @ JjP porque solo se debe devolver un duplicado, y cuando se modifica su respuesta para hacerlo, es lo mismo que la respuesta anterior de @ Naveed. Tampoco incluí la respuesta de @ Marin, que, aunque se publicó antes de la respuesta de @ Naveed, devolvió todos los duplicados en lugar de solo uno (un punto menor, pero no hay ningún punto para evaluar ambos, ya que son idénticos cuando devuelven solo un duplicado).

También modifiqué otras respuestas que devolvieron todos los duplicados para devolver solo el primero encontrado, pero que esencialmente no debería tener ningún efecto en el rendimiento, ya que calcularon todos los duplicados antes de seleccionar uno.

Los resultados para cada punto de referencia se enumeran del más rápido al más lento:

Primero suponga que la matriz contiene 100 elementos:

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

Ahora considere una matriz con 10,000 elementos:

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

Tenga en cuenta que find_a_dup_using_difference(arr)sería mucho más eficiente siArray#difference se implementara en C, que sería el caso si se agregara al núcleo de Ruby.

Conclusión

Muchas de las respuestas son razonables, pero usar un Set es la mejor opción . Es más rápido en los casos de dureza media, la unión más rápida en los casos más difíciles y solo computacionalmente triviales, cuando su elección no importará de todos modos, puede ser vencido.

El único caso muy especial en el que podría elegir la solución de Chris sería si desea utilizar el método para desduplicar por separado miles de matrices pequeñas y espera encontrar un duplicado que generalmente contiene menos de 10 elementos. Esto será un poco más rápido ya que evita la pequeña sobrecarga adicional de crear el Conjunto.

Cary Swoveland
fuente
1
Excelente solucion. No es tan obvio lo que sucede al principio como algunos de los métodos, pero debería ejecutarse en un tiempo verdaderamente lineal, a expensas de un poco de memoria.
Chris Heald
Con find_a_dup_using_set, obtengo el Set back, en lugar de uno de los duplicados. Además, no puedo encontrar "find.with_object" en Ruby docs en ningún lado.
ScottJ
@Scottj, gracias por la captura! Es interesante que nadie haya captado eso antes ahora. Lo arreglé. Eso es Enumerable # find encadenado a Enumerator # with_object . Actualizaré los puntos de referencia, agregando su solución y otros.
Cary Swoveland
1
Excelente comparación @CarySwoveland
Naveed
19

Por desgracia, la mayoría de las respuestas son O(n^2).

Aquí hay una O(n)solución,

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

¿Cuál es la complejidad de esto?

  • Corre en O(n) y se rompe en el primer partido
  • Utiliza O(n)memoria, pero solo la cantidad mínima

Ahora, dependiendo de la frecuencia con la que haya duplicados en su matriz, estos tiempos de ejecución podrían ser aún mejores. Por ejemplo, si la matriz de tamaño O(n)se ha muestreado de una población de k << nelementos diferentes, solo se vuelve la complejidad tanto para el tiempo de ejecución como para el espacio O(k), sin embargo, es más probable que el póster original esté validando la entrada y quiera asegurarse de que no haya duplicados. En ese caso, tanto el tiempo de ejecución como la complejidad de la memoria O(n)ya que esperamos que los elementos no tengan repeticiones para la mayoría de las entradas.

akuhn
fuente
15

Objetos Array rubí tienen un gran método, select.

select {|item| block }  new_ary
select  an_enumerator

La primera forma es lo que te interesa aquí. Le permite seleccionar objetos que pasan una prueba.

Objetos Array de Ruby tienen otro método, count.

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

En este caso, le interesan los duplicados (objetos que aparecen más de una vez en la matriz). La prueba adecuada es a.count(obj) > 1.

Si a = ["A", "B", "C", "B", "A"], entonces

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

Usted declara que solo quiere un objeto. Así que elige uno.

Martín Vélez
fuente
1
Me gusta mucho, pero tienes que lanzar un uniq al final o obtendrás["A", "B", "B", "A"]
Joeyjoejoejr
1
Gran respuesta. Esto es exactamente lo que estaba buscando. Como señaló @Joeyjoejoejr. He enviado una edición para poner .uniqen la matriz.
Surya
Esto es enormemente ineficiente. No solo encuentra todos los duplicados y luego tira todos menos uno, sino que invoca countpara cada elemento de la matriz, lo cual es un desperdicio e innecesario. Vea mi comentario sobre la respuesta de JjP.
Cary Swoveland
Gracias por ejecutar los puntos de referencia. Es útil ver cómo se comparan las diferentes soluciones en tiempo de ejecución. Las respuestas elegantes son legibles, pero a menudo no son las más eficientes.
Martin Velez
9

find_all () devuelve un que arraycontiene todos los elementos de lo enumque blockno es false.

Para obtener duplicateelementos

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

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

O uniqelementos duplicados

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

Algo como esto funcionará

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 }

Es decir, coloque todos los valores en un hash donde la clave es el elemento de la matriz y el valor es el número de ocurrencias. Luego seleccione todos los elementos que ocurran más de una vez. Fácil.

Sergio Tulentsev
fuente
7

Sé que este hilo trata específicamente de Ruby, pero llegué aquí buscando cómo hacer esto dentro del contexto de Ruby on Rails con ActiveRecord y pensé que también compartiría mi solución.

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

Lo anterior devuelve una matriz de todas las direcciones de correo electrónico que están duplicadas en la tabla de base de datos de este ejemplo (que en Rails sería "active_record_classes").

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

Este es un O(n)procedimiento.

Alternativamente, puede hacer cualquiera de las siguientes líneas. También O (n) pero solo una iteració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]
benzhang
fuente
2

Aquí está mi opinión sobre un gran conjunto de datos, como una tabla de dBase heredada para encontrar partes duplicadas

# 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
Konung
fuente
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)
dorio
fuente
1

each_with_object ¡es tu amigo!

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}
Tilo
fuente
1

Este código devolverá la lista de valores duplicados. Las claves hash se utilizan como una forma eficiente de verificar qué valores ya se han visto. Según si se ha visto el valor, la matriz original aryse divide en 2 matrices: la primera contiene valores únicos y la segunda contiene duplicados.

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

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

=> ["hello"]

Puede acortarlo aún más, aunque a costa de una sintaxis un poco más compleja, de esta forma:

hash={}
arr.partition { |v| !hash.has_key?(v) && hash[v]=0 }.last.uniq
Cryptogopher
fuente
0
a = ["A", "B", "C", "B", "A"]
b = a.select {|e| a.count(e) > 1}.uniq
c = a - b
d = b + c

Resultados

 d
=> ["A", "B", "C"]
Amrit Dhungana
fuente
0

Si está comparando dos matrices diferentes (en lugar de una contra sí misma), una forma muy rápida es utilizar el operador de intersección &proporcionado por la clase Ruby's Array .

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

# Then this...
a & b # => ['c', 'd']
IAmNaN
fuente
1
Eso encuentra elementos que existen en ambas matrices, no duplicados en una matriz.
Kimmo Lehto
Gracias por señalar eso. He cambiado la redacción en mi respuesta. Lo dejaré aquí porque ya ha demostrado ser útil para algunas personas que vienen de la búsqueda.
IAmNaN
0

Necesitaba averiguar cuántos duplicados había y cuáles eran, así que escribí una función basada en lo que Naveed había publicado anteriormente:

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
muneebahmad
fuente
-1
  1. Creemos un método de duplicación que tome una matriz de elementos como entrada
  2. En el cuerpo del método, creemos 2 nuevos objetos de matriz, uno se ve y otro está duplicado
  3. finalmente, iteremos a través de cada objeto en una matriz dada y para cada iteración, encontremos que ese objeto existía en la matriz vista.
  4. si el objeto existía en la matriz seen_, entonces se considera como un objeto duplicado y empuja ese objeto a la matriz_de_ duplicación
  5. si el objeto no existía en lo visto, entonces se considera como un objeto único y lo empuja a matriz_visible

demostremos en Implementación de Código

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

Ahora llame al método de duplicación y al resultado de retorno de salida:

dup_elements = duplication [1,2,3,4,4,5,6,6]
puts dup_elements.inspect
Yugesh Palvai
fuente
Las respuestas de solo código generalmente están mal vistas en este sitio. ¿Podría editar su respuesta para incluir algunos comentarios o explicaciones de su código? Las explicaciones deberían responder preguntas como: ¿Qué hace? Como lo hace ¿A dónde va? ¿Cómo resuelve el problema de OP? Ver: Cómo responder . ¡Gracias!
Eduardo Baitello
-4

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

Note que lo anterior es destructivo

Max
fuente
esto no devuelve valores duplicados
andriy-baran