【问题标题】:How can I perform an inner join with two object arrays in JavaScript?如何在 JavaScript 中对两个对象数组执行内连接?
【发布时间】:2017-07-14 17:13:45
【问题描述】:

我有两个对象数组:

var a = [
  {id: 4, name: 'Greg'},
  {id: 1, name: 'David'},
  {id: 2, name: 'John'},
  {id: 3, name: 'Matt'},
]

var b = [
  {id: 5, name: 'Mathew', position: '1'},
  {id: 6, name: 'Gracia', position: '2'},
  {id: 2, name: 'John', position: '2'},
  {id: 3, name: 'Matt', position: '2'},
]

我想对ab 这两个数组进行内连接,并像这样创建第三个数组(如果 position 属性不存在,则它变为 null):

var result = [{
  {id: 4, name: 'Greg', position: null},
  {id: 1, name: 'David', position: null},
  {id: 5, name: 'Mathew', position: '1'},
  {id: 6, name: 'Gracia', position: '2'},
  {id: 2, name: 'John', position: '2'},
  {id: 3, name: 'Matt', position: '2'},
}]

我的方法:

function innerJoinAB(a,b) {
    a.forEach(function(obj, index) {
        // Search through objects in first loop
        b.forEach(function(obj2,i2){
        // Find objects in 2nd loop
        // if obj1 is present in obj2 then push to result.
        });
    });
}

但是时间复杂度是O(N^2)。我如何在O(N) 中做到这一点?我的朋友告诉我,我们可以使用减速器和Object.assign

我无法弄清楚这一点。请帮忙。

【问题讨论】:

  • 你有两个对象数组。似乎您需要将一个数组的所有值复制到一个新数组中,然后将第二个(以及后续)数组合并到其中。 Array.prototype.reduce 可能是一个好的开始。主键 id 是什么?由于您使用数组来保存对象,因此您可能还想创建一个 ID 到数组索引的索引,这样您就可以轻松找到 ID,而不必每次都遍历数组。
  • PS 内连接可能不是正确的术语,因为据我了解,这仅给出了两个集合都匹配的结果集(因此您的示例只会给出 ID 为 2 和 3 的行)。这更像是一个典型的合并。
  • @NicholasSmith 这是 JS,不是 JSON
  • 根据您的输出示例,您想要的是完全外连接,而不是内连接。

标签: javascript arrays inner-join


【解决方案1】:

解决方法之一。

const a = [
  {id: 4, name: 'Greg'},
  {id: 1, name: 'David'},
  {id: 2, name: 'John'},
  {id: 3, name: 'Matt'},
];

const b = [
  {id: 5, name: 'Mathew', position: '1'},
  {id: 6, name: 'Gracia', position: '2'},
  {id: 2, name: 'John', position: '2'},
  {id: 3, name: 'Matt', position: '2'},
];

const r = a.filter(({ id: idv }) => b.every(({ id: idc }) => idv !== idc));
const newArr = b.concat(r).map((v) => v.position ? v : { ...v, position: null });

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

