Cara menghitung elemen string identik dalam array Ruby

91

Saya memiliki yang berikut ini Array = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]

Bagaimana cara menghasilkan hitungan untuk setiap elemen identik ?

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

atau menghasilkan hash Dimana:

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

pengguna398520
sumber
2
Mulai dari Ruby 2.7, Anda dapat menggunakan Enumerable#tally. Info selengkapnya di sini .
SRack

Jawaban:

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

Memberi anda

{"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1} 
Mauricio
sumber
3
+1 Suka jawaban yang dipilih, tapi saya lebih suka penggunaan inject dan tidak ada variabel "eksternal".
18
Jika Anda menggunakan each_with_objectsebagai pengganti injectAnda tidak harus mengembalikan ( ;total) di blok.
mfilej
12
Untuk anak cucu, inilah arti @mfilej:array.each_with_object(Hash.new(0)){|string, hash| hash[string] += 1}
Gon Zifroni
2
Dari Ruby 2.7, Anda hanya dapat melakukan: names.tally.
Hallgeir Wilhelmsen
99

Ruby v2.7 + (terbaru)

Mulai ruby ​​v2.7.0 (dirilis Desember 2019), bahasa inti sekarang menyertakan Enumerable#tally- metode baru , yang dirancang khusus untuk masalah ini:

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

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

Ruby v2.4 + (saat ini didukung, tetapi lebih lama)

Kode berikut tidak mungkin dalam ruby ​​standar ketika pertanyaan ini pertama kali diajukan (Februari 2011), karena menggunakan:

Penambahan modern pada Ruby memungkinkan implementasi berikut:

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 + (tidak digunakan lagi)

Jika menggunakan versi ruby ​​yang lebih lama, tanpa akses ke Hash#transform_valuesmetode yang disebutkan di atas , Anda dapat menggunakan Array#to_h, yang telah ditambahkan ke Ruby v2.1.0 (dirilis Desember 2013):

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

Untuk versi ruby ​​yang lebih lama ( <= 2.1), ada beberapa cara untuk mengatasinya, tetapi (menurut saya) tidak ada cara "terbaik" yang jelas. Lihat jawaban lain untuk posting ini.

Tom Lord
sumber
Saya hendak memposting: P. Apakah ada perbedaan yang terlihat antara menggunakan countselain size/ length?
es ツ
1
@SagarPandya Tidak, tidak ada perbedaan. Tidak seperti Array#sizedan Array#length, Array#count dapat mengambil argumen atau blok opsional; tetapi jika digunakan dengan keduanya maka implementasinya identik. Lebih khusus lagi, ketiga metode memanggil di LONG2NUM(RARRAY_LEN(ary))bawah tenda: hitung / panjang
Tom Lord
1
Ini adalah contoh bagus dari Ruby idiomatik. Jawaban yang bagus.
slhck
1
Kredit tambahan! Sortir menurut hitungan.group_by(&:itself).transform_values(&:count).sort_by{|k, v| v}.reverse
Abram
2
@Abram Anda bisa sort_by{ |k, v| -v}, tidak reverseperlu! ;-)
Sony Santos
26

Sekarang menggunakan Ruby 2.2.0 Anda dapat memanfaatkan itselfmetode ini .

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
sumber
3
Setuju, tetapi saya sedikit lebih suka names.group_by (&: sendiri) .map {| k, v | [k, v.count]}. to_h sehingga Anda tidak perlu mendeklarasikan objek hash
Andy Day
8
@andrewkday Mengambil satu langkah lebih jauh, ruby ​​v2.4 menambahkan metode: Hash#transform_valuesyang memungkinkan kami untuk lebih menyederhanakan kode Anda:names.group_by(&:itself).transform_values(&:count)
Tom Lord
Juga, ini adalah poin yang sangat halus (yang sepertinya tidak lagi relevan untuk pembaca selanjutnya!), Tetapi perhatikan bahwa kode Anda juga menggunakan Array#to_h- yang telah ditambahkan ke Ruby v2.1.0 (dirilis Desember 2013 - yaitu hampir 3 tahun setelah pertanyaan asli diminta!)
Tom Lord
17

Sebenarnya ada struktur data yang melakukan ini: MultiSet .

Sayangnya tidak ada MultiSet implementasi di pustaka inti Ruby atau pustaka standar, tetapi ada beberapa penerapan yang beredar di web.

Ini adalah contoh yang bagus tentang bagaimana pilihan struktur data dapat menyederhanakan algoritma. Faktanya, dalam contoh khusus ini, algoritme bahkan sepenuhnya hilang. Ini benar-benar hanya:

Multiset.new(*names)

Dan itu dia. Contoh, menggunakan 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

