如何合并散列数组以获取值数组的散列
Posted
技术标签:
【中文标题】如何合并散列数组以获取值数组的散列【英文标题】:How to merge array of hashes to get hash of arrays of values 【发布时间】:2011-07-26 08:17:43 【问题描述】:这与Turning a Hash of Arrays into an Array of Hashes in Ruby相反。
优雅和/或高效地将散列数组转换为值是所有值的数组的散列:
hs = [
a:1, b:2 ,
a:3, c:4 ,
b:5, d:6
]
collect_values( hs )
#=> :a=>[1,3], :b=>[2,5], :c=>[4], :d=>[6]
这段简洁的代码几乎可以工作,但是当没有重复时无法创建数组:
def collect_values( hashes )
hashes.inject() |a,b| a.merge(b) |_,x,y| [*x,*y]
end
collect_values( hs )
#=> :a=>[1,3], :b=>[2,5], :c=>4, :d=>6
这段代码可以,但你能写一个更好的版本吗?
def collect_values( hashes )
# Requires Ruby 1.8.7+ for Object#tap
Hash.new |h,k| h[k]=[] .tap do |result|
hashes.each |h| h.each |k,v| result[k]<<v
end
end
仅适用于 Ruby 1.9 的解决方案是可以接受的,但应注意。
以下是使用三个不同的哈希数组对以下各种答案(以及我自己的一些答案)进行基准测试的结果:
每个哈希都有不同的键,因此不会发生合并:[:a=>1, :b=>2, :c=>3, :d=>4, :e=>5, :f=>6, :g=>7, ...]
每个哈希都具有相同的键,因此发生最大合并:[:a=>1, :a=>2, :a=>3, :a=>4, :a=>5, :a=>6, :a=>7, ...]
[:c=>1, :d=>1, :c=>2, :f=>1, :c=>1, :d=>1, :h=>1, :c=>3, ...]
用户系统总真实
Phrogz 2a 0.577000 0.000000 0.577000 ( 0.576000)
Phrogz 2b 0.624000 0.000000 0.624000 (0.620000)
格伦 1 0.640000 0.000000 0.640000 (0.641000)
Phrogz 1 0.671000 0.000000 0.671000 (0.668000)
迈克尔 1 0.702000 0.000000 0.702000 ( 0.700000)
迈克尔 2 0.717000 0.000000 0.717000 (0.726000)
格伦 2 0.765000 0.000000 0.765000 (0.764000)
fl00r 0.827000 0.000000 0.827000 (0.836000)
锯 0.874000 0.000000 0.874000 ( 0.868000)
托克兰 1 0.873000 0.000000 0.873000 ( 0.876000)
托克兰 2 1.077000 0.000000 1.077000 ( 1.073000)
Phrogz 3 2.106000 0.093000 2.199000 (2.209000)
最快的代码是我加的这个方法:
def collect_values(hashes)
.tap |r| hashes.each |h| h.each |k,v| (r[k]||=[]) << v
end
我接受了“glenn mcdonald's answer”,因为它在速度方面具有竞争力,相当简洁,但(最重要的是)因为它指出了使用带有自修改默认 proc 的哈希以方便构建的危险,因为这可能会在用户稍后对其编制索引时引入错误的更改。
最后,这里是基准代码,如果您想运行自己的比较:
require 'prime' # To generate the third hash
require 'facets' # For tokland1's map_by
AZSYMBOLS = (:a..:z).to_a
TESTS =
'26 Distinct Hashes' => AZSYMBOLS.zip(1..26).map|a| Hash[*a] ,
'26 Same-Key Hashes' => ([:a]*26).zip(1..26).map|a| Hash[*a] ,
'26 Mixed-Keys Hashes' => (2..27).map do |i|
factors = i.prime_division.transpose
Hash[AZSYMBOLS.values_at(*factors.first).zip(factors.last)]
end
def phrogz1(hashes)
Hash.new |h,k| h[k]=[] .tap do |result|
hashes.each |h| h.each |k,v| result[k]<<v
end
end
def phrogz2a(hashes)
.tap |r| hashes.each |h| h.each |k,v| (r[k]||=[]) << v
end
def phrogz2b(hashes)
hashes.each_with_object() |h,r| h.each |k,v| (r[k]||=[]) << v
end
def phrogz3(hashes)
result = hashes.inject() |a,b| a.merge(b) |_,x,y| [*x,*y]
result.each |k,v| result[k] = [v] unless v.is_a? Array
end
def glenn1(hs)
hs.reduce() |h,pairs| pairs.each |k,v| (h[k] ||= []) << v; h
end
def glenn2(hs)
hs.map(&:to_a).flatten(1).reduce() |h,(k,v)| (h[k] ||= []) << v; h
end
def fl00r(hs)
h = Hash.new|h,k| h[k]=[]
hs.map(&:to_a).flatten(1).each|v| h[v[0]] << v[1]
h
end
def sawa(a)
a.map(&:to_a).flatten(1).group_by|k,v| k.each_value|v| v.map!|k,v| v
end
def michael1(hashes)
h = Hash.new|h,k| h[k]=[]
hashes.each_with_object(h) do |h, result|
h.each |k, v| result[k] << v
end
end
def michael2(hashes)
h = Hash.new|h,k| h[k]=[]
hashes.inject(h) do |result, h|
h.each |k, v| result[k] << v
result
end
end
def tokland1(hs)
hs.map(&:to_a).flatten(1).map_by |k, v| [k, v]
end
def tokland2(hs)
Hash[hs.map(&:to_a).flatten(1).group_by(&:first).map |k, vs|
[k, vs.map|o|o[1]]
]
end
require 'benchmark'
N = 10_000
Benchmark.bm do |x|
x.report('Phrogz 2a') TESTS.each |n,h| N.times phrogz2a(h)
x.report('Phrogz 2b') TESTS.each |n,h| N.times phrogz2b(h)
x.report('Glenn 1 ') TESTS.each |n,h| N.times glenn1(h)
x.report('Phrogz 1 ') TESTS.each |n,h| N.times phrogz1(h)
x.report('Michael 1') TESTS.each |n,h| N.times michael1(h)
x.report('Michael 2') TESTS.each |n,h| N.times michael2(h)
x.report('Glenn 2 ') TESTS.each |n,h| N.times glenn2(h)
x.report('fl00r ') TESTS.each |n,h| N.times fl00r(h)
x.report('sawa ') TESTS.each |n,h| N.times sawa(h)
x.report('Tokland 1') TESTS.each |n,h| N.times tokland1(h)
x.report('Tokland 2') TESTS.each |n,h| N.times tokland2(h)
x.report('Phrogz 3 ') TESTS.each |n,h| N.times phrogz3(h)
end
【问题讨论】:
如果您需要此功能,请随时为这个问题投票,以便其他人可以找到它。我问了这个问题并包含了一些工作代码,因为(据我所知)在 Stack Overflow 上已经没有很好的答案了。 +1 提出一个有趣的问题,唉,到目前为止,我想不出比您的工作解决方案更好的方法。 我认为每个答案也应该根据您的hs
提供标准化的基准测试结果。
@theTinMan 问,你会收到:)
【参考方案1】:
['a' => 1, 'b' => 2, 'c' => 3].reduce Hash.new, :merge
【讨论】:
虽然这个 sn-p 可以帮助回答这个问题,但最好添加一个解释为什么你认为这没有帮助 其实这个sn-p并没有回答问题。它产生的结果是:a=>3, :b=>5, :c=>4, :d=>6
,而不是所需的 :a=>[1,3], :b=>[2,5], :c=>[4], :d=>[6]
。这与 Ich 已经发布的解决方案相同。【参考方案2】:
这个呢?
hs.reduce(, :merge)
最短!但是性能很差:
user system total real
Phrogz 2a 0.240000 0.010000 0.250000 ( 0.247337)
Phrogz 2b 0.280000 0.000000 0.280000 ( 0.274985)
Glenn 1 0.290000 0.000000 0.290000 ( 0.290370)
Phrogz 1 0.310000 0.000000 0.310000 ( 0.315548)
Michael 1 0.360000 0.000000 0.360000 ( 0.356760)
Michael 2 0.360000 0.000000 0.360000 ( 0.360119)
Glenn 2 0.370000 0.000000 0.370000 ( 0.369354)
fl00r 0.390000 0.000000 0.390000 ( 0.385883)
sawa 0.410000 0.000000 0.410000 ( 0.408190)
Tokland 1 0.410000 0.000000 0.410000 ( 0.410097)
Tokland 2 0.490000 0.000000 0.490000 ( 0.497325)
Ich 1.410000 0.000000 1.410000 ( 1.413176) # <<-- new
Phrogz 3 1.760000 0.010000 1.770000 ( 1.762979)
【讨论】:
它又可爱又短,但它无法产生正确的结果。 (阅读问题以了解它们是什么。)实际结果::a=>3, :b=>5, :c=>4, :d=>6
。【参考方案3】:
我认为比较获胜者可能会很有趣:
def phrogz2a(hashes)
.tap |r| hashes.each |h| h.each |k,v| (r[k]||=[]) << v
end
略有不同:
def phrogz2ai(hashes)
Hash.new |h,k| h[k]=[].tap |r| hashes.each |h| h.each |k,v| r[k] << v
end
因为人们通常可以采用任何一种方法(通常是创建一个空数组或散列)。
使用 Phrogz 的基准代码,这里是它们的比较方式:
user system total real
Phrogz 2a 0.440000 0.010000 0.450000 ( 0.444435)
Phrogz 2ai 0.580000 0.010000 0.590000 ( 0.580248)
【讨论】:
这是有趣的。谢谢你。【参考方案4】:Facet 的Enumerable#map_by 在这些情况下会派上用场。这种实现无疑会比其他实现慢,但模块化和紧凑的代码总是更容易维护:
require 'facets'
hs.flat_map(&:to_a).map_by |k, v| [k, v]
#=> :b=>[2, 5], :d=>[6], :c=>[4], :a=>[1, 3]
【讨论】:
你的#second
方法从何而来?
@Phrogz:active_support。我用它只是为了简洁,如果没有加载AS,就写典型的块。
谢谢,我写了一个简单的块并将您的答案与其他答案进行了基准比较,并更新了问题中的摘要。【参考方案5】:
h = Hash.new|h,k| h[k]=[]
hs.map(&:to_a).flatten(1).each|v| h[v[0]] << v[1]
【讨论】:
【参考方案6】:与使用map(&:to_a).flatten(1)
的其他一些答案相同。问题是如何修改哈希值。我利用了数组是可变的这一事实。
def collect_values a
a.map(&:to_a).flatten(1).group_by|k, v| k.
each_value|v| v.map!|k, v| v
end
【讨论】:
【参考方案7】:任你选:
hs.reduce() |h,pairs| pairs.each |k,v| (h[k] ||= []) << v; h
hs.map(&:to_a).flatten(1).reduce() |h,(k,v)| (h[k] ||= []) << v; h
我强烈反对像其他建议那样弄乱哈希的默认值,因为然后 检查 一个值会修改哈希,这对我来说似乎是非常错误的。
【讨论】:
我认为第二种解决方案会更快 这是一个很好的点,我的 Hash-with-default_proc 与 default_proc 完好无损,从而影响了未来的使用。 (当我获得更多选票时会 +1。) 在快速性能测试中,第二个比第一个慢约 1.5 倍。为什么你期望第二个更快,fl00r? 在我来到这里之前,我一直在努力思考这个问题。我试过: a.reduce() |acc,k,v| acc[k]=v; acc 不幸地产生了这个:=> :a=>"b"=>nil, :c=>"d"=>nil。显然这不是我想要的。为了获得所需的东西,您必须使用您的解决方案,即在块中运行每个。为什么 reduce 不允许我们将 hash 的 key 和 value 传递给第二个参数?如果它允许,那么我们就不需要在块的范围内使用 each。 其实我误解了这里的问题。他们希望值是一个数组。难怪。否则你只需要做 a.reduce(:merge)。【参考方案8】:怎么样?
def collect_values(hashes)
h = Hash.new|h,k| h[k]=[]
hashes.each_with_object(h) do |h, result|
h.each |k, v| result[k] << v
end
end
编辑 - 也可以使用注入,但恕我直言,没有那么好:
def collect_values( hashes )
h = Hash.new|h,k| h[k]=[]
hashes.inject(h) do |result, h|
h.each |k, v| result[k] << v
result
end
end
【讨论】:
为什么h = Hash.new([])
不起作用?为此我收到了
。
@rubyprince 这不起作用,因为 a) 当您请求不存在的密钥时,它只会 返回 一个数组,但不会设置 该数组的键,并且 b) 它为每个键返回 same 数组。在 IRB 中运行此代码并思考输出:h=Hash.new([]); p h[1]; p h; h[:foo] << :a; p h[:bar]; p h
我希望我能两次投票支持你提醒我each_with_object
。然而,有趣的是,使用它比仅使用tap
更多字符且速度更慢。 (比较基准测试中的 phrogz2a
和 phrogz2b
方法。)以上是关于如何合并散列数组以获取值数组的散列的主要内容,如果未能解决你的问题,请参考以下文章