วิธีนับองค์ประกอบสตริงที่เหมือนกันในอาร์เรย์ 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

13
สำหรับลูกหลานนี่คือสิ่งที่ @mfilej หมายถึง:array.each_with_object(Hash.new(0)){|string, hash| hash[string] += 1}
Gon Zifroni

2
จากทับทิม 2.7 names.tallyคุณก็สามารถทำได้:
Hallgeir Wilhelmsen

103

Ruby v2.7 + (ล่าสุด)

ตั้งแต่ Ruby v2.7.0 (เผยแพร่เมื่อเดือนธันวาคม 2019) ภาษาหลักในขณะนี้มี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 + (รองรับในปัจจุบัน แต่เก่ากว่า)

รหัสต่อไปนี้ใช้ไม่ได้ในทับทิมมาตรฐานเมื่อมีการถามคำถามนี้เป็นครั้งแรก (กุมภาพันธ์ 2554) เนื่องจากใช้:

  • Object#itselfซึ่งเพิ่มเข้ามาใน Ruby v2.2.0 (เผยแพร่เมื่อธันวาคม 2014)
  • Hash#transform_valuesซึ่งเพิ่มเข้ามาใน Ruby v2.4.0 (เผยแพร่เมื่อธันวาคม 2016)

ส่วนเพิ่มเติมที่ทันสมัยเหล่านี้ใน 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วิธีการที่กล่าวมาข้างต้นคุณสามารถใช้แทนArray#to_hซึ่งเพิ่มเข้ามาใน Ruby v2.1.0 (เผยแพร่เมื่อเดือนธันวาคม 2013):

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แทนsize/ length?
น้ำแข็งツ

1
@SagarPandya ไม่มีไม่มีความแตกต่าง ซึ่งแตกต่างจากArray#sizeและArray#length, Array#count สามารถใช้อาร์กิวเมนต์ตัวเลือกหรือบล็อก; แต่ถ้าใช้กับทั้งคู่การใช้งานจะไม่เหมือนกัน โดยเฉพาะอย่างยิ่งทั้งสามวิธีเรียกLONG2NUM(RARRAY_LEN(ary))ภายใต้ประทุน: จำนวน / ความยาว
Tom Lord

1
นี่เป็นตัวอย่างที่ดีของ Ruby ที่เป็นสำนวน คำตอบที่ดี
slhck

1
สินเชื่อพิเศษ! เรียงตามจำนวน.group_by(&:itself).transform_values(&:count).sort_by{|k, v| v}.reverse
อับราม

2
@Abram คุณทำได้sort_by{ |k, v| -v}ไม่reverseจำเป็น! ;-)
Sony Santos

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 (&: ตัวมันเอง) เล็กน้อยแผนที่ {| k, v | [k, v.count]}. to_h เพื่อที่คุณจะได้ไม่ต้องประกาศแฮชวัตถุ
Andy Day

8
@andrewkday ก้าวไปอีกขั้นหนึ่ง Ruby v2.4 ได้เพิ่มวิธีการ: Hash#transform_valuesซึ่งช่วยให้เราลดความซับซ้อนของรหัสของคุณได้มากยิ่งขึ้น:names.group_by(&:itself).transform_values(&:count)
Tom Lord

นอกจากนี้นี่เป็นประเด็นที่ละเอียดอ่อนมาก (ซึ่งอาจไม่เกี่ยวข้องกับผู้อ่านในอนาคตอีกต่อไป!) แต่โปรดทราบว่ารหัสของคุณยังใช้Array#to_h- ซึ่งเพิ่มใน Ruby v2.1.0 (เผยแพร่เมื่อเดือนธันวาคม 2013 - เกือบ 3 ปีหลังจากคำถามเดิม ถูกถาม!)
Tom Lord

17

MultiSetมีจริงเป็นโครงสร้างข้อมูลที่ไม่นี้:

น่าเสียดายที่ไม่มีMultiSetการใช้งานในไลบรารีหลักของ Ruby หรือไลบรารีมาตรฐาน แต่มีการใช้งานสองสามอย่างที่ลอยอยู่บนเว็บ

