So zählen Sie identische Zeichenfolgenelemente in einem Ruby-Array

90

Ich habe folgendes Array = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]

Wie erstelle ich eine Zählung für jedes identische Element ?

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

oder einen Hash erstellen Wo:

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

user398520
quelle
1
Ab Ruby 2.7 können Sie verwenden Enumerable#tally. Mehr Infos hier .
SRack

Antworten:

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

gibt Ihnen

{"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1} 
Mauricio
quelle
3
+1 Wie die ausgewählte Antwort, aber ich bevorzuge die Verwendung von Inject und keine "externe" Variable.
18
Wenn Sie each_with_objectstattdessen verwenden, müssen injectSie ;totalam Block nicht ( ) zurückgeben.
Mfilej
12
Für die Nachwelt bedeutet @mfilej Folgendes:array.each_with_object(Hash.new(0)){|string, hash| hash[string] += 1}
Gon Zifroni,
2
Ab Ruby 2.7 können Sie einfach : names.tally.
Hallgeir Wilhelmsen
98

Ruby v2.7 + (aktuell)

Ab Ruby v2.7.0 (veröffentlicht im Dezember 2019) enthält die Kernsprache jetzt Enumerable#tally- eine neue Methode , die speziell für dieses Problem entwickelt wurde:

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

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

Ruby v2.4 + (derzeit unterstützt, aber älter)

Der folgende Code war in Standard-Ruby nicht möglich, als diese Frage zum ersten Mal gestellt wurde (Februar 2011), da sie verwendet wird:

  • Object#itself, das zu Ruby v2.2.0 hinzugefügt wurde (veröffentlicht im Dezember 2014).
  • Hash#transform_values, das zu Ruby v2.4.0 hinzugefügt wurde (veröffentlicht im Dezember 2016).

Diese modernen Ergänzungen zu Ruby ermöglichen die folgende Implementierung:

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 + (veraltet)

Wenn Sie eine ältere Ruby-Version ohne Zugriff auf die oben genannte Hash#transform_valuesMethode verwenden, können Sie stattdessen Array#to_hFolgendes verwenden , die Ruby v2.1.0 (veröffentlicht im Dezember 2013) hinzugefügt wurde:

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

Für noch ältere Ruby-Versionen ( <= 2.1) gibt es mehrere Möglichkeiten, dies zu lösen, aber (meiner Meinung nach) gibt es keinen eindeutigen "besten" Weg. Siehe die anderen Antworten auf diesen Beitrag.

Tom Lord
quelle
Ich wollte gerade posten: P. Gibt es einen erkennbaren Unterschied zwischen der Verwendung countanstelle von size/ length?
Eis ツ
1
@SagarPandya Nein, es gibt keinen Unterschied. Im Gegensatz zu Array#sizeund Array#length, Array#count kann ein optionales Argument oder Block nehmen; Wenn es jedoch mit keinem verwendet wird, ist seine Implementierung identisch. Genauer gesagt, alle drei Methoden rufen LONG2NUM(RARRAY_LEN(ary))unter der Haube: Anzahl / Länge
Tom Lord
1
Dies ist so ein schönes Beispiel für idiomatischen Ruby. Gute Antwort.
Slhck
1
Extra Gutschrift! Sortieren nach Anzahl.group_by(&:itself).transform_values(&:count).sort_by{|k, v| v}.reverse
Abram
2
@Abram kannst du sort_by{ |k, v| -v}, nicht reversenötig! ;-)
Sony Santos
26

