Як підрахувати однакові елементи рядків у масиві Ruby


91

У мене таке 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

Відповіді:


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}

дає вам

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

3
+1 Подобається вибраній відповіді, але я віддаю перевагу використанню inject та жодної "зовнішньої" змінної.

18
Якщо ви використовуєте each_with_objectзамість цього, injectвам не потрібно повертати ( ;total) до блоку.
mfilej

12
Для нащадків це означає @mfilej:array.each_with_object(Hash.new(0)){|string, hash| hash[string] += 1}
Гон Зіфроні,

2
Від Рубі 2.7, ви можете просто зробити: names.tally.
Hallgeir Wilhelmsen

99

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 + (наразі підтримується, але старіший)

Наступний код був неможливим у стандартному рубіні, коли це запитання було вперше задано (лютий 2011 р.), Оскільки він використовує:

  • 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))під капотом: кол / довжина
Том Лорд

1
Це такий гарний приклад ідіоматичної Рубі. Чудова відповідь.
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 (&: сам) .map {| k, v | [k, v.count]}. to_h, щоб вам не потрібно було ніколи оголошувати хеш-об'єкт
Енді Дей

8
@andrewkday Зробивши цей крок далі, ruby ​​v2.4 додав метод: Hash#transform_valuesякий дозволяє нам ще більше спростити ваш код:names.group_by(&:itself).transform_values(&:count)
Том Лорд

Крім того, це дуже тонкий момент (який, швидше за все, вже не стосується майбутніх читачів!), Але зауважте, що ваш код також використовує Array#to_h- що було додано до Ruby v2.1.0 (випущено в грудні 2013 року - тобто майже через 3 роки після вихідного запитання запитали!)
Том Лорд,

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
@Andrew Grimm: І слово "мультимножина" (де Бруйн, 1970-ті), і концепція (Dedekind 1888) походять з математики. Multisetрегулюється суворими математичними правилами і підтримує типові операції з множинами (об'єднання, перетин, доповнення, ...) способом, який здебільшого відповідає аксіомам, законам і теоремам "нормальної" математичної теорії множин, хоча деякі важливі закони не утримуйте, коли намагаєтесь узагальнити їх на мультимножини. Але це далеко поза моїм розумінням справи. Я використовую їх як структуру даних програмування, а не як математичну концепцію.
Jörg W Mittag

Щоб трохи розширити цю тему: "... таким чином, що здебільшого відповідає аксіомам ..." : "Нормальні" множини зазвичай формально визначаються набором аксіом (припущень), що називається "Теорія множин Цермело-Франкеля ". Однак одна з цих аксіом: аксіома розширення стверджує, що сукупність визначається саме її членами - наприклад {A, A, B} = {A, B}. Це явно порушення самого визначення мультимножин!
Том Лорд

... Однак, не вдаючись у надто подробиці (оскільки це програмний форум, а не просунута математика!), Можна формально визначити мультимножини математично за допомогою аксіом для наборів Крисп, аксіом Пеано та інших специфічних аксіом MultiSet.
Том Лорд,

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
Лев Лукомський

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) і виконує додаткову роботу (наприклад, це має враховувати "Юду" 3 рази) !. Я б також запропонував eachзамість map(результат карти відкидається)

Дякую за це! Я змінив карту на кожну. Крім того, я уніфікував масив, перш ніж пройти його. Може, зараз вирішено питання складності?
Шреяс

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 :-) Це, безумовно, найкращий спосіб, хоча можна стверджувати, що це не ефективно для пам'яті. Також зверніть увагу, що грані мають незліченну частоту #.
Tokland


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 - я дійсно погоджуюся з вами щодо продуктивності (я навіть це згадав у своїй відповіді) - але як новачок, який намагається зрозуміти фактичний код та необхідні кроки, я вважаю, що це допомагає бути більш багатослівним, і тоді можна рефакторингу покращити виконання та зробити код більш декларативним
Самі Бірнбаум

1
Я дещо згоден із @SamiBirnbaum. Це єдиний, який майже не використовує спеціальних знань про рубін Hash.new(0). Найближчий до псевдокоду. Це може бути добре для читабельності, але також зайва робота може зашкодити читабельності читачам, які це помічають, оскільки в більш складних випадках вони витратять трохи часу, думаючи, що вони збожеволіли, намагаючись зрозуміти, чому це зроблено.
Адамантіш

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 мілісекунд

:)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.