简体   繁体   中英

How to calculate intersection of multiple arrays in JavaScript? And what does [equals: function] mean?

I am aware of this question, simplest code for array intersection but all the solutions presume the number of arrays is two, which cannot be certain in my case.

I have divs on a page with data that contains arrays. I want to find the values common to all arrays. I do not know how many divs/arrays I will have in advance. What is the best way to calculate values common to all arrays?

var array1 = ["Lorem", "ipsum", "dolor"];
var array2 = ["Lorem", "ipsum", "quick", "brown", "foo"];
var array3 = ["Jumps", "Over", "Lazy", "Lorem"];
var array4 = [1337, 420, 666, "Lorem"];
//Result should be ["Lorem"];

I found another solution elsewhere, using Underscore.js.

var arrayOfArrays = [[4234, 2323, 43], [1323, 43, 1313], [23, 34, 43]];
_.intersection.apply(_, arrayOfArrays)
//Result is [43]

I've tested this with simple dummy data at my end and it seems to work. But for some reason, some of the arrays I'm producing, which contain simple strings, also automatically include an added value, "equals: function":

["Dummy1", "Dummy2", "Dummy3", equals: function]

And whenever I use the Underscore.js intersection method, on an array of arrays, I always get [equals: function] in dev tools, and not - if "Dummy3" is common to all arrays - ["Dummy3"].

So TL;DR is there another solution to array intersection that would suit my case? And can anyone explain what [equals: function] means here? When I expand the item in the dev tools, it produces an empty array and a list of methods available on arrays (pop, push, shift etc), but these methods are all faded out, while equals: function is highlighted.

You could just use Array#reduce with Array#filter and Array#includes .

 var array1 = ["Lorem", "ipsum", "dolor"], array2 = ["Lorem", "ipsum", "quick", "brown", "foo"], array3 = ["Jumps", "Over", "Lazy", "Lorem"], array4 = [1337, 420, 666, "Lorem"], data = [array1, array2, array3, array4], result = data.reduce((a, b) => a.filter(c => b.includes(c))); console.log(result);

I wrote a helper function for this:

function intersection() {
  var result = [];
  var lists;

  if(arguments.length === 1) {
    lists = arguments[0];
  } else {
    lists = arguments;
  }

  for(var i = 0; i < lists.length; i++) {
    var currentList = lists[i];
    for(var y = 0; y < currentList.length; y++) {
        var currentValue = currentList[y];
      if(result.indexOf(currentValue) === -1) {
        var existsInAll = true;
        for(var x = 0; x < lists.length; x++) {
          if(lists[x].indexOf(currentValue) === -1) {
            existsInAll = false;
            break;
          }
        }
        if(existsInAll) {
          result.push(currentValue);
        }
      }
    }
  }
  return result;
}

Use it like this:

intersection(array1, array2, array3, array4); //["Lorem"]

Or like this:

intersection([array1, array2, array3, array4]); //["Lorem"]

Full code here

UPDATE 1

A slightly smaller implementation here using filter

This can be done pretty succinctly if you fancy employing some recursion and the new ES2015 syntax:

 const array1 = ["Lorem", "ipsum", "dolor"]; const array2 = ["Lorem", "ipsum", "quick", "brown", "foo"]; const array3 = ["Jumps", "Over", "Lazy", "Lorem"]; const array4 = [1337, 420, 666, "Lorem"]; const arrayOfArrays = [[4234, 2323, 43], [1323, 43, 1313], [23, 34, 43]]; // Filter xs where, for a given x, there exists some y in ys where y === x. const intersect2 = (xs,ys) => xs.filter(x => ys.some(y => y === x)); // When there is only one array left, return it (the termination condition // of the recursion). Otherwise first find the intersection of the first // two arrays (intersect2), then repeat the whole process for that result // combined with the remaining arrays (intersect). Thus the number of arrays // passed as arguments to intersect is reduced by one each time, until // there is only one array remaining. const intersect = (xs,ys,...rest) => ys === undefined ? xs : intersect(intersect2(xs,ys),...rest); console.log(intersect(array1, array2, array3, array4)); console.log(intersect(...arrayOfArrays)); // Alternatively, in old money, var intersect2ES5 = function (xs, ys) { return xs.filter(function (x) { return ys.some(function (y) { return y === x; }); }); }; // Changed slightly from above, to take a single array of arrays, // which matches the underscore.js approach in the Q., and is better anyhow. var intersectES5 = function (zss) { var xs = zss[0]; var ys = zss[1]; var rest = zss.slice(2); if (ys === undefined) { return xs; } return intersectES5([intersect2ES5(xs, ys)].concat(rest)); }; console.log(intersectES5([array1, array2, array3, array4])); console.log(intersectES5(arrayOfArrays));