Mit Ruby 2.2.0 können Sie jetzt die itselfMethode nutzen .

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
quelle
3
Stimmen Sie zu, aber ich bevorzuge leicht names.group_by (&: selbst) .map {| k, v | [k, v.count]}. to_h, damit Sie nie ein Hash-Objekt deklarieren müssen
Andy Day
8
@andrewkday Ruby v2.4 ging noch einen Schritt weiter und fügte die folgende Methode hinzu: Damit Hash#transform_valueskönnen wir Ihren Code noch weiter vereinfachen:names.group_by(&:itself).transform_values(&:count)
Tom Lord
Dies ist auch ein sehr subtiler Punkt (der für zukünftige Leser wahrscheinlich nicht mehr relevant ist!). Beachten Sie jedoch, dass Ihr Code auch verwendet Array#to_h- was Ruby v2.1.0 hinzugefügt wurde (veröffentlicht im Dezember 2013 - dh fast 3 Jahre nach der ursprünglichen Frage) wurde gefragt!)
Tom Lord
17

Es gibt tatsächlich eine Datenstruktur, die dies tut : MultiSet.

Leider gibt es keine MultiSetImplementierung in der Ruby-Kernbibliothek oder der Standardbibliothek, aber es gibt einige Implementierungen im Web.

Dies ist ein großartiges Beispiel dafür, wie die Auswahl einer Datenstruktur einen Algorithmus vereinfachen kann. In diesem speziellen Beispiel verschwindet der Algorithmus sogar vollständig . Es ist buchstäblich nur:

Multiset.new(*names)

Und das ist es. Beispiel mit 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

Beispiel mit 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
quelle
Entsteht das MultiSet-Konzept aus der Mathematik oder einer anderen Programmiersprache?
Andrew Grimm
2
@ Andrew Grimm: Sowohl das Wort "Multiset" (de Bruijn, 1970er Jahre) als auch das Konzept (Dedekind 1888) stammen aus der Mathematik. Multisetunterliegt strengen mathematischen Regeln und unterstützt die typischen Mengenoperationen (Vereinigung, Schnittmenge, Komplement, ...) auf eine Weise, die größtenteils mit den Axiomen, Gesetzen und Theoremen der "normalen" mathematischen Mengenlehre übereinstimmt, obwohl einige wichtige Gesetze dies tun Nicht gedrückt halten, wenn Sie versuchen, sie auf Multisets zu verallgemeinern. Aber das geht weit über mein Verständnis der Sache hinaus. Ich benutze sie als Programmierdatenstruktur, nicht als mathematisches Konzept.
Jörg W Mittag
Um diesen Punkt ein wenig zu erweitern : "... auf eine Weise, die größtenteils mit den Axiomen übereinstimmt ..." : "Normale" Mengen werden normalerweise formal durch eine Reihe von Axiomen (Annahmen) definiert, die als "Zermelo-Frankel-Mengen-Theorie" bezeichnet werden ". Eines dieser Axiome: Das Axiom der Extensionalität besagt, dass eine Menge von ihren Mitgliedern genau definiert wird - z {A, A, B} = {A, B}. Dies ist eindeutig ein Verstoß gegen die Definition von Multi-Sets!
Tom Lord
... Ohne jedoch zu sehr ins Detail zu gehen (da dies ein Software-Forum ist, keine fortgeschrittene Mathematik!), Kann man Multi-Sets formell mathematisch über Axiome für Crisp-Sets, die Peano-Axiome und andere MultiSet-spezifische Axiome definieren.
Tom Lord
13

Enumberable#each_with_object erspart Ihnen die Rückgabe des endgültigen Hashs.

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

Kehrt zurück:

=> {"Jason"=>2, "Teresa"=>1, "Judah"=>3, "Michelle"=>1, "Allison"=>1}
Anconia
quelle
Stimmen Sie zu, each_with_objectVariante ist für mich besser lesbar alsinject
Lev Lukomsky
8

Ruby 2.7+

Ruby 2.7 wird Enumerable#tallygenau für diesen Zweck eingeführt. Es gibt eine gute Zusammenfassung hier .

In diesem Anwendungsfall:

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

Dokumente zu den veröffentlichten Funktionen finden Sie hier .

Hoffe das hilft jemandem!

SRack
quelle
Fantastische Neuigkeiten!
Tadman
6

Das funktioniert.

