Как подсчитать одинаковые строковые элементы в массиве 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}

user398520
источник
3
Начиная с 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}
Гон Зифрони
2
От Руби 2.7, вы можете просто сделать: names.tally.
Hallgeir Wilhelmsen
104

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 + (устарело)

Если вы используете старую версию Ruby без доступа к вышеупомянутому 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}

Для даже более старых версий Ruby ( <= 2.1) есть несколько способов решить эту проблему, но (на мой взгляд) нет однозначного «лучшего» способа. Смотрите другие ответы на этот пост.

Том Лорд
источник
Я собирался опубликовать: P. Есть ли заметная разница между использованием countвместо size/ length?
ice ツ
1
@SagarPandya Нет, разницы нет. В отличие от Array#sizeи Array#length, Array#count может принимать необязательный аргумент или блок; но если не используется ни один из них, то его реализация идентична. В частности, все три метода вызывают LONG2NUM(RARRAY_LEN(ary))под капотом: count / length
Tom Lord
1
Это прекрасный пример идиоматического Ruby. Отличный ответ.
slhck 01
1
Дополнительный кредит! Сортировка по количеству.group_by(&:itself).transform_values(&:count).sort_by{|k, v| v}.reverse
Абрам
2
@ Абрам, ты можешь 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 (& :self) .map {| k, v | [k, v.count]}. to_h, чтобы вам не приходилось когда-либо объявлять хэш-объект
Энди Дэй
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 - математика или другой язык программирования?
Эндрю Гримм
2
@ Эндрю Гримм: И слово «мультимножество» (де Брёйн, 1970-е годы), и концепция (Дедекинд, 1888) возникли в математике. Multisetуправляется строгими математическими правилами и поддерживает типичные операции над множеством (объединение, пересечение, дополнение, ...) способом, который в основном согласуется с аксиомами, законами и теоремами "нормальной" математической теории множеств, хотя некоторые важные законы это делают не выполняются, когда вы пытаетесь обобщить их на мультимножества. Но это выходит за рамки моего понимания этого вопроса. Я использую их как структуру данных программирования, а не как математическую концепцию.
Jörg W Mittag
Чтобы немного расширить этот вопрос: «... способом, который в основном согласуется с аксиомами ...» : «Нормальные» множества обычно формально определяются набором аксиом (предположений), называемым «теорией множеств Цермело-Франкеля. ". Однако, один из этих аксиом: аксиома объемности состояний , что набор определяется именно его членами - например {A, A, B} = {A, B}. Это явно нарушение самого определения множественных наборов!
Tom Lord
... Однако, не вдаваясь в подробности (поскольку это форум по программному обеспечению, а не продвинутая математика!), Можно формально определить множественные множества математически с помощью аксиом для Crisp-множеств, аксиом Пеано и других аксиом, специфичных для 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
Лев Лукомский
9

Рубин 2.7+

Ruby 2.7 вводится именно Enumerable#tallyдля этой цели. Там хорошее резюме здесь .

В этом случае использования:

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

Документация о выпускаемых функциях находится здесь .

Надеюсь, это кому-то поможет!

SRack
источник
Отличные новости!
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(результат карты отбрасывается)
Спасибо за это! Я изменил карту для каждого из них, а также перед просмотром массива я 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 имеет частоту Enumerable #.
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). Самый близкий к псевдокоду. Это может быть полезно для удобочитаемости, но также выполнение ненужной работы может повредить читаемости для читателей, которые это заметят, потому что в более сложных случаях они потратят немного времени, думая, что сходят с ума, пытаясь понять, почему это делается.
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 миллисекунды

:)

Алекс Мур-Ниеми
источник