Cómo contar elementos de cadena idénticos en una matriz Ruby

91

Tengo lo siguiente Array = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]

¿Cómo produzco un recuento para cada elemento idéntico ?

Where:
"Jason" = 2, "Judah" = 3, "Allison" = 1, "Teresa" = 1, "Michelle" = 1?

o producir un hachís Donde:

Donde: hash = {"Jason" => 2, "Judah" => 3, "Allison" => 1, "Teresa" => 1, "Michelle" => 1}

user398520
fuente
2
A partir de Ruby 2.7 puede usar Enumerable#tally. Más info aquí .
SRack el

Respuestas:

82
names = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]
counts = Hash.new(0)
names.each { |name| counts[name] += 1 }
# => {"Jason" => 2, "Teresa" => 1, ....
Dylan Markow
fuente
127
names.inject(Hash.new(0)) { |total, e| total[e] += 1 ;total}

te dio

{"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1} 
Mauricio
fuente
3
+1 Me gusta la respuesta seleccionada, pero prefiero el uso de inyectar y no una variable "externa".
18
Si usa en each_with_objectlugar de inject, no tiene que regresar ( ;total) en el bloque.
mfilej
12
Para la posteridad, esto es lo que significa @mfilej:array.each_with_object(Hash.new(0)){|string, hash| hash[string] += 1}
Gon Zifroni
2
De Rubí 2.7, sólo tiene que hacer: names.tally.
Hallgeir Wilhelmsen
99

Ruby v2.7 + (más reciente)

A partir de ruby ​​v2.7.0 (lanzado en diciembre de 2019), el lenguaje principal ahora incluye Enumerable#tallyun nuevo método diseñado específicamente para este problema:

names = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]

names.tally
#=> {"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1}

Ruby v2.4 + (actualmente compatible, pero más antiguo)

El siguiente código no era posible en ruby ​​estándar cuando se hizo esta pregunta por primera vez (febrero de 2011), ya que usa:

Estas modernas adiciones a Ruby permiten la siguiente implementación:

names = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]

names.group_by(&:itself).transform_values(&:count)
#=> {"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1}

Ruby v2.2 + (obsoleto)

Si usa una versión anterior de Ruby, sin acceso al Hash#transform_valuesmétodo mencionado anteriormente , puede usar Array#to_h, que se agregó a Ruby v2.1.0 (lanzado en diciembre de 2013):

names.group_by(&:itself).map { |k,v| [k, v.length] }.to_h
#=> {"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1}

Para versiones aún más antiguas de ruby ​​( <= 2.1), hay varias formas de resolver esto, pero (en mi opinión) no existe una "mejor" forma clara. Vea las otras respuestas a esta publicación.

Tom Lord
fuente
Estaba a punto de publicar: P. ¿Hay alguna diferencia discernible entre usar en countlugar de size/ length?
hielo ツ
1
@SagarPandya No, no hay diferencia. A diferencia de Array#sizey Array#length, Array#count puede tomar un argumento o bloque opcional; pero si se usa con ninguno, entonces su implementación es idéntica. Más específicamente, los tres métodos llaman LONG2NUM(RARRAY_LEN(ary))bajo el capó: recuento / longitud
Tom Lord
1
Este es un buen ejemplo de Ruby idiomático. Gran respuesta.
slhck
1
¡Crédito adicional! Ordenar por conteo.group_by(&:itself).transform_values(&:count).sort_by{|k, v| v}.reverse
Abram
2
@Abram puedes sort_by{ |k, v| -v}, ¡no es reversenecesario! ;-)
Sony Santos
26

Ahora, con Ruby 2.2.0 puede aprovechar el itselfmétodo .

names = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]
counts = {}
names.group_by(&:itself).each { |k,v| counts[k] = v.length }
# counts > {"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1}
Ahmed Fahmy
fuente
3
De acuerdo, pero prefiero un poco names.group_by (&: sí mismo) .map {| k, v | [k, v.count]}. to_h para que no tenga que declarar nunca un objeto hash
Andy Day
8
@andrewkday Dando un paso más allá, ruby ​​v2.4 agregó el método: Hash#transform_valuesque nos permite simplificar su código aún más:names.group_by(&:itself).transform_values(&:count)
Tom Lord
Además, este es un punto muy sutil (¡que probablemente ya no sea relevante para los lectores futuros!), Pero tenga en cuenta que su código también usa Array#to_h, que se agregó a Ruby v2.1.0 (lanzado en diciembre de 2013, es decir, casi 3 años después de la pregunta original se preguntó!)
Tom Lord
17

De hecho, hay una estructura de datos que hace esto: MultiSet.

Desafortunadamente, no hay MultiSetimplementación en la biblioteca central de Ruby o en la biblioteca estándar, pero hay un par de implementaciones flotando en la web.

Este es un gran ejemplo de cómo la elección de una estructura de datos puede simplificar un algoritmo. De hecho, en este ejemplo en particular, el algoritmo incluso desaparece por completo . Es literalmente solo:

Multiset.new(*names)

Y eso es. Ejemplo, usando https://GitHub.Com/Josh/Multimap/ :

require 'multiset'

names = %w[Jason Jason Teresa Judah Michelle Judah Judah Allison]

histogram = Multiset.new(*names)
# => #<Multiset: {"Jason", "Jason", "Teresa", "Judah", "Judah", "Judah", "Michelle", "Allison"}>

histogram.multiplicity('Judah')
# => 3

Ejemplo, usando http://maraigue.hhiro.net/multiset/index-en.php :

require 'multiset'

names = %w[Jason Jason Teresa Judah Michelle Judah Judah Allison]

histogram = Multiset[*names]
# => #<Multiset:#2 'Jason', #1 'Teresa', #3 'Judah', #1 'Michelle', #1 'Allison'>
Jörg W Mittag
fuente
¿El concepto MultiSet se origina en las matemáticas u otro lenguaje de programación?
Andrew Grimm
2
@Andrew Grimm: Tanto la palabra "multiset" (de Bruijn, 1970) como el concepto (Dedekind 1888) se originaron en las matemáticas. Multisetse rige por reglas matemáticas estrictas y soporta las operaciones típicas de conjuntos (unión, intersección, complemento, ...) de una manera que es mayormente consistente con los axiomas, leyes y teoremas de la teoría matemática de conjuntos "normal", aunque algunas leyes importantes lo hacen no se mantenga cuando intente generalizarlos a conjuntos múltiples. Pero eso está mucho más allá de mi comprensión del asunto. Los uso como una estructura de datos de programación, no como un concepto matemático.
Jörg W Mittag
Para ampliar un poco ese punto: "... de una manera que es mayormente consistente con los axiomas ..." : Los conjuntos "normales" generalmente se definen formalmente mediante un conjunto de axiomas (supuestos) llamado "teoría de conjuntos de Zermelo-Frankel ". Sin embargo, uno de estos axiomas: el axioma de extensionalidad establece que un conjunto se define precisamente por sus miembros, por ejemplo {A, A, B} = {A, B}. ¡Esto es claramente una violación de la definición misma de conjuntos múltiples!
Tom Lord
... Sin embargo, sin entrar en demasiados detalles (¡ya que este es un foro de software, no de matemáticas avanzadas!), Uno puede definir formalmente multi-conjuntos matemáticamente a través de axiomas para Crisp sets, los axiomas de Peano y otros axiomas específicos de MultiSet.
Tom Lord
13

Enumberable#each_with_object te evita devolver el hash final.

names.each_with_object(Hash.new(0)) { |name, hash| hash[name] += 1 }

Devoluciones:

