如何在JavaScript中使用两个对象数组执行内部联接?

时间:2017-02-24 01:05:27

标签: javascript arrays inner-join

我有两个对象数组:

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)中完成?我的朋友告诉我,我们可以使用reducer和Object.assign

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

7 个答案:

答案 0 :(得分:7)

我不知道reduce在这方面有何帮助,但您可以使用MapO(n)中完成相同的任务:

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

// For b values, 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());

由于Map访问和更新为O(1)(平均情况,由于哈希冲突和重新散列,可能会更长),这会使O(n+m)(其中nm分别是ab的长度;您提供的天真解决方案O(n*m)使用与nm相同的含义)。

答案 1 :(得分:4)

如何解决它的方法之一。



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(newArr);




答案 2 :(得分:1)

为了减少时间复杂度,使用更多内存是不可避免的。



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所说:"您不会通过将搜索移动到本机代码来减少问题的复杂性。"我已经就ECMAScript® 2016 Language SpecificationSet.prototype.has()的内部程序咨询Map.prototype.get(),不幸的是,他们似乎都在遍历他们拥有的所有元素。

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);




答案 3 :(得分:0)

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

此外,还需要对未定义的属性进行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);
    });

...在函数开始时节省了一点时间,但是如果没有额外的代码,我觉得它更优雅。

答案 4 :(得分:0)

这是尝试更通用的连接版本,它接受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;
    });
}

答案 5 :(得分:0)

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

let a = [1, 2, 3];
let b = [2, 3, 4];

a.filter(x => b.includes(x)) 

// [2, 3]

答案 6 :(得分:0)

这是一个通用的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对象与切片一起从数组中删除,在每个集合中未找到的所有元素都将删除。这使它成为内部联接,您可以删除此过滤器并具有完整的外部联接。

最后,每个元素都映射到其值,并且您有了合并的数组。