web-dev-qa-db-ja.com

JavaScriptで2つのオブジェクト配列を使用して内部結合を実行するにはどうすればよいですか?

2つのオブジェクト配列があります。

_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'},
]
_

これらの2つの配列abに対して内部結合を実行し、次のような3番目の配列を作成します(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_を使用できると言っていました。

私はこれを理解することができません。助けてください。

7
TechnoCorner

それを解決する方法の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(newArr);
5
kind user

ここでreduceがどのように役立つかはわかりませんが、Mapを使用してO(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の長さです。あなたが与えた素朴な解決策は、nと同じ意味を使用するO(n*m)になります。およびm)。

7
ShadowRanger

時間の複雑さを軽減するために、より多くのメモリを使用することは避けられません。

_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);_
1
Joe Yichong

検索をネイティブコードに移動しても、問題の複雑さは軽減されません。検索は引き続き実行する必要があります。

また、未定義のプロパティをnullにする必要があるという追加は、nullの使用を嫌う多くの理由の1つです。

したがって、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);
    });

...少し時間を節約するために関数の開始時に、しかし私はそれが余分なコードなしでよりエレガントであると感じます。

0
Blindman67

null基準を削除すると(コミュニティの多くはnullを使用するのは悪いと言っています)、非常に簡単な解決策があります

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

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

// [2, 3]
0
Stephen

これは、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;
    });
}
0
Gerrit0

これが一般的な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オブジェクトがスライスとともに配列から削除され、各セットで見つからなかった要素がすべて削除されます。これにより、内部結合になります。このフィルターを削除して、完全な外部結合を作成できます。

最後に、各要素がその値にマップされ、配列がマージされます。

0