【问题标题】:How do I tranpose a list in Elixir?如何在 Elixir 中转置列表?
【发布时间】:2019-08-26 12:36:08
【问题描述】:

我希望有效地转置 Elixir 中的(大)列表列表,其中每个子列表具有相同数量的标量元素。所以从这里:

iex(1)> x = [[1, 2], [3, 4], [5, 6]]
[[1, 2], [3, 4], [5, 6]]

我想解决这个问题:

[[1, 3, 5], [2, 4, 6]]

在 Python 中,我会使用“splat”运算符 *zip

In [1]: x = [[1, 2], [3, 4], [5, 6]]
In [2]: x
Out[2]: [[1, 2], [3, 4], [5, 6]]
In [3]: zip(*x)
Out[3]: [(1, 3, 5), (2, 4, 6)]

这完全等同于转置矩阵。

Elixir 中的等价物是什么?我需要对来自 CSV 的大量列表执行此操作。

奖励:地图类似,例如:

iex(1)> %{"a1" => %{"d1" => 1, "d2" => 2}, "a2" => %{"d1" => 3, "d2" => 4}, "a3" => %{"d1" => 5, "d2" => 6}}
%{
  "a1" => %{"d1" => 1, "d2" => 2},
  "a2" => %{"d1" => 3, "d2" => 4},
  "a3" => %{"d1" => 5, "d2" => 6}
}

变成

%{
  "d1" => %{"a1" => 1, "a2" => 3, "a3" => 5},
  "d2" => %{"a1" => 2, "a2" => 4, "a3" => 6}
}

【问题讨论】:

    标签: erlang elixir


    【解决方案1】:

    对于您拥有Enum.zip/2Stream.zip/1 的列表

    iex> Enum.zip([[1, 2], [3, 4], [5, 6]])
    [{1, 3, 5}, {2, 4, 6}]
    

    重新排列地图有点棘手。我确信有一种更聪明、更好的方法,但你可以把它分解成一个平面可枚举,然后再把它重新组合成一张地图。在这里,我会考虑使用理解。

    iex> input = %{
    ...>   "a1" => %{"d1" => 1, "d2" => 2},
    ...>   "a2" => %{"d1" => 3, "d2" => 4},
    ...>   "a3" => %{"d1" => 5, "d2" => 6}
    ...> }
    iex> for {outer, map} <- input, {inner, value} <- map, reduce: %{} do
    ...>   acc -> Map.update(acc, inner, %{outer => value}, &Map.put(&1, outer, value))
    ...> end
    %{
      "d1" => %{"a1" => 1, "a2" => 3, "a3" => 5}, 
      "d2" => %{"a1" => 2, "a2" => 4, "a3" => 6}
    }
    

    【讨论】:

      【解决方案2】:

      对于列表:

      正如 BrettBeatty 所说,您可以按如下方式使用 zip/1:

      iex(1)> x = [[1, 2], [3, 4], [5, 6]]
      iex(2)> x |> Enum.zip |>  Enum.map(&(Tuple.to_list(&1)))
      

      还有一种递归方式可以做到这一点:

      defmodule E do
        def transpose([]), do: []
        def transpose([[]|_]), do: []
        def transpose(list) do
          [Enum.map(list, &hd/1) | transpose(Enum.map(list, &tl/1))]
        end
      end
      
      iex(3)> x |> E.transpose()
      

      【讨论】:

        【解决方案3】:

        Brett 的回答是绝对正确的;我只是想分享另一种使用Kernel.put_in/3Access.key/2 转换地图的方法。这种方法很容易可扩展以转置任何深度的地图。

        Enum.reduce(map, %{}, fn {k, vs}, acc ->
          Enum.reduce(vs, acc, fn {ik, iv}, acc ->
            put_in(acc, [Access.key(ik, %{}), k], iv)
          end)
        end)
        
        #⇒ %{
        #    "d1" => %{"a1" => 1, "a2" => 3, "a3" => 5},
        #    "d2" => %{"a1" => 2, "a2" => 4, "a3" => 6}
        #  }
        

        Access.key/2 接受默认值,允许您根据需要将元素埋得尽可能深。

        put_in(%{}, [
          Access.key(:k1, %{}),
          Access.key(:k2, %{}),
          Access.key(:k3, %{})
        ], 42)
        #⇒ %{k1: %{k2: %{k3: 42}}}
        

        基于Access的数组转置解决方案也是可能的,但需要预先知道结果列表的长度。

        len = list |> Enum.map(&Enum.count/1) |> Enum.max()
        Enum.map(0..len-1, fn idx ->
          Enum.map(list, &get_in(&1, [Access.at(idx)]))
        end)
        #⇒ [[1, 3, 5], [2, 4, 6]]
        

        Enum.zip/1 不同,它将保留所有元素,将nils 放在输入小于最大长度的位置。

        【讨论】:

        • 非常适合参差不齐的数组。
        • 我个人更喜欢 Access 而不是所有原生的 Map/List/Enum 方法,因为它可以轻松概括所有内容。
        • 如果您添加 Enum.zip 方法来完成列表问题,我将接受您出色的一般答案作为主要答案。
        • 我不喜欢这里的Enum.zip/1,因为它会生成元组,然后将其转换为列表,这 a) 很慢,b) 当长度大于 255 时就会失败(元组最大大小为255.) 我可能会使用Enum.reduce/3 发布答案以获得列表,但我喜欢上面的方法。
        • 欧普斯;上面的评论是错误的,元组可能是any reasonable size。无论如何,我发现先转换为元组,然后再转换为元组是多余且不优雅的。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-01-18
        • 2022-12-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多