【问题标题】: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=&gt;1}, {:b=&gt;2}, {:c=&gt;3}, {:d=&gt;4}, {:e=&gt;5}, {:f=&gt;6}, {:g=&gt;7}, ...]

  • 每个哈希都具有相同的键,因此发生最大合并:
    [{:a=&gt;1}, {:a=&gt;2}, {:a=&gt;3}, {:a=&gt;4}, {:a=&gt;5}, {:a=&gt;6}, {:a=&gt;7}, ...]

  • 混合了唯一密钥和共享密钥:
    [{:c=&gt;1}, {:d=&gt;1}, {:c=&gt;2}, {:f=&gt;1}, {:c=&gt;1, :d=&gt;1}, {:h=&gt;1}, {:c=&gt;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 问,你会收到:)

标签: ruby arrays hash merge


【解决方案1】:
[{'a' => 1}, {'b' => 2}, {'c' => 3}].reduce Hash.new, :merge

【讨论】:

  • 虽然这个 sn-p 可以帮助回答这个问题,但最好添加一个解释为什么你认为这没有帮助
  • 其实这个sn-p并没有回答问题。它产生的结果是{:a=&gt;3, :b=&gt;5, :c=&gt;4, :d=&gt;6},而不是所需的{ :a=&gt;[1,3], :b=&gt;[2,5], :c=&gt;[4], :d=&gt;[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=&gt;3, :b=&gt;5, :c=&gt;4, :d=&gt;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(&amp;: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] &lt;&lt; :a; p h[:bar]; p h
      • 我希望我能两次投票支持你提醒我each_with_object。然而,有趣的是,使用它比仅使用tap 更多字符且速度更慢。 (比较基准测试中的 phrogz2aphrogz2b 方法。)
      猜你喜欢
      • 2015-02-03
      • 2014-11-22
      • 2018-01-02
      • 1970-01-01
      • 1970-01-01
      • 2023-04-10
      • 2014-03-14
      • 2011-07-24
      • 2015-09-18
      相关资源
      最近更新 更多