Come contare elementi stringa identici in un array Ruby


92

Ho il seguente Array = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]

Come faccio a produrre un conteggio per ogni elemento identico ?

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

o produrre un hash Dove:

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


3
A partire da Ruby 2.7 puoi usare Enumerable#tally. Maggiori info qui .
SRack

Risposte:


83
names = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]
counts = Hash.new(0)
names.each { |name| counts[name] += 1 }
# => {"Jason" => 2, "Teresa" => 1, ....

128
names.inject(Hash.new(0)) { |total, e| total[e] += 1 ;total}

ti dà

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

3
+1 Come la risposta selezionata, ma preferisco l'uso di inject e nessuna variabile "esterna".

18
Se usi each_with_objectinvece di injectnon devi restituire ( ;total) al blocco.
mfilej

13
Per i posteri, questo è ciò che significa @mfilej:array.each_with_object(Hash.new(0)){|string, hash| hash[string] += 1}
Gon Zifroni

2
Da Rubino 2.7, si può semplicemente fare: names.tally.
Hallgeir Wilhelmsen

104

Ruby v2.7 + (più recente)

A partire da ruby ​​v2.7.0 (rilasciato a dicembre 2019), il linguaggio di base ora include Enumerable#tallyun nuovo metodo , progettato specificamente per questo 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 + (attualmente supportato, ma precedente)

Il codice seguente non era possibile in ruby ​​standard quando questa domanda è stata posta per la prima volta (febbraio 2011), poiché utilizza:

  • Object#itself, che è stato aggiunto a Ruby v2.2.0 (rilasciato a dicembre 2014).
  • Hash#transform_values, che è stato aggiunto a Ruby v2.4.0 (rilasciato a dicembre 2016).

Queste aggiunte moderne a Ruby consentono la seguente implementazione:

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 + (deprecato)

Se si utilizza una versione precedente di Ruby, senza accesso al Hash#transform_valuesmetodo sopra menzionato , è possibile invece utilizzare Array#to_h, che è stata aggiunta a Ruby v2.1.0 (rilasciata a dicembre 2013):

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

Anche per le versioni più vecchie di ruby ​​( <= 2.1), ci sono diversi modi per risolvere questo problema, ma (a mio parere) non esiste un modo "migliore" chiaro. Vedi le altre risposte a questo post.


Stavo per postare: P. C'è qualche differenza evidente tra l'utilizzo al countposto di size/ length?
ghiaccio ツ

1
@SagarPandya No, non c'è differenza. A differenza di Array#sizee Array#length, Array#count può accettare un argomento o un blocco opzionale; ma se usato con nessuno dei due, la sua implementazione è identica. Più specificamente, tutti e tre i metodi chiamano LONG2NUM(RARRAY_LEN(ary))sotto il cofano: count / length
Tom Lord

1
Questo è un bell'esempio di Ruby idiomatico. Bella risposta.
slhck

1
Credito extra! Ordina per conteggio.group_by(&:itself).transform_values(&:count).sort_by{|k, v| v}.reverse
Abram

2
@ Abram puoi sort_by{ |k, v| -v}, non è reversenecessario! ;-)
Sony Santos

26

Ora usando Ruby 2.2.0 puoi sfruttare il itselfmetodo .

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}

3
D'accordo, ma preferisco leggermente names.group_by (& :self) .map {| k, v | [k, v.count]}. to_h in modo da non dover mai dichiarare un oggetto hash
Andy Day

8
@andrewkday Facendo un ulteriore passo avanti, ruby ​​v2.4 ha aggiunto il metodo: Hash#transform_valuesche ci consente di semplificare ancora di più il tuo codice:names.group_by(&:itself).transform_values(&:count)
Tom Lord

Inoltre, questo è un punto molto sottile (che probabilmente non è più rilevante per i futuri lettori!), Ma nota che anche il tuo codice utilizza Array#to_h- che è stato aggiunto a Ruby v2.1.0 (rilasciato a dicembre 2013 - cioè quasi 3 anni dopo la domanda originale è stato chiesto!)
Tom Lord

17

In realtà c'è una struttura di dati che fa questo: MultiSet.

Sfortunatamente, non c'è alcuna MultiSetimplementazione nella libreria principale di Ruby o nella libreria standard, ma ci sono un paio di implementazioni che fluttuano sul web.

Questo è un ottimo esempio di come la scelta di una struttura dati possa semplificare un algoritmo. In effetti, in questo particolare esempio, l'algoritmo scompare completamente . È letteralmente solo:

Multiset.new(*names)

E questo è tutto. Esempio, utilizzando 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

Esempio, utilizzando 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'>

Il concetto MultiSet ha origine dalla matematica o da un altro linguaggio di programmazione?
Andrew Grimm,

