Ruby配列内の同一の文字列要素をカウントする方法


92

私は以下を持っています Array = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]

同一の要素ごとにカウントを生成するにはどうすればよいですか?

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

またはハッシュを生成します。ここで:

ここで、hash = {"Jason" => 2、 "Judah" => 3、 "Allison" => 1、 "Teresa" => 1、 "Michelle" => 1}


2
Ruby 2.7以降、を使用できますEnumerable#tally。詳細はこちら
SRack

回答:


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}

あなたにあげる

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

3
+1選択した回答と同様ですが、「外部」変数を使用せずに注入を使用することをお勧めします。

18
each_with_object代わりに使用する場合は、ブロックでinject;total)を返す必要はありません。
mfilej 2013

13
後世のために、これは@mfilejが意味するものです:array.each_with_object(Hash.new(0)){|string, hash| hash[string] += 1}
Gon Zifroni 2015年

2
Ruby 2.7から、次のことが簡単にできますnames.tally
HallgeirWilhelmsen19年

103

Ruby v2.7 +(最新)

ruby v2.7.0(2019年12月にリリース)の時点で、コア言語には、この問題のために特別に設計されEnumerable#tally新しいメソッドが含まれています。

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

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

Ruby v2.4 +(現在サポートされていますが古い)

次のコードは、この質問が最初に尋ねられたとき(2011年2月)、次のコードを使用しているため、標準のルビーでは使用できませんでした。

  • Object#itself、Ruby v2.2.0(2014年12月リリース)に追加されました。
  • Hash#transform_values、Ruby v2.4.0(2016年12月リリース)に追加されました。

Rubyへのこれらの最新の追加により、次の実装が可能になります。

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 +(非推奨)

上記のHash#transform_values方法にアクセスせずに古いバージョンのrubyを使用している場合は、代わりにArray#to_h、Ruby v2.1.0(2013年12月にリリース)に追加されたを使用できます。

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

さらに古いルビーバージョン(<= 2.1)の場合、これを解決する方法はいくつかありますが、(私の意見では)明確な「最良の」方法はありません。この投稿に対する他の回答を参照してください。


投稿しようとしていた:P。/のcount代わりに使用することの間に識​​別可能な違いはありますか?sizelength
氷ツ

1
@SagarPandyaいいえ、違いはありません。異なりArray#sizeArray#lengthArray#count できるオプションの引数やブロックを取ります。ただし、どちらとも使用しない場合、その実装は同じです。具体的には、すべての3つの方法が呼び出すLONG2NUM(RARRAY_LEN(ary))ボンネットの下に:/長さ
トム・ロード

1
これは慣用的なRubyの良い例です。素晴らしい答え。
slhck

1
追加クレジット!回数で並べ替え.group_by(&:itself).transform_values(&:count).sort_by{|k, v| v}.reverse
アブラム

2
@Abramできますsort_by{ |k, v| -v}reverse必要ありません!;-)
ソニーサントス

26

Ruby 2.2.0を使用すると、このitselfメソッドを活用できます。

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
同意しますが、names.group_by(&:itself).map {| k、v |を少し好みます。[k、v.count]}。to_hなので、ハッシュオブジェクトを宣言する必要はありません
Andy Day

8
:@andrewkdayさらにこの一歩を取ると、ルビーV2.4は、メソッドを追加Hash#transform_values私たちはもっと自分のコードを簡素化することができます:names.group_by(&:itself).transform_values(&:count)
トム主

また、これは非常に微妙な点です(これは、将来の読者には関係がない可能性があります!)が、コードArray#to_hでは、Ruby v2.1.0(2013年12月にリリース)に追加された(つまり、元の質問からほぼ3年後)も使用されていることに注意してください。頼まれた)!
トム主

17

実際には、これを行うデータ構造がありますMultiSet

残念ながら、MultiSetRubyコアライブラリまたは標準ライブラリには実装がありませんが、Web上にいくつかの実装があります。

これは、データ構造の選択によってアルゴリズムを単純化する方法の優れた例です。実際、この特定の例では、アルゴリズムは完全になくなります。それは文字通りただです:

Multiset.new(*names)

以上です。例、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

例、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'>

MultiSetの概念は、数学または別のプログラミング言語に由来しますか?
Andrew Grimm