Using a combination of ideas from several contributors and the latest ES6 goodness, I arrived at

 const array1 = ["Lorem", "ipsum", "dolor"]; const array2 = ["Lorem", "ipsum", "quick", "brown", "foo"]; const array3 = ["Jumps", "Over", "Lazy", "Lorem"]; const array4 = [1337, 420, 666, "Lorem"]; Array.prototype.intersect = function intersect(a, ...b) { const c = function (a, b) { b = new Set(b); return a.filter((a) => b.has(a)); }; return undefined === a ? this : intersect.call(c(this, a), ...b); }; console.log(array1.intersect(array2, array3, array4)); // ["Lorem"]

For anyone confused by this in the future,

_.intersection.apply(_, arrayOfArrays)

Is in fact the most elegant way to do this. But:

var arrayOfArrays = [[43, 34343, 23232], [43, 314159, 343], [43, 243]];
arrayOfArrays = _.intersection.apply(_, arrayOfArrays);

Will not work! Must do

var differentVariableName = _.intersection.apply(_,arrayOfArrays);

Small recursive divide and conquer solution that does not rely on es6 or any library.

It accepts an array of arrays which makes the code shorter and allows you to pass arguments by using map.

 function intersection(a) { if (a.length > 2) return intersection([intersection(a.slice(0, a.length / 2)), intersection(a.slice(a.length / 2))]); if (a.length == 1) return a[0]; return a[0].filter(function(item) { return a[1].indexOf(item) !== -1; }); } var list1 = [ 'a', 'b', 'c' ]; var list2 = [ 'd', 'b', 'e' ]; var list3 = [ 'f', 'b', 'e' ]; console.log(intersection([list1, list2, list3]));

If you can use ES6 Maps and your arrays items are scalar values (easily usable as Map keys), then you can try this (works in my case) :

const intersect_lists = (lists) => {
    const results = []
    const lookup = new Map()

    lists.map((list, idx) => {
        list.map((element) => {
            const count = lookup.get(element) || 0

            if(count === idx) {
                lookup.set(element, 1 + count)
            } else {
                lookup.delete(element)
            }
        })
    })

    // only elements present in all lists will have 
    // their respective counter equllling the total number of lists
    Array.from(lookup.keys()).map((key) => {
        if(lookup.get(key) === lists.length) {
            results.push(key)
        }
    })

    return results
}

Optionally you can pre-sort "lists" (of lists) by creasing length to avoid lots of iterations of the outer map() call, especially if lists lengths are heterogenous :

lists.sort((l1, l2) => l1.length - l2.length).map((list, idx) => { ... })

Your code with _lodash is working fine.

As you can say in this fiddle:

this code:

var arrayOfArrays = [[4234, 2323, 43], [1323, 43, 1313], [23, 34, 43]];
var a = _.intersection.apply(_, arrayOfArrays);
console.log(a);
console.log(a.length);

Will have output:

[42]
1

Maybe you see

equals: function

because you are using kind of debugger.

Try to just print the array with console.log , you will get only 42.

Lodash pure:

_.keys(_.pickBy(_.groupBy(_.flatten(arrays)), function (e) {return e.length > 1}))

Lodash with plain js:

var elements = {}, duplicates = {};
 _.each(arrays, function (array) {
     _.each(array, function (element) {
         if (!elements[element]) {
             elements[element] = true;
         } else {
             duplicates[element] = true;
         }
     });
 });
_.keys(duplicates);

Arg0n's answer is great, but just in case anyone is looking for intersection of object arrays, i modified Arg0n's answer a bit to deal with it.

function getIntersectedData() {
    var lists = [[{a:1,b:2},{a:2,b:2}],[{a:1,b:2},{a:3,b:3}],[{a:1,b:2},{a:4,b:4}]];
    var result = [];
    for (var i = 0; i < lists.length; i++) {
        var currentList = lists[i];
        for (var y = 0; y < currentList.length; y++) {
            var currentValue = currentList[y];
            if (customIndexOf(result,currentValue)) {
                var existsInAll = true;
                for (var x = 0; x < lists.length; x++) {
                    if(customIndexOf(lists[x],currentValue)){
                        existsInAll = false;
                        break;
                    }
                }
                if (existsInAll) {
                    result.push(currentValue);
                }
            }
        }
        return result;
    }
}

function customIndexOf(array,value){
    var notFind = true;
    array.forEach(function(element){
        if(element.a === value.a){
            notFind = false;
        }
    });
    return notFind;
}

I manage to accomplish this with a reduce call:

 var intersected = intersect([[1, 2, 3], [2, 3, 4], [3, 4, 5]]); console.log(intersected); // [3] function intersect(arrays) { if (0 === arrays.length) { return []; } return arrays.reduce((intersection, array) => { return intersection.filter(intersectedItem => array.some(item => intersectedItem === item)); }, arrays[0]); }

