【问题标题】:Concat two list with maps more efficient in elixir在长生不老药中使用更有效的地图连接两个列表
【发布时间】:2018-04-27 22:46:48
【问题描述】:

我有 2 个地图列表:

list1 = 
[
 %{amount: 1, id: 112006},
 %{amount: 1, id: 78798},
 %{amount: 6, id: 92572},
 %{amount: 1, id: 89750},
 %{amount: 1, id: 81418},
 %{amount: 3, id: 92062},
 %{amount: 1, id: 82373},
 %{amount: 1, id: 92856}...
]

list2 =
[
 %{count: 5, id: [112006, 92062, 92856, 67812, 70736], name: "Object 1"},
 %{count: 655, id: [92572, 22432, 32368, 34180, 34181, 34182, ...],    name: "Object 2"},
 %{count: 238, id: [26052, 30430, 37067, 37068, 41228, 42686, ...], name: "Object 3"}
 ...
]

list1 包含 30000 多个地图,list2 包含大约 100 个地图,两个列表中的 id 相同,我想将两个列表合并为一个:

[
 %{count: 5, all_count: 5 name: "Object 1"},
 %{count: 655, all_count: 3, name: "Object 2"},
 ....
]

使用新的 all_count-key,它是来自 list1 的所有金额的总和,具有与 list2 中的 id-array 中相同的 id。

我做到了:

Enum.map(list2, fn(map) ->
    all_count =
      list1
      |> Enum.filter(&Enum.member?(map.id, &1.id))
      |> Enum.map(&(&1.amount))
      |> Enum.sum
     Map.put(map, :all_count, all_count)
   end)

witch 可以工作,但速度很慢,我需要更快的东西,尝试使用 Flow:

Enum.map(list2, fn(map) ->
    all_count =
      list1
      |> Flow.from_enumerable()
      |> Flow.filter(&Enum.member?(map.id, &1.id))
      |> Flow.map(&(&1.amount))
      |> Enum.sum
     Map.put(map, :all_count, all_count)
   end)

得到它有点快但不多,任何提示如何更快地得到它?蒂亚。

【问题讨论】:

    标签: elixir


    【解决方案1】:

    您的问题的主要关键是 Filter 是一个 O(n) 操作,因此在每次迭代中,您都在循环遍历列表的所有 30k 元素。

    这会使您的整个操作O(n^2) 变得复杂,因此不可扩展。

    通过将第一个列表转换为hash_table,可以将您的问题降低到O(n) 的复杂性,如下所示:

    list1 = [
     %{amount: 1, id: 112006},
     %{amount: 1, id: 78798},
     %{amount: 6, id: 92572},
     %{amount: 1, id: 89750},
     %{amount: 1, id: 81418},
     %{amount: 3, id: 92062},
     %{amount: 1, id: 82373},
     %{amount: 1, id: 92856}
    ]
    
    hash_table = Enum.reduce(list1, %{}, fn e, a -> Map.merge(a, %{e.id => e}) end)
    

    根据 cmets 的建议,Map.merge 的更好替代方案是:

    hash_table = Enum.reduce(list1, %{}, &Map.put(&2, &1.id, &1))
    

    所以你会得到以下内容:

    %{
      78798 => %{amount: 1, id: 78798},
      81418 => %{amount: 1, id: 81418},
      82373 => %{amount: 1, id: 82373},
      89750 => %{amount: 1, id: 89750},
      92062 => %{amount: 3, id: 92062},
      92572 => %{amount: 6, id: 92572},
      92856 => %{amount: 1, id: 92856},
      112006 => %{amount: 1, id: 112006}
    }
    

    现在不用循环遍历每个元素,您可以简单地查找所需的元素 具有 O(1) 访问权限 具有 O(log n) 访问权限,例如list1[82373] 会给你%{amount: 1, id: 82373},你可以从中得到你的金额。如果您预计除了数量之外,这些数据点中的任何一个都不需要更多键,您可以通过将id 直接指向数量值来进一步简化操作。

    一旦你有了概念证明,你可以修改你的程序以完全采用 hash_map 数据结构,这样你就可以避免不断地将list1 转换为hash_map 结构。也许您也可以考虑将其全部放在 ETS 表中,这可以为您提供O(1) 查找访问权限,as stated in the docs

    这些提供了将大量数据存储在一个 Erlang 运行时系统,并具有对数据的恒定访问时间。

    【讨论】:

    • 旁注: hash_table = Enum.reduce(list1, %{}, &Map.put(&2, &1.id, &1)) 应该比 Map.merge 快。
    • 在地图中查找和插入是 Elixir 中的 O(log n),而不是 O(1)
    • @Dogbert 一如既往,非常感谢。我能找到的最佳参考是:erlang.org/pipermail/erlang-questions/2015-September/… 但我想知道为什么 Map 不提供 O(1) 访问。似乎获得 O(1) 的唯一方法是 ETS,或者将映射转换为模块,其中函数作为键,值作为这些函数的返回值。很想就这个话题提出一个问题,因为似乎没有关于这个话题的彻底解决。
    • Micmus 很乐意澄清闲散的频道:en.wikipedia.org/wiki/Hash_array_mapped_trie
    【解决方案2】:

    您可以尝试,而不是过滤 list1 中每个映射函数中的 ID,而是将其转换为映射,其中键是 id,值是 amount 部分:

    map1 = list1 |> List.foldl(%{}, fn(m, acc) -> Map.put(acc, m.id, m.amount) end)
    
    # Result
    %{
      78798 => 1,
      81418 => 1,
      82373 => 1,
      89750 => 1,
      92062 => 3,
      92572 => 6,
      92856 => 1,
      112006 => 1
      ...
    }
    

    然后您可以稍微调整您的代码。或者您也可以尝试使用List.foldl/3 来构建您的结果列表:

    List.foldl(list2, [], fn(map, acc) ->
      map_count = Map.take(map, [:count, :name])
      count = map.id |> List.foldl(0, &(map1[&1] + &2))
      acc ++ [Map.put(map_count, :all_count, count)]
    end)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-12-31
      • 1970-01-01
      • 2016-02-01
      相关资源
      最近更新 更多