2
@Andrew Grimm:彼の言葉は「マルチセット」(de Bruijn、1970年代)であり、概念(Dedekind 1888)は数学に端を発しています。Multisetは厳密な数学的規則によって管理され、「通常の」数学的集合論の公理、法則、および定理とほぼ一致する方法で、典型的な集合演算(和集合、共通部分、補集合など)をサポートしますが、いくつかの重要な法則はそれらをマルチセットに一般化しようとするとき成り立たない。しかし、それは私の理解をはるかに超えています。私はそれらを数学的な概念としてではなく、プログラミングデータ構造として使用します。
イェルクWミッターク

その点について少し詳しく説明すると、「...公理とほぼ一致する方法で...」:「通常の」集合は通常、「ツェルメロフランケル集合論」と呼ばれる公理の集合(仮定)によって正式に定義されます。 "。ただし、これらの公理の1つ:拡張性の公理は、集合がそのメンバーによって正確に定義されることを示してい{A, A, B} = {A, B}ます。これは明らかにマルチセットの定義そのものに違反しています!
トム・ロード

...ただし、あまり詳しく説明しなくても(これはソフトウェアフォーラムであり、高度な数学ではありません!)、クリスプセットの公理、ペアノの公理、およびその他のマルチセット固有の公理を介して、マルチセットを数学的に正式に定義できます。
トム・ロード

13

Enumberable#each_with_object 最終的なハッシュを返す必要がなくなります。

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

戻り値:

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

同意します、each_with_objectバリアントは私にとってより読みやすいですinject
Lev Lukomsky 2018

9

Ruby 2.7+

Ruby 2.7は、まさにEnumerable#tallyこの目的のために導入されています。ここに良い要約があります

このユースケースでは:

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

リリースされている機能に関するドキュメントはこちらです。

これが誰かを助けることを願っています!


素晴らしいニュース!
tadman

6

これは機能します。

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

2
+1別のアプローチの場合-これは理論上の複雑さが悪化しますが- O(n^2)(一部の値では重要になりますn余分な作業を行います(たとえば、「ユダ」を3倍カウントする必要があります)。また、私は示唆しているeachの代わりに、map(マップ結果が破棄されている)

それをありがとう!マップをそれぞれに変更しました。また、配列を通過する前に配列を一意にしました。たぶん今、複雑さの問題は解決されていますか?
Shreyas 2011

6

以下は、もう少し関数型プログラミングスタイルです。

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

利点の1つgroup_byは、これを使用して、同等であるが完全に同一ではないアイテムをグループ化できることです。

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

関数型プログラミングを聞きましたか?+1 :-)これは間違いなく最良の方法ですが、メモリ効率が悪いと主張することもできます。ファセットにはEnumerable#frequencyがあることにも注意してください。
tokland 2011年

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}

クレジットFrankWambutt


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

ここには素晴らしい実装がたくさんあります。

しかし、初心者として、私はこれを読み、実装するのが最も簡単だと思います

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}

私たちが取ったステップ:

  • ハッシュを作成しました
  • names配列をループしました
  • それぞれの名前がnames配列に出現した回数を数えました
  • を使用してキーを作成し、を使用しnameて値を作成しましたcount

少し冗長かもしれませんが(パフォーマンスに関しては、キーをオーバーライドして不要な作業を行うことになります)、私の意見では、達成したいことを読み、理解するのは簡単です。


2
受け入れられた答えよりもそれがどのように読みやすいかはわかりません、そしてそれは明らかに悪いデザインです(多くの不必要な作業をします)。
トム卿

@Tom Lord-パフォーマンスについては同意します(私の回答でも言及しました)-しかし、実際のコードと必要な手順を理解しようとしている初心者として、より冗長にすると、リファクタリングして改善できることがわかりますパフォーマンスとコードの宣言型化
SamiBirnbaum19年

1
@SamiBirnbaumにいくらか同意します。これは、のような特別なルビーの知識をほとんど使用しない唯一のものですHash.new(0)。擬似コードに最も近い。これは読みやすさにとっては良いことですが、不必要な作業を行うと、それに気付いた読者にとって読みやすさが損なわれる可能性があります。より複雑なケースでは、なぜそれが行われたのかを理解しようと夢中になっていると考えるのに少し時間がかかるからです。
アダマン

1

これは答えというよりはコメントですが、コメントはそれを正当化するものではありません。そうした場合Array = foo、IRBの少なくとも1つの実装がクラッシュします。

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>

それArrayはクラスだからです。


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

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

経過時間0.028ミリ秒

興味深いことに、stupidgeekの実装は次のベンチマークを実行しました。

経過時間0.041ミリ秒

そして勝利の答え:

経過時間0.011ミリ秒

:)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.