Comment compter les éléments de chaîne identiques dans un tableau Ruby


91

J'ai ce qui suit Array = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]

Comment produire un décompte pour chaque élément identique ?

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

ou produire un hash Où:

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


2
Depuis Ruby 2.7, vous pouvez utiliser Enumerable#tally. Plus d'infos ici .
SRack

Réponses:


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

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

vous donne

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

3
+1 Comme la réponse choisie, mais je préfère l'utilisation d'inject et aucune variable «externe».

18
Si vous utilisez à la each_with_objectplace de injectvous n'avez pas à retourner ( ;total) au bloc.
mfilej

12
Pour la postérité, voici ce que signifie @mfilej:array.each_with_object(Hash.new(0)){|string, hash| hash[string] += 1}
Gon Zifroni

2
De Ruby 2.7, vous pouvez simplement faire: names.tally.
Hallgeir Wilhelmsen

99

Ruby v2.7 + (dernière)

À partir de ruby ​​v2.7.0 (publié en décembre 2019), le langage de base comprend désormais Enumerable#tally- une nouvelle méthode , conçue spécifiquement pour ce problème:

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

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

Ruby v2.4 + (actuellement pris en charge, mais plus ancien)

Le code suivant n'était pas possible en rubis standard lorsque cette question a été posée pour la première fois (février 2011), car il utilise:

Ces ajouts modernes à Ruby permettent l'implémentation suivante:

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 + (obsolète)

Si vous utilisez une ancienne version de Ruby , sans accès à la Hash#transform_valuesméthode mentionnée ci-dessus , vous pouvez utiliser à la place Array#to_h, qui a été ajoutée à Ruby v2.1.0 (publiée en décembre 2013):

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

Pour les versions ruby ​​encore plus anciennes ( <= 2.1), il existe plusieurs façons de résoudre ce problème, mais (à mon avis) il n'y a pas de "meilleure" façon claire. Voir les autres réponses à ce post.


J'étais sur le point de poster: P. Y a-t-il une différence perceptible entre utiliser countau lieu de size/ length?
glace ツ

1
@SagarPandya Non, il n'y a pas de différence. Contrairement à Array#sizeet Array#length, Array#count peut prendre un argument ou un bloc optionnel; mais s'il est utilisé avec ni l'un ni l'autre, son implémentation est identique. Plus précisément, les trois méthodes appellent LONG2NUM(RARRAY_LEN(ary))sous le capot: nombre / longueur
Tom Lord

1
C'est un si bel exemple de Ruby idiomatique. Très bonne réponse.
slhck

1
Crédit supplémentaire! Trier par nombre.group_by(&:itself).transform_values(&:count).sort_by{|k, v| v}.reverse
Abram

2
@Abram vous pouvez sort_by{ |k, v| -v}, pas reversebesoin! ;-)
Sony Santos

26

Maintenant, en utilisant Ruby 2.2.0, vous pouvez tirer parti de la itselfméthode .

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'accord, mais je préfère légèrement names.group_by (&: lui-même) .map {| k, v | [k, v.count]}. to_h pour que vous n'ayez jamais à déclarer un objet de hachage
Andy Day

8
@andrewkday Pour aller plus loin, ruby ​​v2.4 a ajouté la méthode: Hash#transform_valuesqui nous permet de simplifier encore plus votre code:names.group_by(&:itself).transform_values(&:count)
Tom Lord

C'est aussi un point très subtil (qui n'est probablement plus pertinent pour les futurs lecteurs!), Mais notez que votre code utilise également Array#to_h - qui a été ajouté à Ruby v2.1.0 (publié en décembre 2013 - soit près de 3 ans après la question initiale a été demandé!)
Tom Lord

17

Il y a en fait une structure de données qui le fait: MultiSet.

Malheureusement, il n'y a pas d' MultiSetimplémentation dans la bibliothèque principale de Ruby ou dans la bibliothèque standard, mais quelques implémentations flottent sur le Web.

Ceci est un excellent exemple de la façon dont le choix d'une structure de données peut simplifier un algorithme. En fait, dans cet exemple particulier, l'algorithme disparaît même complètement . C'est littéralement juste:

Multiset.new(*names)

Et c'est tout. Exemple, en utilisant 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

Exemple, en utilisant 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'>

Le concept MultiSet provient-il des mathématiques ou d'un autre langage de programmation?
Andrew Grimm

2
@Andrew Grimm: Le mot "multiset" (de Bruijn, 1970s) et le concept (Dedekind 1888) sont issus des mathématiques. Multisetest régi par des règles mathématiques strictes et prend en charge les opérations d'ensembles typiques (union, intersection, complément, ...) d'une manière qui est principalement cohérente avec les axiomes, lois et théorèmes de la théorie mathématique des ensembles "normale", bien que certaines lois importantes le fassent ne tient pas lorsque vous essayez de les généraliser en multisets. Mais c'est bien au-delà de ma compréhension de la question. Je les utilise comme une structure de données de programmation, pas comme un concept mathématique.
Jörg W Mittag

