【问题标题】:Efficient mapping of arrays高效的数组映射
【发布时间】:2019-09-21 19:02:54
【问题描述】:

我正在尝试找出在 JS 中同时比较/合并/操作两个数组(列表)的最佳/最有效或最实用的方法。

我在下面给出的示例是整体概念的一个简单示例。在我当前的项目中,我处理了一些非常疯狂的列表映射、过滤等处理非常大的对象列表。

如下所述,我在比较列表时的第一个想法 (version1) 是遍历第一个列表(即 map),然后在匿名/回调函数中,过滤第二个列表以满足比较(例如匹配 id)。根据下面的version1,这显然有效。

我有一个性能方面的问题,因为在每次迭代/调用 map 时,通过这种方法,整个第二个列表都会被过滤,只是为了找到一个与过滤器匹配的项目。

此外,过滤器会通过 list2 中应在 list1 中匹配的所有其他项目。意思(因为那句话可能没有意义):

list1.map   list2.filter

id:1        [id:3,id:2,id:1]
                          ^-match
id:2        [id:3,id:2,id:1]
                     ^-match
id:3        [id:3,id:2,id:1]
                ^-match

理想情况下,在 map (list1 id:1) 的第一次迭代中,当过滤器遇到 list2 id:3(第一项)时,它只会匹配到 list1 id:3

用上面的概念思考(前面遇到的时候匹配后面的id,我想出了version2)。

这使得list2变成了一个字典,然后通过key查找任意序列中的值。

const list1 = [
  {id: '1',init:'init1'},
  {id: '2',init:'init2'},
  {id: '3',init:'init3'}
];
const list2 = [
  {id: '2',data:'data2'},
  {id: '3',data:'data3'},
  {id: '4',data:'data4'}
];

/* ---------
* version 1
*/

const mergedV1 = list1.map(n => (
  {...n,...list2.filter(f => f.id===n.id)[0]}
));
/* [ 
  {"id": "1", "init": "init1"}, 
  {"id": "2", "init": "init2", "data": "data2"}, 
  {"id": "3", "init": "init3", "data": "data3"} 
] */

/* ---------
* version 2
*/

const dictList2 = list2.reduce((dict,item) => (dict[item.id]=item,dict),{}); 
// does not handle duplicate ids but I think that's 
// outside the context of this question.

const mergedV2 = list1.map(n => ({...n,...dictList2[n.id]}));
/* [ 
  {"id": "1", "init": "init1"}, 
  {"id": "2", "init": "init2", "data": "data2"}, 
  {"id": "3", "init": "init3", "data": "data3"} 
] */

JSON.stringify(mergedV1) === JSON.stringify(mergedV2);
// true

// and just for fun
const sqlLeftOuterJoinInJS = list1 => list2 => on => {
  const dict = list2.reduce((dict,item) => ( 
    dict[item[on]]=item,dict
  ),{});
  return list1.map(n => ({...n,...dict[n[on]]}
))};

显然,上面的示例非常简单(合并两个列表,每个列表的长度为 3)。我正在处理更复杂的实例。

我不知道是否有一些我应该使用的更智能(并且功能理想)的技术。

【问题讨论】:

  • 是否有任何数组应该包含一组唯一的 id 对象?意思是id 在任一数组中的每个值只出现一次?

标签: javascript arrays dictionary functional-programming


【解决方案1】:

您可以关闭该组的所需密钥,并使用 Map 收集所有对象。

function merge(key) {
    var map = new Map;
    return function (r, a) {
        a.forEach(o => {
            if (!map.has(o[key])) r.push(map.set(o[key], {}).get(o[key]));
            Object.assign(map.get(o[key]), o);
        });
        return r;
    };
}

const
    list1 = [{ id: '1', init: 'init1' }, { id: '2', init: 'init2' }, { id: '3', init: 'init3' }],
    list2 = [{ id: '2', data: 'data2' }, { id: '3', data: 'data3' }, { id: '4', data: 'data4' }],
    result = [list1, list2].reduce(merge('id'), []);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

【讨论】:

  • 真漂亮
  • 看起来很漂亮。让我更多地关注map。我的一个问题是Object.assign(map.get(o[key]), o); 在没有左手赋值的情况下会做什么?是不是在变异o
  • 它改变第一个参数(map.get(o[key]) 的结果)并将第二个o 分配给它。