นี่เป็นตัวอย่างที่ดีว่าการเลือกโครงสร้างข้อมูลสามารถทำให้อัลกอริทึมง่ายขึ้นได้อย่างไร ในความเป็นจริงในตัวอย่างนี้อัลกอริทึมยังหายไปโดยสิ้นเชิง แท้จริงมันเป็นเพียง:

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
@ แอนดรูว์กริมม์: ทั้งคำว่า "หลายชุด" (เดอบรอยน์, 1970) และแนวคิด (Dedekind 1888) มีต้นกำเนิดในวิชาคณิตศาสตร์ Multisetถูกควบคุมโดยกฎทางคณิตศาสตร์ที่เข้มงวดและสนับสนุนการดำเนินการเซ็ตทั่วไป (ยูเนี่ยน, จุดตัด, ส่วนเติมเต็ม, ... ) ในลักษณะที่ส่วนใหญ่สอดคล้องกับสัจพจน์กฎหมายและทฤษฎีของทฤษฎีเซตทางคณิตศาสตร์ "ปกติ" แม้ว่ากฎหมายที่สำคัญบางอย่างจะทำอย่าค้างไว้เมื่อคุณพยายามที่จะสรุปให้เป็นชุดหลายชุด แต่นั่นเป็นวิธีที่เกินความเข้าใจของฉันในเรื่องนี้ ฉันใช้มันเป็นโครงสร้างข้อมูลการเขียนโปรแกรมไม่ใช่แนวคิดทางคณิตศาสตร์
Jörg W Mittag

เพื่อขยายความเล็กน้อยในประเด็นนั้น: "... in a way that most สอดคล้องกับสัจพจน์ ... " : ชุด "ปกติ" มักกำหนดอย่างเป็นทางการโดยชุดของสัจพจน์ (สมมติฐาน) ที่เรียกว่า "ทฤษฎีเซตเซอร์เมโล - แฟรงเคิล ". แต่หนึ่งในหลักการเหล่านี้คือความจริงของ Extensionalityระบุว่าเป็นชุดที่มีการกำหนดไว้อย่างแม่นยำโดยสมาชิก - {A, A, B} = {A, B}เช่น นี่เป็นการละเมิดคำจำกัดความของหลายชุดอย่างชัดเจน!
Tom Lord

... อย่างไรก็ตามโดยไม่ต้องลงรายละเอียดมากเกินไป (เนื่องจากนี่คือฟอรัมซอฟต์แวร์ไม่ใช่คณิตศาสตร์ขั้นสูง!) เราสามารถกำหนดหลายชุดทางคณิตศาสตร์อย่างเป็นทางการผ่านสัจพจน์สำหรับ Crisp, สัจพจน์ Peano และสัจพจน์เฉพาะ MultiSet อื่น ๆ
Tom Lord

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

9

รูบี้ 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) และทำงานพิเศษ (ต้องนับเป็น "ยูดาห์" 3x เป็นต้น)! ฉันขอแนะนำeachแทนmap(ผลแผนที่จะถูกทิ้ง)

ขอบคุณสำหรับสิ่งนั้น! ฉันเปลี่ยนแผนที่ไปแต่ละแผนที่แล้วนอกจากนี้ฉันยัง uniq'ed อาร์เรย์ก่อนที่จะผ่านมัน บางทีตอนนี้ปัญหาความซับซ้อนได้รับการแก้ไขแล้ว?
Shreyas

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

ข้อดีอย่างหนึ่ง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 :-) นี่เป็นวิธีที่ดีที่สุดแน่นอนแม้ว่าจะเป็นที่ถกเถียงกันอยู่ว่าหน่วยความจำไม่เต็มประสิทธิภาพ โปรดสังเกตด้วยว่า Facets มีความถี่ # ที่นับได้
tokland



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

@ Tom Lord - ฉันเห็นด้วยกับคุณในเรื่องประสิทธิภาพ (ฉันพูดถึงสิ่งนั้นในคำตอบของฉัน) - แต่ในฐานะผู้เริ่มต้นที่พยายามทำความเข้าใจรหัสจริงและขั้นตอนที่จำเป็นฉันพบว่ามันช่วยให้ละเอียดมากขึ้นแล้วก็สามารถ refactor เพื่อปรับปรุงได้ ประสิทธิภาพและทำให้โค้ดมีการเปิดเผยมากขึ้น
Sami Birnbaum

1
ฉันเห็นด้วยกับ @SamiBirnbaum Hash.new(0)นี้เป็นเพียงคนเดียวที่ใช้แทบไม่มีความรู้ทับทิมพิเศษเช่น ใกล้เคียงที่สุดกับรหัสเทียม นั่นอาจเป็นสิ่งที่ดีสำหรับความสามารถในการอ่าน แต่การทำงานที่ไม่จำเป็นอาจส่งผลเสียต่อความสามารถในการอ่านสำหรับผู้อ่านที่สังเกตเห็นเพราะในกรณีที่ซับซ้อนมากขึ้นพวกเขาจะใช้เวลาเล็กน้อยในการคิดว่าพวกเขากำลังจะบ้าเพื่อพยายามหาสาเหตุว่าทำไมถึงทำ
Adamantish

1

นี่เป็นความคิดเห็นมากกว่าคำตอบ แต่ความคิดเห็นจะไม่ทำให้เกิดความยุติธรรม หากคุณทำเช่นArray = fooนั้นคุณจะขัดข้องในการใช้งาน 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>

นั่นเป็นเพราะ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.