好的,我在解决这个问题时遇到了一些麻烦。 我最初试图制作一个word_counter哈希,它将出现次数作为关键字,并将所有单词的数组作为值。
我的代码是..
string = "hello hello hello hi hi to to a"
word_count = string.scan(/\w+/).each_with_object(Hash.new(0)) do |word,hash|
hash[word.downcase] += 1
end
word_count = word_count.group_by {|k,v| v }
# => {3=>[["hello", 3]], 2=>[["hi", 2], ["to", 2]], 1=>[["a", 1]]}
所以主要的问题是我不想要一个二维数组作为值,只希望它们包含这些单词。
我最终找到了这个解决方案
word_count.inject({}) {|h, (k,v)| h[v] ||= []; h[v] << k; h }
# => {3=>["hello"], 2=>["hi", "to"], 1=>["a"]}
这正是我所需要的,但我还没有完全理解它是如何工作的。有人可以带我走过这个吗?谢谢!
更新:感谢您的回答,我真的很感激他们!我对现在发生的事情有了更透彻的了解。
答案 0 :(得分:4)
首先,知道Enumerable#inject
也称为reduce
可能有也可能没有帮助,因为它需要一组值(散列或数组)并将其“减少”为单个值。在这种情况下,结果值本身就是另一个集合,但它可以是任何东西;操作和返回类型由传递给inject
的初始值和块确定。
任何时候你可能会发现自己想做这样的事情:
my_result = some_starter_value
some_collection.each do |item|
my_result.incorporate( some_function_of(item) )
end
你基本上是以手动形式输入注入/减少模式。如果您使用inject
,则上述代码将变为:
my_result = some_collection.inject( some_starter_value ) do |so_far, item|
so_far.incorporate( some_function_of(item) )
so_far
end
在函数式编程语言中,此操作称为“折叠” - 特别是“左折叠”。
要认识到的重要一点是,作为“结果到目前为止”传递到块中的值是块的最后一次运行的返回值。因此,块不仅必须修改结果,还要返回其新值。我喜欢使用Object#tap
来实现自动:
my_result = some_collection.inject( some_starter_value ) do |so_far, item|
so_far.tap { |sf| sf.incorporate( some_function_of(item) ) }
# tap call returns so_far itself, no matter what the block returns
end
无论如何,关于你的代码:
word_count.inject({}) {|h, (k,v)| h[v] ||= []; h[v] << k; h }
您正在哈希上运行inject
,并传入空哈希作为初始值。所以手动版本看起来像这样:
my_result = {}
word_count.each do |key, value|
my_result[value] ||= [] # if my_result[value] is nil, set it to empty array
my_result[value] << key # append this key to the array
end
当您在散列上运行inject
时,该块会获得两个值:当前结果 - 远至,以及当前键/值对作为数组。所以它看起来像这样:
my_result = word_count.inject({}) do |new_hash, kvpair|
key, value = kvpair
new_hash[value] ||= []
new_hash[value] << key
new_hash # remember to return new value from block
end
但您可以使用解构来跳过单独的键/值拆分步骤:
my_result = word_count.inject({}) do |new_hash, (key, value)|
new_hash[value] ||= []
new_hash[value] << key
new_hash # remember to return new value from block
end
将变量名称简化为h,k和v,并将其放在一行{
... }
代替do
... {{1}那是你的代码。
正如我所说,我个人喜欢使用end
而不是在最后重复哈希。如果您愿意,也可以将初始化和附加合并到一个表达式中:
tap
但这可能会伤害可读性。您可以尝试使用原始哈希执行的操作,并在构造函数中指定默认值:
my_result = word_count.inject({}) do |new_hash, (key, value)|
new_hash.tap { |h| (h[value] ||= []) << key }
end
但实际上并没有产生预期的效果,因为每个条目都获得对同一个数组的引用,因此最终会有完全相同的单词列表。相反,你需要做这样的事情:
# Warning: does not work!
my_result = word_count.inject( Hash.new([]) ) do |new_hash, (key, value)|
new_hash.tap { |h| h[value] << key }
end
......在这一点上,它不再仅仅是在块内进行初始化的改进。
最后,这种特定类型的缩减,其中对块的每次调用都得到它作为“迄今为止的值”对同一(可变)对象的不变引用,可能最好使用Enumberable#each_with_object
在Ruby中建模。而不是my_result = word_count.inject( Hash.new {|h,k| h[k]=[]} ) do |new_hash, (key, value)|
new_hash.tap { |h| h[value] << key }
end
- 正如您在构建字数的初始哈希时所做的那样。与inject
不同(但与inject
类似),tap
并不关心块的返回值;它总是返回传递给它的同一个对象。令人困惑的是,当each_with_object
将前一个块的返回值作为第一个参数传递时,inject
将其对象作为 last 参数传递(可能是为了以与之相对应的方式行为)同名的Enumerable#each_with_index
):
each_with_object
答案 1 :(得分:1)
word_count.inject({}) { |h, (k,v)| h[v] ||= []; h[v] << k; h }
inject
是一种经典的函数式编程工具。
它需要一个初始值(在图片中它被称为“z”),然后将所有值逐个应用于它,产生一个新值,它将成为下一轮的初始值。
inject(initial) { |memo, obj| block } → obj
文档列出了这个简单计算总和的例子,也许它有助于理解这个概念:
(5..10).inject { |sum, n| sum + n } #=> 45
块的解释:
{ |h, (k,v)| h[v] ||= []; h[v] << k; h }
^ ^ ^ ^ ^
| | | | return the hash for the next round.
| | | add the element to the list
| | create a new entry in the hash if it doesn't exist yet
| the next input, in this case, a key-value pair
previous value of hash (in the first round this is the initial value)
答案 2 :(得分:1)
您正在对哈希中的键值对进行分组。这就是你将数组作为['hi', 2]
的原因。要仅按键值对键进行分组,您必须首先使用哈希中的值进行分组:
word_count.keys.group_by{ |k| word_count[k] }
# => {3=>["hello"], 2=>["hi", "to"]}
答案 3 :(得分:1)
这里有一些值得一提的东西。
计算单词
首先,你的正则表达式应该是/\w+/
,而不是/\w/
,但我希望这是一个错字。
你拥有的是更像Ruby的版本:
string = "hello hello hello hi hi to to"
arr = string.scan(/\w+/)
word_count = {}
count = 0
arr.each do |word|
word_count[word] = 0 unless word_count.key?(word)
word_count[word] += 1
end
word_count
#=> {"hello"=>3, "hi"=>2, "to"=>2}
替换掉arr
可以保存一条语句,使用Enumerable#each_with_object可以省去另外两条语句:
count = 0
string.scan(/\w+/).each_with_object({}) do |word, word_count|
word_count[word] = 0 unless word_count.key?(word)
word_count[word] += 1
end
#=> {"hello"=>3, "hi"=>2, "to"=>2}
each_with_object
还有一个好处就是可以阻止所有东西远离窥探(通过创建一个新范围)。
使用默认值零定义word_count
:
word_count = Hash.new(0)
表示如果word_count
没有密钥word
:
word_count[word] #=> 0
重要的是要理解上述语句不更改哈希word_count
。声明:
word_count[word] += 1
扩展为:
word_count[word] = word_count[word] + 1
变为:
word_count[word] = 0 + 1
如果word_count
没有密钥word
。这有时称为计算哈希。所以,一种Ruby方式是写:
string.scan(/\w+/).each_with_object(Hash.new(0)) {|word, word_count| word_count[word] += 1}
#=> {"hello"=>3, "hi"=>2, "to"=>2}
由于您希望计数不区分大小写,因此您已将word
转换为小写。我们还将结果捕获到变量:
word_count = string.scan(/\w+/).each_with_object(Hash.new(0)) do |word, word_count|
word_count[word.downcase] += 1
end
word_count
#=> {"hello"=>3, "hi"=>2, "to"=>2}
另一种方式:
word_count = string.scan(/\w+/).each_with_object({}) do |word, word_count|
word.downcase!
word_count[word] = (word_count[word] || 0) + 1
end
#=> {"hello"=>3, "hi"=>2, "to"=>2}
如果word_count
没有密钥word
,则操作行变为:
word_count[word] = (nil || 0) + 1 #=> 0 + 1
还有一个:
word_count = string.scan(/\w+/).map(&:downcase).group_by(&:itself)
#=> {"hello"=>["hello", "hello", "hello"],
# "hi"=>["hi", "hi"],
# "to"=>["to", "to"]}
word_count.update(word_count) { |*,arr| arr.size }
#=> {"hello"=>3, "hi"=>2, "to"=>2}
这使用Hash#update(aka merge!
)的形式,它使用一个块来确定合并的两个哈希中存在的键的值,这里是所有键。 Object#itself附带了Ruby v2.2。对于早期版本,您需要:
group_by { |word| word }
确定具有相同计数的字词
鉴于word_count
,您的解决方案是:
count_to_words = word_count.group_by { |k,v| v }
#=> {3=>[["hello", 3]], 2=>[["hi", 2], ["to", 2]], 1=>[["a", 1]]}
(在v2.2 +你可以写word_count.group_by(&:itself)
。)
你真是太近了!再多一步:
count_to_words.keys.each do |k|
count_to_words[k] = count_to_words[k].map(&:first)
end
count_to_words
#=> { 3=>["hello"], 2=>["hi", "to"] }
或(正如@Mark提醒我):
count_to_words.tap do |h|
h.keys.each { |k| h[k] = h[k].map(&:first) }
end
#=> { 3=>["hello"], 2=>["hi", "to"] }
我们可以将count_to_words
的{{1}}计算结合起来:
word_count
甚至可以替换word_count.group_by { |k,v| v }.tap do |h|
h.keys.each { |k| h[k] = h[k].map(&:first) }
end
#=> { 3=>["hello"], 2=>["hi", "to"] }
:
word_count
修改string.scan(/\w+/).each_with_object(Hash.new(0)) do |word, word_count|
word_count[word] += 1
end.group_by { |k,v| v }.tap do |h|
h.keys.each { |k| h[k] = h[k].map(&:first) }
end
#=> { 3=>["hello"], 2=>["hi", "to"] }
的最后一种方法(使用count_to_words
,正如我之前所做的那样):
update
还有一件事。你有:
count_to_words.update(count_to_words) { |*,arr| arr.map(&:first) }
#=> { 3=>["hello"], 2=>["hi", "to"] }
重用变量word_count = word_count.group_by { |k,v| v }
。不要那样做。 word_count
现在是一个误导性的名称,因为你现在必须记住每次运行代码时都要重新计算word_count
。总之,不要这样做! 1
Enumerable#reduce(又名word_count
)怎么样?
inject
在Ruby v1.9中引入了它。在此之前,Rubiests使用Enumerable#each_with_object
以更直接的方式完成reduce
所做的事情。 (each_with_object
仍然非常有价值,reduce
是一个简单的例子)。如果您使用找到的arr.reduce(:+)
检查已完成的解决方案,您会发现它们非常相似。两个不同之处:
inject
这里需要为下一次迭代返回“备忘录”;因此,那令人讨厌的inject
;和我之前在; h
中解释过h[v] ||= []
。 (我更喜欢写h[v] ||= []; h[v] << k
。)
1 ...但我可以通过更新来做到这一点。