我正在查看有关如何从数组返回模式的代码,我遇到了以下代码:
def mode(array)
answer = array.inject ({}) { |k, v| k[v]=array.count(v);k}
answer.select { |k,v| v == answer.values.max}.keys
end
我试图概念化语法背后的含义,因为我对 Ruby 相当陌生,并不完全了解哈希在这里的使用方式。任何帮助将不胜感激。
逐行:
answer = array.inject ({}) { |k, v| k[v]=array.count(v);k}
这将组装计数哈希。我不会称变量为answer
因为它不是答案,它是一个中间步骤。inject()
方法(也称为reduce()
(允许您迭代集合,保留累加器(例如,运行总计或在这种情况下哈希收集计数(。它需要起始值 {}
,以便在尝试存储值时存在哈希。给定数组[1,2,2,2,3,4,5,6,6]
计数将如下所示:{1=>1, 2=>3, 3=>1, 4=>1, 5=>1, 6=>2}
.
answer.select { |k,v| v == answer.values.max}.keys
这将选择上述哈希值等于最大值(换句话说,最大值(的所有元素。然后,它标识与最大值关联的keys
。请注意,如果它们共享最大值,它将列出多个值。
另一种选择:
如果您不关心返回多个,则可以按如下方式使用group_by:
array.group_by{|x|x}.values.max_by(&:size).first
或者,在 Ruby 2.2+ 中:
array.group_by{&:itself}.values.max_by(&:size).first
inject
方法的作用类似于累加器。下面是一个更简单的示例:
sum = [1,2,3].inject(0) { |current_tally, new_value| current_tally + new_value }
0 是起点。
因此,在第一行之后,我们有一个哈希值,将每个数字映射到它出现的次数。
该模式调用最频繁的元素,这就是下一行的作用:仅选择等于最大值的元素。
你的问题已经得到了回答,@Mark提到了不同的计算方法。我只想关注改进第一行代码的其他方法:
answer = array.inject ({}) { |k, v| k[v] = array.count(v); k }
首先,让我们创建一些数据:
array = [1,2,1,4,3,2,1]
使用each_with_object
而不是inject
我怀疑代码可能相当旧,因为在 v. 1.9 中引入的 Enumerable#each_with_object 可以说是比 Enumerable#inject(又名 reduce
(更好的选择。如果我们要使用each_with_object
,第一行将是:
answer = array.each_with_object ({}) { |v,k| k[v] = array.count(v) }
#=> {1=>3, 2=>2, 4=>1, 3=>1}
each_with_object
返回对象,块变量 v
持有的哈希。
如您所见,each_with_object
与inject
非常相似,唯一的区别是:
- 没有必要
- 像
inject
那样将v
从块返回到each_with_object
(inject
块末尾那个烦人; v
的原因(; - 对象的块变量(
k
(跟在v
后面each_with_object
,而它跟v
inject
;和 - 当未给定块时,
each_with_object
返回一个枚举器,这意味着它可以链接到其他方法(例如,arr.each_with_object.with_index ...
.
不要误会我的意思,inject
仍然是一种非常强大的方法,在许多情况下它是无与伦比的。
另外两项改进
除了将inject
替换为 each_with_object
之外,让我做另外两个更改:
answer = array.uniq.each_with_object ({}) { |k,h| h[k] = array.count(k) }
#=> {1=>3, 2=>2, 4=>1, 3=>1}
在原始表达式中,inject
返回的对象(有时称为"备忘录"(由块变量 k
表示,我用它来表示哈希键("k"表示"键"(。同样,由于对象是一个哈希,我选择使用 h
作为其块变量。像许多其他变量一样,我更喜欢保持块变量简短,并使用指示对象类型的名称(例如,a
表示数组,h
表示哈希,s
表示字符串,sym
表示符号等(。
现在假设:
array = [1,1]
然后inject
会将第一个1
传递到块中,然后计算k[1] = array.count(1) #=> 2
,因此返回给inject
的哈希k
将被{1=>2}
。然后它会将第二个1
传递到块中,再次计算k[1] = array.count(1) #=> 2
,用1=>1
覆盖k
中的1=>1
;也就是说,根本不改变它。对于array
的唯一值这样做不是更有意义吗?这就是为什么我有:array.uniq...
.
更好的是:使用计数哈希
这仍然效率很低 - 所有这些counts
。这是一种阅读效果更好且可能更有效的方法:
array.each_with_object(Hash.new(0)) { |k,h| h[k] += 1 }
#=> {1=>3, 2=>2, 4=>1, 3=>1}
让我们来看看这个血腥的细节。首先,Hash#new 的文档显示:"如果指定了obj
[即Hash.new(obj)
],则此单个对象将用于所有默认值。这意味着如果:
h = Hash.new('cat')
而h
没有密钥dog
,那么:
h['dog'] #=> 'cat'
重要提示:最后一个表达式经常被误解。它仅返回默认值。 str = "It does *not* add the key-value pair 'dog'=>'cat' to the hash."
让我重复一遍:puts str
.
现在让我们看看这里发生了什么:
enum = array.each_with_object(Hash.new(0))
#=> #<Enumerator: [1, 2, 1, 4, 3, 2, 1]:each_with_object({})>
我们可以通过将其转换为数组来查看枚举器的内容:
enum.to_a
#=> [[1, {}], [2, {}], [1, {}], [4, {}], [3, {}], [2, {}], [1, {}]]
这七个元素通过方法传递到块中 each
:
enum.each { |k,h| h[k] += 1 }
=> {1=>3, 2=>2, 4=>1, 3=>1}
很酷,是吗?
我们可以使用 Enumerator#next 来模拟这一点。enum
([1, {}]
(的第一个值被传递给块并分配给块变量:
k,h = enum.next
#=> [1, {}]
k #=> 1
h #=> {}
我们计算:
h[k] += 1
#=> h[k] = h[k] + 1 (what '+=' means)
# = 0 + 1 = 1 (h[k] on the right equals the default value
# of 1 since `h` has no key `k`)
所以现在:
h #=> {1=>1}
接下来,each
将enum
的第二个值传递到块中,并执行类似的计算:
k,h = enum.next
#=> [2, {1=>1}]
k #=> 2
h #=> {1=>1}
h[k] += 1
#=> 1
h #=> {1=>1, 2=>1}
当传入enum
的第三个元素时,情况会有所不同,因为h
现在有一个关键1
:
k,h = enum.next
#=> [1, {1=>1, 2=>1}]
k #=> 1
h #=> {1=>1, 2=>1}
h[k] += 1
#=> h[k] = h[k] + 1
#=> h[1] = h[1] + 1
#=> h[1] = 1 + 1 => 2
h #=> {1=>1, 2=>1}
其余计算的执行方式类似。