Como contar elementos de string idênticos em uma matriz Ruby

92

Eu tenho o seguinte Array = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]

Como faço para produzir uma contagem para cada elemento idêntico ?

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

ou produzir um hash Onde:

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

user398520
fonte
3
A partir do Ruby 2.7 você pode usar Enumerable#tally. Mais informações aqui .
SRack de

Respostas:

83
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
fonte
128
names.inject(Hash.new(0)) { |total, e| total[e] += 1 ;total}

da-te

{"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1} 
Mauricio
fonte
3
+1 Gosto da resposta selecionada, mas prefiro o uso de injetar e nenhuma variável "externa".
18
Se você usar em each_with_objectvez de inject, não precisará retornar ( ;total) no bloco.
mfilej
13
Para a posteridade, @mfilej significa isto:array.each_with_object(Hash.new(0)){|string, hash| hash[string] += 1}
Gon Zifroni
2
De Ruby 2.7, você pode simplesmente fazer: names.tally.
Hallgeir Wilhelmsen de
104

Ruby v2.7 + (mais recente)

A partir do ruby ​​v2.7.0 (lançado em dezembro de 2019), a linguagem central agora inclui Enumerable#tally- um novo método , projetado especificamente 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 + (atualmente compatível, mas mais antigo)

O código a seguir não era possível no ruby ​​padrão quando esta pergunta foi feita pela primeira vez (fevereiro de 2011), pois usa:

  • Object#itself, que foi adicionado ao Ruby v2.2.0 (lançado em dezembro de 2014).
  • Hash#transform_values, que foi adicionado ao Ruby v2.4.0 (lançado em dezembro de 2016).

Essas adições modernas ao Ruby permitem a seguinte implementação:

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)

Se estiver usando uma versão mais antiga do Ruby, sem acesso ao Hash#transform_valuesmétodo mencionado acima , você pode usar o Array#to_h, que foi adicionado ao Ruby v2.1.0 (lançado em dezembro 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 versões de ruby ​​ainda mais antigas ( <= 2.1), existem várias maneiras de resolver isso, mas (na minha opinião) não existe uma "melhor" maneira bem definida. Veja as outras respostas a esta postagem.

Tom Lord
fonte
Eu estava prestes a postar: P. Existe alguma diferença perceptível entre usar em countvez de size/ length?
gelo ツ
1
@SagarPandya Não, não há diferença. Ao contrário de Array#sizee Array#length, Array#count pode receber um argumento ou bloco opcional; mas se usado com nenhum, sua implementação é idêntica. Mais especificamente, todos os três métodos chamam LONG2NUM(RARRAY_LEN(ary))sob o capô: contagem / comprimento
Tom Lord
1
Este é um bom exemplo de Ruby idiomático. Ótima resposta.
slhck de
1
Crédito extra! Classificar por contagem.group_by(&:itself).transform_values(&:count).sort_by{|k, v| v}.reverse
Abrão
2
@Abram você pode sort_by{ |k, v| -v}, não reverseprecisa! ;-)
Sony Santos
26

Agora, usando Ruby 2.2.0, você pode aproveitar o 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
fonte
3
Concordo, mas prefiro um pouco names.group_by (&: próprio) .map {| k, v | [k, v.count]}. to_h para que você não precise declarar um objeto hash
Andy Day
8
@andrewkday Indo além, ruby ​​v2.4 adicionou o método: Hash#transform_valuesque nos permite simplificar seu código ainda mais:names.group_by(&:itself).transform_values(&:count)
Tom Lord
Além disso, este é um ponto muito sutil (que provavelmente não é mais relevante para leitores futuros!), Mas observe que seu código também usa Array#to_h- que foi adicionado ao Ruby v2.1.0 (lançado em dezembro de 2013 - ou seja, quase 3 anos após a pergunta original foi perguntado!)
Tom Lord
17

Na verdade, há uma estrutura de dados que faz isso: MultiSet.

Infelizmente, não há MultiSetimplementação na biblioteca central ou biblioteca padrão do Ruby, mas existem algumas implementações flutuando pela web.

Este é um ótimo exemplo de como a escolha de uma estrutura de dados pode simplificar um algoritmo. Na verdade, neste exemplo específico, o algoritmo desaparece completamente . É literalmente apenas:

Multiset.new(*names)

E é isso. Exemplo, 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