=> {"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1}
Anconia
fuente
De acuerdo, la each_with_objectvariante es más legible para mí queinject
Lev Lukomsky
9

Ruby 2.7+

Ruby 2.7 se presenta Enumerable#tallycon este propósito exacto. Hay un buen resumen aquí .

En este caso de uso:

array.tally
# => { "Jason" => 2, "Judah" => 3, "Allison" => 1, "Teresa" => 1, "Michelle" => 1 }

Los documentos sobre las funciones que se lanzarán están aquí .

¡Espero que esto ayude a alguien!

SRack
fuente
¡Noticias fantásticas!
tadman
6

Esto funciona.

arr = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]
result = {}
arr.uniq.each{|element| result[element] = arr.count(element)}
Shreyas
fuente
2
+1 Para un enfoque diferente - aunque esto tiene una complejidad teórica peor - O(n^2)(lo que importará para algunos valores de n) y hace un trabajo extra (¡tiene que contar para "Judá" 3x, por ejemplo) !. También sugeriría en eachlugar de map(el resultado del mapa se descarta)
¡Gracias por eso! He cambiado el mapa a cada uno. Además, he unificado la matriz antes de revisarla. ¿Quizás ahora el problema de la complejidad esté resuelto?
Shreyas
6

El siguiente es un estilo de programación un poco más funcional:

array_with_lower_case_a = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]
hash_grouped_by_name = array_with_lower_case_a.group_by {|name| name}
hash_grouped_by_name.map{|name, names| [name, names.length]}
=> [["Jason", 2], ["Teresa", 1], ["Judah", 3], ["Michelle", 1], ["Allison", 1]]

Una ventaja group_byes que puede usarlo para agrupar elementos equivalentes pero no exactamente idénticos:

another_array_with_lower_case_a = ["Jason", "jason", "Teresa", "Judah", "Michelle", "Judah Ben-Hur", "JUDAH", "Allison"]
hash_grouped_by_first_name = another_array_with_lower_case_a.group_by {|name| name.split(" ").first.capitalize}
hash_grouped_by_first_name.map{|first_name, names| [first_name, names.length]}
=> [["Jason", 2], ["Teresa", 1], ["Judah", 3], ["Michelle", 1], ["Allison", 1]]
Andrew Grimm
fuente
¿Escuché programación funcional? +1 :-) Esta es definitivamente la mejor manera, aunque se puede argumentar que no es eficiente en memoria. Observe también que Facetas tiene una frecuencia # Enumerable.
tokland
5
a = [1, 2, 3, 2, 5, 6, 7, 5, 5]
a.each_with_object(Hash.new(0)) { |o, h| h[o] += 1 }

# => {1=>1, 2=>2, 3=>1, 5=>3, 6=>1, 7=>1}

Crédito Frank Wambutt

narzero
fuente
3
names = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]
Hash[names.group_by{|i| i }.map{|k,v| [k,v.size]}]
# => {"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1}
Arup Rakshit
fuente
2

Muchas implementaciones geniales aquí.

Pero como principiante, consideraría que este es el más fácil de leer e implementar.

names = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]

name_frequency_hash = {}

names.each do |name|
  count = names.count(name)
  name_frequency_hash[name] = count  
end
#=> {"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1}

Los pasos que dimos:

  • creamos el hash
  • hicimos un bucle sobre el names matriz
  • contamos cuántas veces apareció cada nombre en la namesmatriz
  • Creamos una clave usando el namey un valor usando elcount

Puede ser un poco más detallado (y en términos de rendimiento, estará haciendo un trabajo innecesario con teclas anuladas), pero en mi opinión, es más fácil de leer y comprender para lo que desea lograr.

Sami Birnbaum
fuente
2
No veo cómo eso es más fácil de leer que la respuesta aceptada, y claramente es un diseño peor (hacer mucho trabajo innecesario).
Tom Lord
@Tom Lord: estoy de acuerdo con usted en el rendimiento (incluso lo mencioné en mi respuesta), pero como principiante que trata de comprender el código real y los pasos requeridos, encuentro que ayuda ser más detallado y luego uno puede refactorizar para mejorar rendimiento y hacer código más declarativo
Sami Birnbaum
1
Estoy algo de acuerdo con @SamiBirnbaum. Este es el único que casi no utiliza ningún conocimiento especial de rubí como Hash.new(0). El más cercano al pseudocódigo. Eso puede ser bueno para la legibilidad, pero también hacer un trabajo innecesario puede dañar la legibilidad para los lectores que lo notan porque en casos más complejos pasarán un poco de tiempo pensando que se están volviendo locos tratando de averiguar por qué se hace.
Adamantish
1