Intersection of a variable number of arrays.

This is how I do it:

function getArraysIntersection(list1, list2, ...otherLists) {
  const result = [];

  for (let i = 0; i < list1.length; i++) {
      let item1 = list1[i];
      let found = false;
      for (var j = 0; j < list2.length && !found; j++) {
          found = item1 === list2[j];
      }
      if (found === true) {
          result.push(item1);
      }
  }
  if (otherLists.length) {
    return getArraysIntersection(result, otherLists.shift(), ...otherLists);
  }
  else {
    return result;
  }
}

SNIPPET

 function getArraysIntersection(list1, list2, ...otherLists) { const result = []; for (let i = 0; i < list1.length; i++) { let item1 = list1[i]; let found = false; for (var j = 0; j < list2.length && !found; j++) { found = item1 === list2[j]; } if (found === true) { result.push(item1); } } if (otherLists.length) { return getArraysIntersection(result, otherLists.shift(), ...otherLists); } else { return result; } } const a = {label: "a", value: "value_A"}; const b = {label: "b", value: "value_B"}; const c = {label: "c", value: "value_C"}; const d = {label: "d", value: "value_D"}; const e = {label: "e", value: "value_E"}; const arr1 = [a,b,c]; const arr2 = [a,b,c]; const arr3 = [c]; const t0 = performance.now(); const intersection = getArraysIntersection(arr1,arr2,arr3); const t1 = performance.now(); console.log('This took t1-t0: ' + (t1-t0).toFixed(2) + ' ms'); console.log(intersection);

const intersect = (arrayA, arrayB) => {
    return arrayA.filter(elem => arrayB.includes(elem));
};
const intersectAll = (...arrays) => {
    if (!Array.isArray(arrays) || arrays.length === 0) return [];
    if (arrays.length === 1) return arrays[0];
    return intersectAll(intersect(arrays[0], arrays[1]), ...arrays.slice(2));
};

The cleanest way I've found to do this wasn't actually listed on this page, so here you are:

arrays[0].filter(elem => arrays.every(array => array.includes(elem)))

Reads like nice, clear english: every array includes the element. It assumes that you have at least 1 element in arrays, though. If you can't make this assumption, you can use optional chaining:

arrays?[0].filter(elem => arrays.every(array => array.includes(elem))) ?? []

For anyone who might need, this implements the intersection inside an array of arrays:

intersection(array) {
  if (array.length === 1)
    return array[0];
  else {
    array[1] = array[0].filter(value => array[1].includes(value));
    array.shift();
    return intersection(array);
  }
}
function getIntersection(ar1,ar2,...arrays){
    if(!ar2) return ar1

    let intersection = ar1.filter(value => ar2.includes(value));

    if(arrays.length ===0 ) return intersection

    return getIntersection(intersection,...arrays)
}

console.log(getIntersection([1,2,3], [3,4], [5,6,3]) // [3]

Sol with Maps
// nums1 = [1,2,2,1], nums2 = [2,2]

// n  m
// O(nm) + space O(min(n, m))

// preprocess nums2 to a Map<number, count>
// O(n + m) + space(min(n, m))
// process the shorter one

let preprocessTarget = nums1
let loopTarget = nums2

if (nums1.length > nums2.length) {
  preprocessTarget = nums2
  loopTarget = nums1
}

// Map<element, number>
const countMap = new Map()
for (let num of preprocessTarget) {
  if (countMap.has(num)) {
    countMap.set(num, countMap.get(num) + 1)
  } else {
    countMap.set(num, 1)
  }
}


const result = []

for (let num of loopTarget) {
  if (countMap.has(num)) {
    result.push(num)
    
    const count = countMap.get(num)
    if (count === 1) {
      countMap.delete(num)
    } else {
      countMap.set(num, count - 1)
    }
  }
}

return result

Function to calculate intersection of multiple arrays in JavaScript Write a method that creates an array of unique values that are included in all given arrays. Expected Result: ([1, 2], [2, 3]) => [2]

 const arr1 = [1, 2, 1, 2, 1, 2]; const arr2 = [2, 3]; const arr3 = ["a", "b"]; const arr4 = ["b", "c"]; const arr5 = ["b", "e", "c"]; const arr6 = ["b", "b", "e"]; const arr7 = ["b", "c", "e"]; const arr8 = ["b", "e", "c"]; const intersection = (...arrays) => { (data = [...arrays]), (result = data.reduce((a, b) => a.filter((c) => b.includes(c)))); return [...new Set(result)]; }; console.log(intersection(arr1, arr2)); // [2] console.log(intersection(arr3, arr4, arr5)); // ['b'] console.log(intersection(arr5, arr6, arr7, arr8)); // ['b', 'e']

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM