组哈希值-Ruby

时间:2015-12-02 18:32:58

标签: ruby hash inject

好的,我在解决这个问题时遇到了一些麻烦。 我最初试图制作一个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"]}

这正是我所需要的,但我还没有完全理解它是如何工作的。有人可以带我走过这个吗?谢谢!

更新:感谢您的回答,我真的很感激他们!我对现在发生的事情有了更透彻的了解。

4 个答案:

答案 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是一种经典的函数式编程工具。

inject, aka reduce, aka fold

它需要一个初始值(在图片中它被称为“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 ...但我可以通过更新来做到这一点。