【问题标题】:How do I flatten a nested hash, recursively, into an array of arrays with a specific format?如何递归地将嵌套哈希展平为具有特定格式的数组数组?
【发布时间】:2019-08-01 07:46:06
【问题描述】:

我有一个看起来像这样的嵌套哈希:

{
  'a' => {
    'b' => ['c'],
    'd' => {
      'e' => ['f'],
      'g' => ['h', 'i', 'j', 'k']
    },
    'l' => ['m', 'n', 'o', 'p']
  },
  'q' => {
    'r' => ['s']
  }
}

哈希可以有更多的嵌套,但最后一层的值总是数组。

我想将散列“展平”为一种格式,在该格式中,我得到一个数组数组,表示构成嵌套散列的整个“路径/分支”的所有键和值,从最低级别的值一直到哈希的顶部。这有点像从底部开始遍历“树”,同时在途中收集键和值。

特定哈希的输出应该是:

[
  ['a', 'b', 'c'],
  ['a', 'd', 'e', 'f'],
  ['a', 'd', 'g', 'h', 'i', 'j', 'k'],
  ['a', 'l', 'm', 'n', 'o', 'p'],
  ['q', 'r', 's']
]

我尝试了很多不同的方法,但到目前为止都没有奏效。再次记住,可能会出现比这些更多的级别,因此解决方案必须是通用的。

注意:数组的顺序和其中元素的顺序并不重要。

我做了以下,但它并没有真正起作用:

tree_def = {
  'a' => {
    'b' => ['c'],
    'd' => {
      'e' => ['f'],
      'g' => ['h', 'i', 'j', 'k']
    },
    'l' => ['m', 'n', 'o', 'p']
  },
  'q' => {
    'r' => ['s']
  }
}
branches = [[]]

collect_branches = lambda do |tree, current_branch|
  tree.each do |key, hash_or_values|
    current_branch.push(key)
    if hash_or_values.kind_of?(Hash)
      collect_branches.call(hash_or_values, branches.last)
    else # Reached lowest level in dependency tree (which is always an array)
      # Add a new branch
      branches.push(current_branch.clone)
      current_branch.push(*hash_or_values)
      current_branch = branches.last
    end
  end
end

collect_branches.call(tree_def, branches[0])

branches #=> wrong result

【问题讨论】:

  • 看起来很简单。递归下降到散列,记下您在此分支中访问过的键。当您看到一个数组时,无需进一步递归。将其附加到键列表并返回。
  • “有点像穿越‘树’”——是的。我想说,这完全就像遍历一棵树。只是不知道为什么你认为这是“从底部开始”。
  • @SergioTulentsev 这只是为了更“清楚”输出所代表的内容,有 5 个“最终”值最终会给出 5 个分支。这是你给出的好建议,但是我仍然很难弄清楚如何跟踪在递归遍历时向哪个“分支”添加什么值的代码......
  • 跟踪很容易,只需将临时状态传递给参数中的递归调用。
  • this 是@CarySwoveland 提供的最佳解决方案

标签: ruby


【解决方案1】:

正如 cmets 中所暗示的:

看起来很简单。递归下降到散列,记下您在此分支中访问的键。当您看到一个数组时,无需进一步递归。将其附加到键列表并返回

跟踪很容易,只需将临时状态传递给参数中的递归调用。

我的意思是这样的:

def tree_flatten(tree, path = [], &block)
  case tree
  when Array
    block.call(path + tree)
  else
    tree.each do |key, sub_tree|
      tree_flatten(sub_tree, path + [key], &block)
    end
  end
end

tree_flatten(tree_def) do |path|
  p path
end

这段代码只是简单地打印每个扁平化的路径,但您也可以将它存储在一个数组中。或者甚至修改tree_flatten 以返回一个准备好的数组,而不是一个一个地产生元素。

【讨论】:

  • 很好的答案,我明白了!
  • 如果 OP 对此感到满意,那就太好了,但 tree_flatten 不会返回所需的数据结构(数组)。
  • @ArtuX 是的,这是明确提到的。另外,这将是一个很好的练习。