【讨论】:

  • 请注意,这仍然是O(N^2) 的时间复杂度(技术上O(N*M) 其中NM 是您的两个数组的长度)
  • 另外,它使用name 作为主键,这可能不是预期的,因为名称可能是非唯一的
  • @FelixDombek 请提供更多信息。
  • filters 所在的行中,我认为您应该比较的是ID,而不是名称
  • @FelixDombek 同意,但在这种特殊情况下,它没有任何区别(:无论如何,我已经改变了它。
【解决方案2】:

我不知道 reduce 在这里有什么帮助,但您可以使用 Map 来 在O(n)完成同样的任务:

const a = [
  {id: 4, name: 'Greg'},
  {id: 1, name: 'David'},
  {id: 2, name: 'John'},
  {id: 3, name: 'Matt'}];

const b = [
  {id: 5, name: 'Mathew', position: '1'},
  {id: 6, name: 'Gracia', position: '2'},
  {id: 2, name: 'John', position: '2'},
  {id: 3, name: 'Matt', position: '2'}];

var m = new Map();
// Insert all entries keyed by ID into the Map, filling in placeholder
// 'position' since the Array 'a' lacks 'position' entirely:
a.forEach(function(x) { x.position = null; m.set(x.id, x); });

// For values in 'b', insert them if missing, otherwise, update existing values:
b.forEach(function(x) {
    var existing = m.get(x.id);
    if (existing === undefined)
        m.set(x.id, x);
    else
        Object.assign(existing, x);
});

// Extract resulting combined objects from the Map as an Array
var result = Array.from(m.values());

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

因为Map 的访问和更新是O(1)(平均而言 - 因为哈希 碰撞和重新散列,它可以更长),这使得O(n+m)(其中nm分别是ab的长度;天真的解决方案你 given 将是 O(n*m),对于 nm 使用相同的含义)。

【讨论】:

  • 一个问题:如果位置设置在数组a中,如果在b中没有重复,它将丢失。
  • @Gerrit0:是的,我注意到 cmets 中的假设(a 总是缺少position)。在处理a 时,您可以轻松调整它以使x.position 的集合成为条件,但是OP 提供的输入表明a 从来没有positionb 总是这样做。同样,这假设id 本身是唯一的(不需要name 作为密钥的一部分,假设name 将匹配id)。
  • 这在我看来像是左连接,而不是内连接。
  • @phil:同意。 OP 要求内连接,但他们想要的输出是左连接。我提供了一个可以产生他们想要的输出的答案,因为很明显他们使用了错误的术语。
【解决方案3】:

为了降低时间复杂度,不可避免地要使用更多的内存。

var a = [
  {id: 4, name: 'Greg'},
  {id: 1, name: 'David'},
  {id: 2, name: 'John'},
  {id: 3, name: 'Matt'},
]

var b = [
  {id: 5, name: 'Mathew', position: '1'},
  {id: 6, name: 'Gracia', position: '2'},
  {id: 2, name: 'John', position: '2'},
  {id: 3, name: 'Matt', position: '2'},
]     

var s = new Set();
var result = [];
b.forEach(function(e) {
    result.push(Object.assign({}, e));
    s.add(e.id);
});
a.forEach(function(e) {
    if (!s.has(e.id)) {
      var temp = Object.assign({}, e);
      temp.position = null;
      result.push(temp);
    }
});
console.log(result);

更新

正如@Blindman67 所述:“将搜索移至本机代码并不能降低问题的复杂性。”关于Set.prototype.has()Map.prototype.get()的内部过程,我已经咨询了ECMAScript® 2016 Language Specification,不幸的是,它们似乎都遍历了它们拥有的所有元素。

Set.prototype.has ( value )#

The following steps are taken:

    Let S be the this value.
    If Type(S) is not Object, throw a TypeError exception.
    If S does not have a [[SetData]] internal slot, throw a TypeError exception.
    Let entries be the List that is the value of S's [[SetData]] internal slot.
    Repeat for each e that is an element of entries,
        If e is not empty and SameValueZero(e, value) is true, return true.
    Return false. 

http://www.ecma-international.org/ecma-262/7.0/#sec-set.prototype.has

Map.prototype.get ( key )#

The following steps are taken:

    Let M be the this value.
    If Type(M) is not Object, throw a TypeError exception.
    If M does not have a [[MapData]] internal slot, throw a TypeError exception.
    Let entries be the List that is the value of M's [[MapData]] internal slot.
    Repeat for each Record {[[Key]], [[Value]]} p that is an element of entries,
        If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, return p.[[Value]].
    Return undefined. 

http://www.ecma-international.org/ecma-262/7.0/#sec-map.prototype.get

也许,我们可以使用Object,它可以通过名称直接访问其属性,例如哈希表或关联数组,例如:

var a = [
  {id: 4, name: 'Greg'},
  {id: 1, name: 'David'},
  {id: 2, name: 'John'},
  {id: 3, name: 'Matt'},
]

var b = [
  {id: 5, name: 'Mathew', position: '1'},
  {id: 6, name: 'Gracia', position: '2'},
  {id: 2, name: 'John', position: '2'},
  {id: 3, name: 'Matt', position: '2'},
]     

var s = {};
var result = [];
b.forEach(function(e) {
    result.push(Object.assign({}, e));
    s[e.id] = true;
});
a.forEach(function(e) {
    if (!s[e.id]) {
      var temp = Object.assign({}, e);
      temp.position = null;
      result.push(temp);
    }
});
console.log(result);

【讨论】:

  • 您误解了规范。 Set 描述的是文档中的基本逻辑,而不是实际的实施策略。 gethas 必须是次线性的,所以最坏的情况是它们是 O(log n) 并且建议的实现是哈希表 O(n)。早先阅读the overall Set docs:
  • "集合对象必须使用哈希表或其他机制来实现,这些机制平均提供的访问时间与集合中的元素数量呈次线性关系。本集合对象规范中使用的数据结构仅用于描述 Set 对象所需的可观察语义。它不是一个可行的实现模型。”
  • 一个问题@Y.C 你为什么用result.push(Object.assign({}, e));为什么我们不能直接推送 result.push(e);它会给出相同的结果吗?
  • @TechnoCorner Object.assign() 提供了一个浅克隆,它将所有可枚举的自身属性的值从一个或多个源对象复制到目标对象。因此,如果您更改了result 数组中的对象,它不会影响原始对象,因为它们是具有相同内容的不同对象。但是如果您不介意对原始对象进行更改,那么在这种情况下,显然result.push(e) 更有效。
  • @ShadowRanger 所以,你无法确定哪个浏览器使用了哪个实现或机制,但你知道JS中的对象可以用作哈希表或关联数组。
【解决方案4】:

您不会通过将搜索移至本机代码来降低问题的复杂性。搜索仍然必须完成。

此外,还需要将未定义的属性设为 null 是我不喜欢使用 null 的众多原因之一。

所以没有 null 的解决方案看起来像

var a = [
  {id: 4, name: 'Greg',position: '7'},
  {id: 1, name: 'David'},
  {id: 2, name: 'John'},
  {id: 3, name: 'Matt'},
]

var b = [
  {id: 5, name: 'Mathew', position: '1'},
  {id: 6, name: 'Gracia', position: '2'},
  {id: 2, name: 'John', position: '2'},
  {id: 3, name: 'Matt', position: '2'},
]


function join (indexName, ...arrays) {
    const map = new Map();
    arrays.forEach((array) => {
        array.forEach((item) => {
            map.set(
                item[indexName],
                Object.assign(item, map.get(item[indexName]))
            );
        })
    })
    return [...map.values()];
}

并被调用

const joinedArray = join("id", a, b);

使用默认值加入稍微复杂一些,但应该很方便,因为它可以加入任意数量的数组并自动将缺失的属性设置为提供的默认值。

在加入后测试默认值以节省一点时间。

function join (indexName, defaults, ...arrays) {
    const map = new Map();
    arrays.forEach((array) => {
        array.forEach((item) => {
            map.set(
                item[indexName], 
                Object.assign( 
                    item, 
                    map.get(item[indexName])
                )
            );
        })
    })
    return [...map.values()].map(item => Object.assign({}, defaults, item));

}

使用

const joinedArray = join("id", {position : null}, a, b);

你可以添加...

    arrays.shift().forEach((item) => {  // first array is a special case.
        map.set(item[indexName], item);
    });

...在函数的开头节省一点时间,但我觉得没有额外的代码更优雅。

【讨论】:

    【解决方案5】:

    这是一个更通用的连接版本的尝试,它接受 N 个对象并基于主 id 键合并它们。

    如果性能至关重要,您最好使用像 ShadowRanger 提供的特定版本,它不需要动态构建所有属性键的列表。

    此实现假定所有缺少的属性都应设置为 null,并且每个输入数组中的每个对象都具有相同的属性(尽管数组之间的属性可能不同)

    var a = [
        {id: 4, name: 'Greg'},
        {id: 1, name: 'David'},
        {id: 2, name: 'John'},
        {id: 3, name: 'Matt'},
    ];
    var b = [
        {id: 5, name: 'Mathew', position: '1'},
        {id: 600, name: 'Gracia', position: '2'},
        {id: 2, name: 'John', position: '2'},
        {id: 3, name: 'Matt', position: '2'},
    ];
    
    console.log(genericJoin(a, b));
    
    function genericJoin(...input) {
        //Get all possible keys
        let template = new Set();
        input.forEach(arr => {
            if (arr.length) {
                Object.keys(arr[0]).forEach(key => {
                    template.add(key);
                });
            }
        });
    
        // Merge arrays
        input = input.reduce((a, b) => a.concat(b));
    
        // Merge items with duplicate ids
        let result = new Map();
        input.forEach(item => {
            result.set(item.id, Object.assign((result.get(item.id) || {}), item));
        });
    
        // Convert the map back to an array of objects
        // and set any missing properties to null
        return Array.from(result.values(), item => {
            template.forEach(key => {
                item[key] = item[key] || null;
            });
            return item;
        });
    }

    【讨论】:

      【解决方案6】:

      如果您放弃 null 标准(社区中的许多人说使用 null 不好),那么有一个非常简单的解决方案

      let a = [1, 2, 3];
      let b = [2, 3, 4];
      
      a.filter(x => b.includes(x)) 
      
      // [2, 3]
      

      【讨论】:

      • 1和4呢?
      • 喜欢完全外连接?应该是[].concat( a.filter(x => !b.includes(x)), b.filter(x => !a.includes(x)) )
      【解决方案7】:

      这是一个通用的 O(n*m) 解决方案,其中 n 是记录数,m 是键数。这仅适用于有效的对象键。您可以将任何值转换为 base64 并在需要时使用它。

      const join = ( keys, ...lists ) =>
          lists.reduce(
              ( res, list ) => {
                  list.forEach( ( record ) => {
                      let hasNode = keys.reduce(
                          ( idx, key ) => idx && idx[ record[ key ] ],
                          res[ 0 ].tree
                      )
                      if( hasNode ) {
                          const i = hasNode.i
                          Object.assign( res[ i ].value, record )
                          res[ i ].found++
                      } else {
                          let node = keys.reduce( ( idx, key ) => {
                              if( idx[ record[ key ] ] )
                                  return idx[ record[ key ] ]
                              else
                                  idx[ record[ key ] ] = {}
                              return idx[ record[ key ] ]
                          }, res[ 0 ].tree )
                          node.i = res[ 0 ].i++
                          res[ node.i ] = {
                              found: 1,
                              value: record
                          }
                      }
                  } )
                  return res
              },
              [ { i: 1, tree: {} } ]
               )
               .slice( 1 )
               .filter( node => node.found === lists.length )
               .map( n => n.value )
      
      join( [ 'id', 'name' ], a, b )
      

      这与 Blindman67 的答案基本相同,只是它添加了一个索引对象来标识要加入的记录。记录存储在一个数组中,索引存储给定键集的记录位置以及找到它的列表数。

      每次遇到相同的键集时,都会在树中找到该节点,更新其索引处的元素,并增加找到的次数。

      最后,idx 对象从带有切片的数组中删除,所有未在每个集合中找到的元素都将被删除。这使它成为一个内连接,您可以删除这个过滤器并拥有一个完整的外连接。

      最后每个元素都映射到它的值,你就有了合并后的数组。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-09-24
        • 1970-01-01
        • 1970-01-01
        • 2017-07-27
        • 2022-11-16
        • 1970-01-01
        • 1970-01-01
        • 2022-01-12
        相关资源
        最近更新 更多