Esto es más un comentario que una respuesta, pero un comentario no le haría justicia. Si lo hace Array = foo, bloqueará al menos una implementación de IRB:

C:\Documents and Settings\a.grimm>irb
irb(main):001:0> Array = nil
(irb):1: warning: already initialized constant Array
=> nil
C:/Ruby19/lib/ruby/site_ruby/1.9.1/rbreadline.rb:3177:in `rl_redisplay': undefined method `new' for nil:NilClass (NoMethodError)
        from C:/Ruby19/lib/ruby/site_ruby/1.9.1/rbreadline.rb:3873:in `readline_internal_setup'
        from C:/Ruby19/lib/ruby/site_ruby/1.9.1/rbreadline.rb:4704:in `readline_internal'
        from C:/Ruby19/lib/ruby/site_ruby/1.9.1/rbreadline.rb:4727:in `readline'
        from C:/Ruby19/lib/ruby/site_ruby/1.9.1/readline.rb:40:in `readline'
        from C:/Ruby19/lib/ruby/1.9.1/irb/input-method.rb:115:in `gets'
        from C:/Ruby19/lib/ruby/1.9.1/irb.rb:139:in `block (2 levels) in eval_input'
        from C:/Ruby19/lib/ruby/1.9.1/irb.rb:271:in `signal_status'
        from C:/Ruby19/lib/ruby/1.9.1/irb.rb:138:in `block in eval_input'
        from C:/Ruby19/lib/ruby/1.9.1/irb/ruby-lex.rb:189:in `call'
        from C:/Ruby19/lib/ruby/1.9.1/irb/ruby-lex.rb:189:in `buf_input'
        from C:/Ruby19/lib/ruby/1.9.1/irb/ruby-lex.rb:103:in `getc'
        from C:/Ruby19/lib/ruby/1.9.1/irb/slex.rb:205:in `match_io'
        from C:/Ruby19/lib/ruby/1.9.1/irb/slex.rb:75:in `match'
        from C:/Ruby19/lib/ruby/1.9.1/irb/ruby-lex.rb:287:in `token'
        from C:/Ruby19/lib/ruby/1.9.1/irb/ruby-lex.rb:263:in `lex'
        from C:/Ruby19/lib/ruby/1.9.1/irb/ruby-lex.rb:234:in `block (2 levels) in each_top_level_statement'
        from C:/Ruby19/lib/ruby/1.9.1/irb/ruby-lex.rb:230:in `loop'
        from C:/Ruby19/lib/ruby/1.9.1/irb/ruby-lex.rb:230:in `block in each_top_level_statement'
        from C:/Ruby19/lib/ruby/1.9.1/irb/ruby-lex.rb:229:in `catch'
        from C:/Ruby19/lib/ruby/1.9.1/irb/ruby-lex.rb:229:in `each_top_level_statement'
        from C:/Ruby19/lib/ruby/1.9.1/irb.rb:153:in `eval_input'
        from C:/Ruby19/lib/ruby/1.9.1/irb.rb:70:in `block in start'
        from C:/Ruby19/lib/ruby/1.9.1/irb.rb:69:in `catch'
        from C:/Ruby19/lib/ruby/1.9.1/irb.rb:69:in `start'
        from C:/Ruby19/bin/irb:12:in `<main>'

C:\Documents and Settings\a.grimm>

Eso es porque Arrayes una clase.

Andrew Grimm
fuente
1
arr = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]

arr.uniq.inject({}) {|a, e| a.merge({e => arr.count(e)})}

Tiempo transcurrido 0,028 milisegundos

Curiosamente, la implementación de stupidgeek evaluó:

Tiempo transcurrido 0,041 milisegundos

y la respuesta ganadora:

Tiempo transcurrido 0.011 milisegundos

:)

Alex Moore-Niemi
fuente