【解决方案2】:

你可以这样做:

def flat_hash(h)
  return [h] unless h.kind_of?(Hash)
  h.map{|k,v| flat_hash(v).map{|e| e.unshift(k)} }.flatten(1)
end

input = {
  'a' => {
    'b' => ['c'],
    'd' => {
      'e' => ['f'],
      'g' => ['h', 'i', 'j', 'k']
    },
    'l' => ['m', 'n', 'o', 'p']
  },
  'q' => {
    'r' => ['s']
  }
}

p flat_hash(input)

输出将是:

[
  ["a", "b", "c"], 
  ["a", "d", "e", "f"], 
  ["a", "d", "g", "h", "i", "j", "k"], 
  ["a", "l", "m", "n", "o", "p"], 
  ["q", "r", "s"]
]

【讨论】:

    【解决方案3】:

    这将返回一个包含所有路径的数组。

    def paths(element, path = [], accu = [])
      case element
      when Hash
        element.each do |key, value|
          paths(value, path + [key], accu)
        end
      when Array
        accu << (path + element)
      end
      accu
    end
    

    为了更好的打印,你可以这样做

    paths(tree_def).map { |path| path.join(".") }
    

    【讨论】:

      【解决方案4】:

      请参阅以下内容,它将继续递归调用,直到到达数组值。

      此递归调用将与多个分支一起使用,op 应该是每个分支的单独副本,因此我使用了始终在此处创建为新对象的字符串,否则数组将与 通过引用调用强>

      hash = {"a"=>{"b"=>["c"], "d"=>{"e"=>["f"], "g"=>["h", "i", "j", "k"]}, "l"=>["m", "n", "o", "p"]}, "q"=>{"r"=>["s"]}}
      
      @output = []
      
      def nested_array(h, op='')
        h.map do |k,v|
          if Hash === v
            nested_array(v, op+k)
          else
            @output << (op+k+v.join).chars
          end
        end
      end
      
      nested_array(hash)
      

      @output 将是您想要的数组。

      [
        ["a", "b", "c"],
        ["a", "d", "e", "f"],
        ["a", "d", "g", "h", "i", "j", "k"], 
        ["a", "l", "m", "n", "o", "p"], 
        ["q", "r", "s"]
      ]
      

      更新:键值对可以多于单个字符,因此nested_array 的以下方法可能会更好。

      def nested_array(h, op=[])
        h.map do |k,v|
          if Hash === v
            nested_array(v, Array.new(op) << k)
          else
            @output << ((Array.new(op) << k) + v)
          end
        end
      end
      

      【讨论】:

      • 好的开始,但不要通过字符串往返。真实数据不会有单字母键/值。
      • @SergioTulentsev 我同意,我只是根据用户案例在上面做了。我添加了为什么我通过字符串而不是数组往返。这满足用户场景,但肯定需要更好的方法。
      • 恐怕您对参考文献的评论没有任何意义。如果你想创建一个新数组,你可以。如果你想通过引用传递一个字符串,你也可以,同样简单。
      • @SergioTulentsev 是的,知道了,我更新了答案,但我喜欢你的答案:)
      【解决方案5】:

      这当然需要递归解决方案。以下方法不会改变原始哈希。

      代码

      def recurse(h)
        h.each_with_object([]) do |(k,v),arr|
          v.is_a?(Hash) ? recurse(v).each { |a| arr << [k,*a] } : arr << [k,*v]
        end 
      end
      

      示例

      h = { 'a'=>{ 'b'=>['c'],
                   'd'=>{ 'e'=>['f'], 'g' => ['h', 'i', 'j', 'k'] },
                   'l' => ['m', 'n', 'o', 'p'] },
            'q'=>{ 'r'=>['s'] } }
      

      recurse h
        #=> [["a", "b", "c"],
        #    ["a", "d", "e", "f"],
        #    ["a", "d", "g", "h", "i", "j", "k"],
        #    ["a", "l", "m", "n", "o", "p"],
        #    ["q", "r", "s"]]
      

      说明

      递归方法执行的操作总是难以解释。以我的经验,最好的方法是用 puts 语句给代码加盐。然而,这本身是不够的,因为在查看输出时,很难跟踪获得特定结果并传递给自身或返回到自身版本的递归级别。解决方案是缩进和取消缩进结果,这就是我在下面所做的。请注意我构建代码的方式和我使用的少数辅助方法是相当通用的,因此这种方法可以适用于检查其他递归方法执行的操作。

      INDENT = 8
      def indent; @col += INDENT; end
      def undent; @col -= INDENT; end
      def pu(s); print " "*@col; puts s; end
      def puhline; pu('-'*(70-@col)); end 
      @col = -INDENT
      

      def recurse(h)
        begin
          indent
          puhline
          pu "passed h = #{h}"
          h.each_with_object([]) do |(k,v),arr|
            pu "  k = #{k}, v=#{v}, arr=#{arr}"
            if v.is_a?(Hash)
              pu "  calling recurse(#{v})"
              ar = recurse(v)
              pu "  return value=#{ar}"
              pu "  calculating recurse(v).each { |a| arr << [k,*a] }"
              ar.each do |a|
                pu "    a=#{a}"
                pu "    [k, *a] = #{[k,*a]}"
                arr << [k,*a]
              end
            else
              pu "  arr << #{[k,*v]}"
              arr << [k,*v]
            end
            pu "arr = #{arr}"
          end.tap { |a| pu "returning=#{a}" }
        ensure
          puhline
          undent
        end
      end
      

      recurse h
      ----------------------------------------------------------------------
      passed h = {"a"=>{"b"=>["c"], "d"=>{"e"=>["f"], "g"=>["h", "i", "j", "k"]},
                  "l"=>["m", "n", "o", "p"]}, "q"=>{"r"=>["s"]}}
        k = a, v={"b"=>["c"], "d"=>{"e"=>["f"], "g"=>["h", "i", "j", "k"]},
                  "l"=>["m", "n", "o", "p"]}, arr=[]
        calling recurse({"b"=>["c"], "d"=>{"e"=>["f"], "g"=>["h", "i", "j", "k"]},
                         "l"=>["m", "n", "o", "p"]})
              --------------------------------------------------------------
              passed h = {"b"=>["c"], "d"=>{"e"=>["f"], "g"=>["h", "i", "j", "k"]},
                          "l"=>["m", "n", "o", "p"]}
                k = b, v=["c"], arr=[]
                arr << ["b", "c"]
              arr = [["b", "c"]]
                k = d, v={"e"=>["f"], "g"=>["h", "i", "j", "k"]}, arr=[["b", "c"]]
                calling recurse({"e"=>["f"], "g"=>["h", "i", "j", "k"]})
                      ------------------------------------------------------
                      passed h = {"e"=>["f"], "g"=>["h", "i", "j", "k"]}
                        k = e, v=["f"], arr=[]
                        arr << ["e", "f"]
                      arr = [["e", "f"]]
                        k = g, v=["h", "i", "j", "k"], arr=[["e", "f"]]
                        arr << ["g", "h", "i", "j", "k"]
                      arr = [["e", "f"], ["g", "h", "i", "j", "k"]]
                      returning=[["e", "f"], ["g", "h", "i", "j", "k"]]
                      ------------------------------------------------------
      

                return value=[["e", "f"], ["g", "h", "i", "j", "k"]]
                calculating recurse(v).each { |a| arr << [k,*a] }
                  a=["e", "f"]
                  [k, *a] = ["d", "e", "f"]
                  a=["g", "h", "i", "j", "k"]
                  [k, *a] = ["d", "g", "h", "i", "j", "k"]
              arr = [["b", "c"], ["d", "e", "f"], ["d", "g", "h", "i", "j", "k"]]
                k = l, v=["m", "n", "o", "p"],
                  arr=[["b", "c"], ["d", "e", "f"], ["d", "g", "h", "i", "j", "k"]]
                arr << ["l", "m", "n", "o", "p"]
              arr = [["b", "c"], ["d", "e", "f"], ["d", "g", "h", "i", "j", "k"],
                     ["l", "m", "n", "o", "p"]]
              returning=[["b", "c"], ["d", "e", "f"], ["d", "g", "h", "i", "j", "k"],
                         ["l", "m", "n", "o", "p"]]
              --------------------------------------------------------------
        return value=[["b", "c"], ["d", "e", "f"], ["d", "g", "h", "i", "j", "k"],
                      ["l", "m", "n", "o", "p"]]
        calculating recurse(v).each { |a| arr << [k,*a] }
          a=["b", "c"]
          [k, *a] = ["a", "b", "c"]
          a=["d", "e", "f"]
          [k, *a] = ["a", "d", "e", "f"]
          a=["d", "g", "h", "i", "j", "k"]
          [k, *a] = ["a", "d", "g", "h", "i", "j", "k"]
          a=["l", "m", "n", "o", "p"]
          [k, *a] = ["a", "l", "m", "n", "o", "p"]
      arr = [["a", "b", "c"], ["a", "d", "e", "f"], ["a", "d", "g", "h", "i", "j", "k"],
             ["a", "l", "m", "n", "o", "p"]]
        k = q, v={"r"=>["s"]}, arr=[["a", "b", "c"], ["a", "d", "e", "f"],
          ["a", "d", "g", "h", "i", "j", "k"], ["a", "l", "m", "n", "o", "p"]]
        calling recurse({"r"=>["s"]})
              --------------------------------------------------------------
      

              passed h = {"r"=>["s"]}
                k = r, v=["s"], arr=[]
                arr << ["r", "s"]
              arr = [["r", "s"]]
              returning=[["r", "s"]]
              --------------------------------------------------------------
        return value=[["r", "s"]]
      ----------------------------------------------------------------------
        calculating recurse(v).each { |a| arr << [k,*a] }
          a=["r", "s"]
          [k, *a] = ["q", "r", "s"]
      arr = [["a", "b", "c"], ["a", "d", "e", "f"], ["a", "d", "g", "h", "i", "j", "k"],
             ["a", "l", "m", "n", "o", "p"], ["q", "r", "s"]]
      returning=[["a", "b", "c"], ["a", "d", "e", "f"], ["a", "d", "g", "h", "i", "j", "k"],
                 ["a", "l", "m", "n", "o", "p"], ["q", "r", "s"]]
      ----------------------------------------------------------------------
        #=> [["a", "b", "c"], ["a", "d", "e", "f"], ["a", "d", "g", "h", "i", "j", "k"],
        #    ["a", "l", "m", "n", "o", "p"], ["q", "r", "s"]] 
      

      【讨论】:

        【解决方案6】:

        这里所有的解都是递归的,下面是非递归的 解决方案。

        def flatten(input)
          sol = []
          while(input.length > 0)
            unprocessed_input = []
            input.each do |l, r|
              if r.is_a?(Array)
                sol << l + r
              else
                r.each { |k, v| unprocessed_input << [l + [k], v] }
              end
            end
            input = unprocessed_input
          end
          return sol
        end
        flatten([[[], h]])
        

        代码说明:

        • 数组形式的哈希是[[k1, v1], [k2, v2]]。
        • 当 input_hash 以上述形式呈现时,可以生成 [[], { a: {..} }],这种形式的 partial_solutions,[ [a], {..} ]。索引“0”保存部分解决方案,索引“1”保存尚未处理的输入。
        • 由于这种格式很容易将 partial_solution 与未处理的输入进行映射并累积未处理的输入,因此将 input_hash 转换为这种格式会导致 [[[], input_hash]]

        解决方案:

        [["a", "b", "c"], ["a", "l", "m", "n", "o", "p"], ["q", "r", "s"], ["a", "d", "e", "f"], ["a", "d", "g", "h", "i", "j", "k"]]
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-08-21
          • 2020-10-01
          • 2022-06-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-02-02
          相关资源
          最近更新 更多