【解决方案2】:

使用filter 进行搜索是错误的。您在第 2 版中的直觉要好得多。 MapSet 提供更快的查找时间。

这是一种分解的方法。它应该很快,但可能不如 Nina 的快。她是速度恶魔>_

const merge = (...lists) =>
  Array .from
    ( lists
        .reduce (merge1, new Map)
        .values ()
    )

const merge1 = (cache, list) =>
  list .reduce
    ( (cache, l) =>
        cache .has (l.id)
          ? update (cache, l.id, l)
          : insert (cache, l.id, l)
    , cache
    )

const insert = (cache, key, value) =>
  cache .set (key, value)

const update = (cache, key, value) =>
  cache .set
    ( key
    , { ...cache .get (key)
      , ...value
      }
    )

const list1 =
  [{ id: '1', init: 'init1' }, { id: '2', init: 'init2' }, { id: '3', init: 'init3' }]

const list2 =
  [{ id: '2', data: 'data2' }, { id: '3', data: 'data3' }, { id: '4', data: 'data4' }]

console .log (merge (list1, list2))

【讨论】:

    【解决方案3】:

    我提供这个是为了完整性,因为我认为 Nina 和 @user633183 提供了最有可能更有效的解决方案。

    如果您希望坚持最初的 filter 示例,即最大查找 N*M,并且您的数组是可变的;您可以考虑在遍历时减少集合。在过去,缩小阵列会对性能产生巨大影响。

    今天的一般模式是使用其他答案中指出的 Map(或 dict),因为它既易于理解又通常有效。

    查找和调整大小

    const list1 = [
      {id: '1',init:'init1'},
      {id: '2',init:'init2'},
      {id: '3',init:'init3'}
    ];
    const list2 = [
      {id: '2',data:'data2'},
      {id: '3',data:'data3'},
      {id: '4',data:'data4'}
    ];
    
    // combine by ID
    let merged = list1.reduce((acc, obj)=>{
      acc.push(obj);
    
      // find index by ID
      let foundIdx = list2.findIndex( el => el.id==obj.id );
      // if found, store and remove from search
      if ( foundIdx >= 0 ){
        obj.data = list2[foundIdx].data;
        list2.splice( foundIdx, 1 );        // shrink lookup array
      }
      return acc;
    },[]);
    
    // store remaining (if you want); i.e. {id:4,data:'data4'}
    merged = merged.concat(list2)
    
    console.log(merged);
    .as-console-wrapper {
      max-height: 100% !important;
      top: 0;
    }

    【讨论】:

      【解决方案4】:

      我不确定是否应该将此问题标记为duplicate,因为您的措辞不同。无论如何,这是我的answer 逐字复制的那个问题。你想要的是equijoin

      const equijoin = (xs, ys, primary, foreign, sel) => {
          const ix = xs.reduce((ix, row) => // loop through m items
              ix.set(row[primary], row),    // populate index for primary table
          new Map);                         // create an index for primary table
      
          return ys.map(row =>              // loop through n items
              sel(ix.get(row[foreign]),     // get corresponding row from primary
              row));                        // select only the columns you need
      };
      

      您可以按如下方式使用它:

      const equijoin = (xs, ys, primary, foreign, sel) => {
          const ix = xs.reduce((ix, row) => ix.set(row[primary], row), new Map);
          return ys.map(row => sel(ix.get(row[foreign]), row));
      };
      
      const list1 = [
          { id: "1", init: "init1" },
          { id: "2", init: "init2" },
          { id: "3", init: "init3" }
      ];
      
      const list2 = [
          { id: "2", data: "data2" },
          { id: "3", data: "data3" },
          { id: "4", data: "data4" }
      ];
      
      const result = equijoin(list2, list1, "id", "id",
          (row2, row1) => ({ ...row1, ...row2 }));
      
      console.log(result);

      使用equijoin 计算答案需要O(m + n) 时间。但是,如果您已经有一个索引,那么它只需要O(n) 时间。因此,如果您计划使用相同的表执行多个等值连接,那么抽象出索引可能是值得的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-01-25
        • 2013-04-29
        • 1970-01-01
        • 2011-02-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多