arr = ["Jason", "Jason", "Teresa", "Judah", "Michelle", "Judah", "Judah", "Allison"]
result = {}
arr.uniq.each{|element| result[element] = arr.count(element)}
Shreyas
quelle
2
+1 Für einen anderen Ansatz - obwohl dies eine schlechtere theoretische Komplexität hat - O(n^2)(was für einige Werte von Bedeutung sein wird n) und zusätzliche Arbeit leistet (es muss zum Beispiel für "Judah" 3x zählen)!. Ich würde auch vorschlagen, eachanstatt map(das Kartenergebnis wird verworfen)
Dank dafür! Ich habe die Karte in jede geändert. Außerdem habe ich das Array vereinheitlicht, bevor ich es durchgegangen bin. Vielleicht ist jetzt das Komplexitätsproblem gelöst?
Shreyas
6

Das Folgende ist ein etwas funktionalerer Programmierstil:

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

Ein Vorteil von group_byist, dass Sie damit äquivalente, aber nicht genau identische Elemente gruppieren können:

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
quelle
Habe ich funktionale Programmierung gehört? +1 :-) Dies ist definitiv der beste Weg, obwohl argumentiert werden kann, dass dies nicht speichereffizient ist. Beachten Sie auch, dass Facetten eine Enumerable # -Frequenz haben.
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
quelle
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
quelle
2

Viele tolle Implementierungen hier.

Aber als Anfänger würde ich dies als am einfachsten zu lesen und zu implementieren betrachten

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}

Die Schritte, die wir unternommen haben:

  • Wir haben den Hash erstellt
  • Wir haben das namesArray durchlaufen
  • Wir haben gezählt, wie oft jeder Name im namesArray vorkommt
  • Wir haben einen Schlüssel mit dem nameund einen Wert mit dem erstelltcount

Es mag etwas ausführlicher sein (und in Bezug auf die Leistung werden Sie unnötige Arbeit mit überschreibenden Schlüsseln leisten), aber meiner Meinung nach ist es einfacher zu lesen und zu verstehen, was Sie erreichen möchten

Sami Birnbaum
quelle
2
Ich sehe nicht, dass das einfacher zu lesen ist als die akzeptierte Antwort, und es ist eindeutig ein schlechteres Design (viele unnötige Arbeiten).
Tom Lord
@ Tom Lord - Ich stimme Ihnen in Bezug auf die Leistung zu (das habe ich sogar in meiner Antwort erwähnt) - aber als Anfänger, der versucht, den tatsächlichen Code und die erforderlichen Schritte zu verstehen, finde ich es hilfreich, ausführlicher zu sein, und dann kann man sich umgestalten, um sich zu verbessern Leistung und Code deklarativer machen
Sami Birnbaum
1
Ich stimme @SamiBirnbaum etwas zu. Dies ist die einzige, die fast kein spezielles Rubinwissen wie verwendet Hash.new(0). Der Pseudocode am nächsten. Das kann eine gute Sache für die Lesbarkeit sein, aber auch unnötige Arbeit kann die Lesbarkeit für Leser beeinträchtigen, die es bemerken, weil sie in komplexeren Fällen ein wenig Zeit damit verbringen, zu denken, dass sie verrückt werden, um herauszufinden, warum es getan wird.
Adamantish
1

Dies ist eher ein Kommentar als eine Antwort, aber ein Kommentar würde dem nicht gerecht. Wenn Sie dies tun Array = foo, stürzen Sie mindestens eine Implementierung von IRB ab:

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>

Das liegt daran, dass Arrayes eine Klasse ist.

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

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

Die verstrichene Zeit betrug 0,028 Millisekunden

Interessanterweise wurde die Implementierung von stupidgeek bewertet:

Die verstrichene Zeit betrug 0,041 Millisekunden

und die gewinnende Antwort:

Die verstrichene Zeit betrug 0,011 Millisekunden

:) :)

Alex Moore-Niemi
quelle