将嵌套的哈希转换为扁平化的哈希

26

这个问题是 这个问题 的相反。

给定一个类似嵌套的哈希表:

{
    :a => {
       :b => {:c => 1, :d => 2},
       :e => 3,
    },
    :f => 4,
}

如何将它转换成一个扁平的哈希表?

{
    [:a, :b, :c] => 1,
    [:a, :b, :d] => 2,
    [:a, :e] => 3,
    [:f] => 4,
}

3
你希望得到一个按顺序排列的键数组,用于查找对应的值? - Linuxios
8个回答

19

另一种方式:

def flat_hash(h,f=[],g={})
  return g.update({ f=>h }) unless h.is_a? Hash
  h.each { |k,r| flat_hash(r,f+[k],g) }
  g
end

h = { :a => { :b => { :c => 1,
                      :d => 2 },
              :e => 3 },
      :f => 4 }

flat_hash(h) #=> {[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}

我知道Ruby并不一定支持TCO,但如果你在这里没有返回g,那么这个代码会被尾递归优化吗? - rusty
我听说过“尾调用优化”这个术语,但我不知道它是什么意思。也许一位计算机科学家可以回答你的问题。 - Cary Swoveland
在我看来,由于您有级联递归,因此无法优化尾调用。 - Stefan Majewsky

16

非常类似于Adiel Mittmann的解决方案。

def flat_hash(h, k = [])
  new_hash = {}
  h.each_pair do |key, val|
    if val.is_a?(Hash)
      new_hash.merge!(flat_hash(val, k + [key]))
    else
      new_hash[k + [key]] = val
    end
  end
  new_hash
end

编辑: 为了优雅而重构。应该几乎与原来一样快。

def flat_hash(hash, k = [])
  return {k => hash} unless hash.is_a?(Hash)
  hash.inject({}){ |h, v| h.merge! flat_hash(v[-1], k + [v[0]]) }
end

这个跑得最快。谢谢。 - sawa
1
@sawa:给你一个未来的提示:如果你想要一个快速的解决方案,下次在问题中提到这一点。通常像Python或Ruby这样的动态语言的主要标准是优美和简洁。如果你特别要求性能,你可能会得到更适合的答案 :) - Niklas B.
1
@sawa:重构以提高代码的优雅性。 - Kyle
@Kyle,你之前的代码有一个好处,就是可以重新定义为Hash上的一个方法。而重写后的代码则不行。 - sawa
@Niklas,也许吧,但大多数Ruby读者都知道sawa是个速度狂。 - Cary Swoveland

8

My attempt:

def flatten_hash(h)
  return { [] => h } unless h.is_a?(Hash)
  Hash[h.map { |a,v1| flatten_hash(v1).map { |b,v2| [[a] + b, v2] } }.flatten(1)]
end

抱歉变量名称不好,必须将其放在一行中。


5

这不是试图给您提供最好的方法,但这是一种方法:P

def flatten(hash)
  return {[] => hash} if !hash.is_a?(Hash)
  map = {}
  hash.each_pair do |key1, value1|
    flatten(value1).each_pair do |key2, value2|
      map[[key1] + key2] = value2
    end
  end
  return map
end

它适用于你的示例,生成以下结果:

{[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}

如果存在空哈希表,则可能无法产生您期望的结果。

3

一种函数式的方法(参见 历史 了解另一种实现):

def recursive_flatten(hash)
  hash.flat_map do |key, value|
    if value.is_a?(Hash)
      recursive_flatten(value).map { |ks, v| [[key] + ks, v] } 
    else
      [[[key], value]]
    end
  end.to_h
end

1

使用DeepEnumerable的声明性解决方案:

require 'deep_enumerable'

h = { :a => { :b => { :c => 1, :d => 2 }, :e => 3 }, :f => 4 }

h.deep_each.map do |k, v|
  [DeepEnumerable.deep_key_to_array(k), v]
end.to_h

或者,对于那些偏爱无点风格的人。
h.deep_each.to_h.shallow_map_keys(&DeepEnumerable.method(:deep_key_to_array))

1
受@cary-swoveland的启发,但是在Hash类中实现:
class Hash
  def deep_flatten(previous_key=[])
    flat_hash = {}
    self.each do |key, value|
      next_key = previous_key+[key]
      flat_hash.update(value.is_a?(Hash) ? value.deep_flatten(next_key) : {next_key=>value})
    end
    return flat_hash
  end
end

h = { :a => { :b => { :c => 1, :d => 2 }, :e => 3 }, :f => 4 }

h.deep_flatten #=> {[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}

1

数组支持/易读的名称/不更新以提高速度/字符串化结果键

def flat_hash(input, base = nil, all = {})
  if input.is_a?(Array)
    input = input.each_with_index.to_a.each(&:reverse!)
  end

  if input.is_a?(Hash) || input.is_a?(Array)
    input.each do |k, v|
      flat_hash(v, base ? "#{base}.#{k}" : k, all)
    end
  else
    all[base] = input
  end

  all
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接