2
@Andrew Grimm: Sia la parola "multiset" (de Bruijn, anni '70) e il concetto (Dedekind 1888) hanno avuto origine in matematica. Multisetè governato da rigide regole matematiche e supporta le tipiche operazioni sugli insiemi (unione, intersezione, complemento, ...) in un modo che è per lo più coerente con gli assiomi, le leggi e i teoremi della teoria matematica "normale" degli insiemi, sebbene alcune leggi importanti lo facciano non tenere quando si tenta di generalizzarli a multiset. Ma è ben oltre la mia comprensione della questione. Li uso come struttura dati di programmazione, non come concetto matematico.
Jörg W Mittag

Per espandere un po ' questo punto: "... in un modo che è per lo più coerente con gli assiomi ..." : Gli insiemi "normali" sono solitamente definiti formalmente da un insieme di assiomi (assunzioni) chiamato "teoria degli insiemi di Zermelo-Frankel ". Tuttavia, uno di questi assiomi: l' assioma di estensionalità afferma che un insieme è definito precisamente dai suoi membri - es {A, A, B} = {A, B}. Questa è chiaramente una violazione della definizione stessa di multi-set!
Tom Lord

... Tuttavia, senza entrare troppo nei dettagli (poiché questo è un forum software, non matematica avanzata!), È possibile definire formalmente multi-set matematicamente tramite assiomi per i set Crisp, gli assiomi di Peano e altri assiomi specifici di MultiSet.
Tom Lord

13

Enumberable#each_with_object ti evita di restituire l'hash finale.

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

Ritorna:

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

each_with_objectinject
D'

9

Ruby 2.7+

Ruby 2.7 sta introducendo proprio Enumerable#tallyper questo scopo. C'è un buon riassunto qui .

In questo caso d'uso:

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

I documenti sulle funzionalità in rilascio sono qui .

Spero che questo aiuti qualcuno!


Notizie fantastiche!
tadman

6

Funziona.

arr = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]
result = {}
arr.uniq.each{|element| result[element] = arr.count(element)}

2
+1 Per un approccio diverso - sebbene abbia una complessità teorica peggiore - O(n^2)(che avrà importanza per alcuni valori di n) e fa un lavoro extra (deve contare per "Judah" 3x, per esempio) !. Suggerirei anche eachinvece di map(il risultato della mappa viene scartato)

Grazie per quello! Ho cambiato la mappa in ognuna. Inoltre, ho uniq'ed l'array prima di esaminarlo. Forse ora il problema della complessità è risolto?
Shreyas,

6

Quello che segue è uno stile di programmazione leggermente più funzionale:

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]]

Un vantaggio di group_byè che puoi usarlo per raggruppare elementi equivalenti ma non esattamente identici:

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]]

Ho sentito la programmazione funzionale? +1 :-) Questo è sicuramente il modo migliore, anche se si può sostenere che non è efficiente in termini di memoria. Si noti inoltre che Facets ha una frequenza 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}

Credito Frank Wambutt


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}

2

Molte ottime implementazioni qui.

Ma come principiante lo considererei il più facile da leggere e implementare

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}

I passi che abbiamo fatto:

  • abbiamo creato l'hash
  • abbiamo eseguito un loop namessull'array
  • abbiamo contato quante volte ogni nome è apparso namesnell'array
  • abbiamo creato una chiave usando il namee un valore usando ilcount

Potrebbe essere leggermente più prolisso (e dal punto di vista delle prestazioni farai del lavoro non necessario con le chiavi di override), ma a mio parere più facile da leggere e capire per quello che vuoi ottenere


2
Non vedo come sia più facile da leggere rispetto alla risposta accettata, ed è chiaramente un progetto peggiore (facendo un sacco di lavoro non necessario).
Tom Lord

@Tom Lord - Sono d'accordo con te sulle prestazioni (l'ho anche menzionato nella mia risposta) - ma come principiante che cerca di capire il codice effettivo e i passaggi richiesti, trovo che aiuti essere più verbosi e quindi si può refactoring per migliorare performance e rendere il codice più dichiarativo
Sami Birnbaum

1
Sono in qualche modo d'accordo con @SamiBirnbaum. Questo è l'unico che non utilizza quasi nessuna conoscenza speciale del rubino Hash.new(0). Il più vicino allo pseudocodice. Questa può essere una buona cosa per la leggibilità, ma anche fare un lavoro non necessario può danneggiare la leggibilità per i lettori che lo notano perché in casi più complessi passeranno un po 'di tempo a pensare che stanno impazzendo cercando di capire perché è fatto.
Adamantish

1

Questo è più un commento che una risposta, ma un commento non gli renderebbe giustizia. In tal caso Array = foo, si blocca almeno un'implementazione di 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>

Questo perché Arrayè una classe.


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

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

Tempo trascorso 0,028 millisecondi

È interessante notare che l'implementazione di stupidgeek ha confrontato:

Tempo trascorso 0,041 millisecondi

e la risposta vincente:

Tempo trascorso 0,011 millisecondi

:)

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.