Pour développer un peu sur ce point: "... d'une manière qui est principalement cohérente avec les axiomes ..." : Les ensembles "normaux" sont généralement formellement définis par un ensemble d'axiomes (hypothèses) appelé "théorie des ensembles de Zermelo-Frankel ". Cependant, l'un de ces axiomes: l' axiome d'extensionnalité déclare qu'un ensemble est défini précisément par ses membres - par exemple {A, A, B} = {A, B}. C'est clairement une violation de la définition même des multi-ensembles!
Tom Lord

... Cependant, sans entrer dans les détails (car il s'agit d'un forum logiciel, pas de mathématiques avancées!), On peut formellement définir mathématiquement des multi-ensembles via des axiomes pour les ensembles Crisp, les axiomes Peano et d'autres axiomes spécifiques à MultiSet.
Tom Lord

13

Enumberable#each_with_object vous évite de renvoyer le hachage final.

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

Retour:

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

D'accord, la each_with_objectvariante est plus lisible pour moi queinject
Lev Lukomsky

9

Ruby 2.7+

Ruby 2.7 introduit Enumerable#tallyprécisément dans ce but. Il y a un bon résumé ici .

Dans ce cas d'utilisation:

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

La documentation sur les fonctionnalités publiées est ici .

J'espère que cela aide quelqu'un!


Fantastique nouvelle!
tadman le

6

Cela marche.

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

2
+1 Pour une approche différente - bien que cela ait une complexité théorique pire - O(n^2)(qui importera pour certaines valeurs de n) et fait un travail supplémentaire (cela doit compter pour "Juda" 3x, par exemple) !. Je suggérerais également eachau lieu de map(le résultat de la carte est en cours de

Merci pour ça! J'ai changé la carte pour chacun et j'ai uniq'ed le tableau avant de le parcourir. Peut-être que maintenant le problème de complexité est résolu?
Shreyas

6

Voici un style de programmation légèrement plus fonctionnel:

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

L'un des avantages de group_byest que vous pouvez l'utiliser pour regrouper des éléments équivalents mais pas exactement identiques:

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

Ai-je entendu la programmation fonctionnelle? +1 :-) C'est certainement le meilleur moyen, bien que l'on puisse affirmer que ce n'est pas efficace en mémoire. Notez également que Facets a une fréquence # 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édit 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

Beaucoup de bonnes implémentations ici.

Mais en tant que débutant, je considérerais cela comme le plus facile à lire et à mettre en œuvre

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}

Les étapes que nous avons prises:

  • nous avons créé le hachage
  • nous avons bouclé le names tableau
  • nous avons compté combien de fois chaque nom est apparu dans le namestableau
  • nous avons créé une clé en utilisant nameet une valeur en utilisant lecount

Cela peut être légèrement plus détaillé (et en termes de performances, vous ferez un travail inutile avec des touches de priorité), mais à mon avis, plus facile à lire et à comprendre pour ce que vous voulez réaliser


2
Je ne vois pas en quoi c'est plus facile à lire que la réponse acceptée, et c'est clairement une conception pire (faire beaucoup de travail inutile).
Tom Lord

@Tom Lord - Je suis d'accord avec vous sur les performances (j'ai même mentionné cela dans ma réponse) - mais en tant que débutant essayant de comprendre le code réel et les étapes requises, je trouve qu'il est utile d'être plus verbeux et ensuite on peut refactoriser pour s'améliorer performance et rendre le code plus déclaratif
Sami Birnbaum

1
Je suis quelque peu d'accord avec @SamiBirnbaum. C'est le seul qui n'utilise presque aucune connaissance spéciale du rubis Hash.new(0). Le plus proche du pseudocode. Cela peut être une bonne chose pour la lisibilité, mais faire un travail inutile peut également nuire à la lisibilité pour les lecteurs qui le remarquent, car dans des cas plus complexes, ils passeront un peu de temps à penser qu'ils deviennent fous à essayer de comprendre pourquoi cela est fait.
Adamantish

1

C'est plus un commentaire qu'une réponse, mais un commentaire ne lui rendrait pas justice. Si vous le faites Array = foo, vous plantez au moins une implémentation d'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>

C'est parce que Arrayc'est une classe.


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

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

Temps écoulé 0,028 millisecondes

Fait intéressant, l'implémentation de stupidgeek a comparé:

Temps écoulé 0,041 millisecondes

et la réponse gagnante:

Temps écoulé 0,011 millisecondes

:)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.