Contoh, menggunakan 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
sumber
Apakah konsep MultiSet berasal dari matematika, atau bahasa pemrograman lain?
Andrew Grimm
2
@ Andrew Grimm: Kata "multiset" (de Bruijn, 1970-an) dan konsepnya (Dedekind 1888) berasal dari matematika. Multisetdiatur oleh aturan matematika yang ketat dan mendukung operasi himpunan tipikal (gabungan, perpotongan, komplemen, ...) dengan cara yang sebagian besar konsisten dengan aksioma, hukum, dan teorema dari teori himpunan matematika "normal", meskipun beberapa hukum penting melakukan tidak tahan saat Anda mencoba menggeneralisasikannya menjadi multiset. Tapi itu jauh di luar pemahaman saya tentang masalah ini. Saya menggunakannya sebagai struktur data pemrograman, bukan sebagai konsep matematika.
Jörg W Mittag
Untuk sedikit memperluas poin itu: "... dengan cara yang sebagian besar konsisten dengan aksioma ..." : Himpunan "Normal" biasanya secara formal ditentukan oleh himpunan aksioma (asumsi) yang disebut "teori himpunan Zermelo-Frankel ". Namun, salah satu aksioma ini: aksioma ekstensionalitas menyatakan bahwa suatu himpunan didefinisikan secara tepat oleh anggotanya - misalnya {A, A, B} = {A, B}. Ini jelas merupakan pelanggaran terhadap definisi multi-set!
Tom Lord
... Namun, tanpa membahas terlalu banyak detail (karena ini adalah forum perangkat lunak, bukan matematika tingkat lanjut!), Seseorang dapat secara formal mendefinisikan multi-set secara matematis melalui aksioma untuk set Crisp, aksioma Peano, dan aksioma spesifik MultiSet lainnya.
Tom Lord
13

Enumberable#each_with_object menyelamatkan Anda dari mengembalikan hash terakhir.

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

Pengembalian:

=> {"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1}
Anconia
sumber
Setuju, each_with_objectvarian lebih mudah dibaca oleh saya daripadainject
Lev Lukomsky
9

Ruby 2.7+

Ruby 2.7 diperkenalkan Enumerable#tallyuntuk tujuan yang tepat ini. Ada ringkasan bagus di sini .

Dalam kasus penggunaan ini:

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

Dokumen tentang fitur yang dirilis ada di sini .

Semoga ini bisa membantu seseorang!

SRack
sumber
Berita fantastis!
tadman
6

Ini bekerja.

arr = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]
result = {}
arr.uniq.each{|element| result[element] = arr.count(element)}
Shreyas
sumber
2
+1 Untuk pendekatan yang berbeda - meskipun ini memiliki kompleksitas teoritis yang lebih buruk - O(n^2)(yang akan menjadi masalah untuk beberapa nilai n) dan melakukan pekerjaan tambahan (harus dihitung untuk "Judah" 3x, misalnya) !. Saya juga menyarankan eachalih-alih map(hasil peta akan dibuang)
Terima kasih untuk itu! Saya telah mengubah peta menjadi masing-masing. Juga, saya telah menyatukan array sebelum melewatinya. Mungkin sekarang masalah kompleksitas sudah terpecahkan?
Shreyas
6

Berikut ini adalah gaya pemrograman yang sedikit lebih fungsional:

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

Salah satu keuntungannya group_byadalah Anda dapat menggunakannya untuk mengelompokkan item yang setara tetapi tidak persis sama:

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
sumber
Apakah saya mendengar pemrograman fungsional? +1 :-) Ini jelas merupakan cara terbaik, meskipun dapat dikatakan bahwa ini tidak hemat memori. Perhatikan juga bahwa Facets memiliki frekuensi # 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}

Kredit Frank Wambutt

narzero
sumber
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
sumber
2

Banyak implementasi bagus di sini.

Tetapi sebagai pemula, saya akan menganggap ini paling mudah untuk dibaca dan diterapkan

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}

Langkah-langkah yang kami ambil:

  • kami membuat hash
  • kami memutar ulang names array
  • kami menghitung berapa kali setiap nama muncul dalam nameslarik
  • kami membuat kunci menggunakan name dan nilai menggunakancount

Ini mungkin sedikit lebih bertele-tele (dan dari segi kinerja Anda akan melakukan beberapa pekerjaan yang tidak perlu dengan kunci utama), tetapi menurut saya lebih mudah untuk membaca dan memahami untuk apa yang ingin Anda capai

Sami Birnbaum
sumber
2
Saya tidak melihat bagaimana itu lebih mudah dibaca daripada jawaban yang diterima, dan itu jelas desain yang lebih buruk (melakukan banyak pekerjaan yang tidak perlu).
Tom Lord
@Tom Lord - Saya setuju dengan Anda tentang kinerja (saya bahkan menyebutkannya dalam jawaban saya) - tetapi sebagai pemula yang mencoba memahami kode aktual dan langkah-langkah yang diperlukan, saya merasa terbantu untuk menjadi lebih bertele-tele dan kemudian seseorang dapat refactor untuk meningkatkan kinerja dan membuat kode lebih deklaratif
Sami Birnbaum
1
Saya agak setuju dengan @SamiBirnbaum. Ini adalah satu-satunya yang menggunakan hampir tidak ada pengetahuan khusus tentang ruby Hash.new(0). Yang paling dekat dengan pseudocode. Itu bisa menjadi hal yang baik untuk keterbacaan tetapi juga melakukan pekerjaan yang tidak perlu dapat merusak keterbacaan bagi pembaca yang menyadarinya karena dalam kasus yang lebih kompleks mereka akan menghabiskan sedikit waktu untuk berpikir mereka gila mencoba mencari tahu mengapa itu dilakukan.
Adamantish
1

Ini lebih merupakan komentar daripada jawaban, tetapi komentar tidak akan adil. Jika Anda melakukannya Array = foo, Anda menghentikan setidaknya satu implementasi 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>

Itu karena Arraykelas.

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

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

Waktu berlalu 0,028 milidetik

Menariknya, implementasi bodohgeek mengacu pada:

Waktu berlalu 0,041 milidetik

dan jawaban kemenangan:

Waktu berlalu 0,011 milidetik

:)

Alex Moore-Niemi
sumber