Exemplo, 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
fonte
O conceito MultiSet origina-se da matemática ou de outra linguagem de programação?
Andrew Grimm
2
@Andrew Grimm: Tanto a palavra "multiset" (de Bruijn, 1970) quanto o conceito (Dedekind 1888) se originaram na matemática. Multiseté governado por regras matemáticas estritas e suporta as operações de conjunto típicas (união, interseção, complemento, ...) de uma forma que é principalmente consistente com os axiomas, leis e teoremas da teoria matemática de conjuntos "normal", embora algumas leis importantes o façam não se mantém quando você tenta generalizá-los para multisets. Mas isso está muito além da minha compreensão do assunto. Eu os uso como uma estrutura de dados de programação, não como um conceito matemático.
Jörg W Mittag
Para expandir um pouco esse ponto: "... de uma forma que é mais consistente com os axiomas ..." : conjuntos "normais" são geralmente formalmente definidos por um conjunto de axiomas (suposições) chamado "teoria dos conjuntos de Zermelo-Frankel " No entanto, um desses axiomas: o axioma da extensionalidade afirma que um conjunto é definido precisamente por seus membros - por exemplo {A, A, B} = {A, B}. Isso é claramente uma violação da própria definição de multi-conjuntos!
Tom Lord
... No entanto, sem entrar em muitos detalhes (já que este é um fórum de software, não matemática avançada!), Pode- se definir formalmente multi-conjuntos matematicamente por meio de axiomas para conjuntos Crisp, os axiomas de Peano e outros axiomas específicos de MultiSet.
Tom Lord
13

Enumberable#each_with_object evita que você retorne o hash final.

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

Retorna:

=> {"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1}
Anconia
fonte
Concordo, a each_with_objectvariante é mais legível para mim do queinject
Lev Lukomsky
9

Ruby 2.7+

Ruby 2.7 está apresentando Enumerable#tallyexatamente para esse propósito. Há um bom resumo aqui .

Neste caso de uso:

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

Os documentos sobre os recursos que estão sendo lançados estão aqui .

Espero que isso ajude alguém!

SRack
fonte
Notícias fantásticas!
tadman
6

Isso funciona.

arr = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]
result = {}
arr.uniq.each{|element| result[element] = arr.count(element)}
Shreyas
fonte
2
+1 Para uma abordagem diferente - embora tenha pior complexidade teórica - O(n^2)(o que importará para alguns valores de n) e faça um trabalho extra (tem que contar para "Judá" 3x, por exemplo) !. Eu também sugeriria em eachvez de map(o resultado do mapa está sendo descartado)
Obrigado por isso! Mudei o mapa para cada um. Além disso, uniqi a matriz antes de examiná-la. Talvez agora o problema da complexidade esteja resolvido?
Shreyas
6

O seguinte é um estilo de programação um pouco mais 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]]

Uma vantagem group_byé que você pode usá-lo para agrupar itens equivalentes, mas não exatamente 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
fonte
Eu ouvi programação funcional? +1 :-) Esta é definitivamente a melhor maneira, embora possa ser argumentado que não é eficiente em termos de memória. Observe também que Facets tem uma frequência 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
fonte
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
fonte
2

Muitas implementações excelentes aqui.

Mas como um iniciante, eu consideraria isso o mais fácil de ler 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}

As etapas que demos:

  • nós criamos o hash
  • nós giramos sobre a namesmatriz
  • contamos quantas vezes cada nome apareceu na namesmatriz
  • criamos uma chave usando o namee um valor usando ocount

Pode ser um pouco mais detalhado (e em termos de desempenho, você fará algum trabalho desnecessário com as teclas de substituição), mas na minha opinião mais fácil de ler e entender para o que você deseja alcançar

Sami Birnbaum
fonte
2
Não vejo como é mais fácil de ler do que a resposta aceita, e é claramente um design pior (fazendo muito trabalho desnecessário).
Tom Lord de
@Tom Lord - concordo com você sobre o desempenho (até mencionei isso em minha resposta) - mas, como um iniciante tentando entender o código real e as etapas necessárias, acho que ajuda ser mais prolixo e então pode-se refatorar para melhorar desempenho e tornar o código mais declarativo
Sami Birnbaum
1
Eu concordo um pouco com @SamiBirnbaum. Este é o único que quase não usa nenhum conhecimento especial de rubi como Hash.new(0). O mais próximo do pseudocódigo. Isso pode ser bom para a legibilidade, mas também fazer um trabalho desnecessário pode prejudicar a legibilidade para os leitores que perceberem, porque em casos mais complexos, eles passarão algum tempo pensando que estão enlouquecendo tentando descobrir por que isso é feito.
Adamantish
1

Este é mais um comentário do que uma resposta, mas um comentário não faria justiça. Se você fizer isso Array = foo, você travará pelo menos uma implementação do 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>

Isso porque Arrayé uma classe.

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

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

Tempo decorrido 0,028 milissegundos

curiosamente, a implementação de stupidgeek comparou:

Tempo decorrido 0,041 milissegundos

e a resposta vencedora:

Tempo decorrido 0,011 milissegundos

:)

Alex Moore-Niemi
fonte