Finding matches between multiple JavaScript Arrays

2019-01-01 07:29发布

I have multiple arrays with string values and I want to compare them and only keep the matching results that are identical between ALL of them.

Given this example code:

var arr1 = ['apple', 'orange', 'banana', 'pear', 'fish', 'pancake', 'taco', 'pizza'];
var arr2 = ['taco', 'fish', 'apple', 'pizza'];
var arr3 = ['banana', 'pizza', 'fish', 'apple'];

I would like to to produce the following array that contains matches from all given arrays:

['apple', 'fish', 'pizza']

I know I can combine all the arrays with var newArr = arr1.concat(arr2, arr3); but that just give me an array with everything, plus the duplicates. Can this be done easily without needing the overhead of libraries such as underscore.js?

(Great, and now i'm hungry too!)

EDIT I suppose I should mention that there could be an unknown amount of arrays, I was just using 3 as an example.

9条回答
ら面具成の殇う
2楼-- · 2019-01-01 07:34

Now, that you've added an indeterminate number of arrays to the question, here's another approach that collects the count for each item into an object and then collates the items that have the max count.

Advantages of this approach:

  1. ~15x faster that brute force search options (used by other answers) if arrays are larger
  2. Does not require ES5 or ES5 shim (works with all browsers)
  3. Completely non-destructive (doesn't change source data at all)
  4. Handles duplicates items in source arrays
  5. Handles an arbitrary number of input arrays

And here's the code:

function containsAll(/* pass all arrays here */) {
    var output = [];
    var cntObj = {};
    var array, item, cnt;
    // for each array passed as an argument to the function
    for (var i = 0; i < arguments.length; i++) {
        array = arguments[i];
        // for each element in the array
        for (var j = 0; j < array.length; j++) {
            item = "-" + array[j];
            cnt = cntObj[item] || 0;
            // if cnt is exactly the number of previous arrays, 
            // then increment by one so we count only one per array
            if (cnt == i) {
                cntObj[item] = cnt + 1;
            }
        }
    }
    // now collect all results that are in all arrays
    for (item in cntObj) {
        if (cntObj.hasOwnProperty(item) && cntObj[item] === arguments.length) {
            output.push(item.substring(1));
        }
    }
    return(output);
}    

Working demo: http://jsfiddle.net/jfriend00/52mAP/

FYI, this does not require ES5 so will work in all browsers without a shim.

In a performance test on 15 arrays each 1000 long, this was more than 10x faster than the search method used in am not i am's answer in this jsperf: http://jsperf.com/in-all-arrays.


Here's a version that uses an ES6 Map and Set to de-dup and keep track of counts. This has the advantage that the type of data is preserved and can be anything (it doesn't even have to have a natural string conversion, the data can even be objects though objects are compared for being the exact same object, not having the same properties/values).

var arrays = [
    ['valueOf', 'toString','apple', 'orange', 'banana', 'banana', 'pear', 'fish', 'pancake', 'taco', 'pizza', 1, 2, 999, 888],
    ['valueOf', 'toString','taco', 'fish', 'fish', 'apple', 'pizza', 1, 999, 777, 999, 1],
    ['valueOf', 'toString','banana', 'pizza', 'fish', 'apple', 'apple', 1, 2, 999, 666, 555]
    ];
    
// subclass for updating cnts    
class MapCnt extends Map {
    constructor(iterable) {
        super(iterable);
    }
    
    cnt(iterable) {
        // make sure items from the array are unique
        let set = new Set(iterable);
        // now update the cnt for each item in the set
        for (let item of set) {
            let cnt = this.get(item) || 0;
            ++cnt;
            this.set(item, cnt);
        }
    }
}


function containsAll(...allArrays) {
    let cntObj = new MapCnt();
    for (array of allArrays) {
        cntObj.cnt(array);
    }
    // now see how many items have the full cnt
    let output = [];
    for (var [item, cnt] of cntObj.entries()) {
        if (cnt === allArrays.length) {
            output.push(item);
        }
    }
    return(output);
}    

var result = containsAll.apply(this, arrays);

document.body.innerHTML = "<pre>[<br>    " + result.join(',<br>    ') + "<br>]</pre>";

查看更多
柔情千种
3楼-- · 2019-01-01 07:34

Assuming there is an array of arrays those we want to find the intersection of, a simplest single liner approach could be

var arr = [[0,1,2,3,4,5,6,7,8,9],[0,2,4,6,8],[4,5,6,7]],
    int = arr.reduce((p,c) => p.filter(e => c.includes(e)));

document.write("<pre>" + JSON.stringify(int) + "</pre>");

查看更多
高级女魔头
4楼-- · 2019-01-01 07:38
var result = arrays.shift().filter(function(v) {
    return arrays.every(function(a) {
        return a.indexOf(v) !== -1;
    });
});

DEMO: http://jsfiddle.net/nWjcp/2/

You could first sort the outer Array to get the shortest Array at the beginning...

arrays.sort(function(a, b) {
    return a.length - b.length;
});

For completeness, here's a solution that deals with duplicates in the Arrays. It uses .reduce() instead of .filter()...

var result = arrays.shift().reduce(function(res, v) {
    if (res.indexOf(v) === -1 && arrays.every(function(a) {
        return a.indexOf(v) !== -1;
    })) res.push(v);
    return res;
}, []);

DEMO: http://jsfiddle.net/nWjcp/4/

查看更多
余生无你
5楼-- · 2019-01-01 07:38

Just for the heck of it, another long hand approach:

function getCommon(a) {

  // default result is copy of first array
  var result = a[0].slice();
  var mem, arr, found = false;

  // For each member of result, see if it's in all other arrays
  // Go backwards so can splice missing entries
  var i = result.length;

  while (i--) {
    mem = result[i];

    // Check in each array
    for (var j=1, jLen=a.length; j<jLen; j++) {
      arr = a[j];
      found = false;

      // For each member of arr and until found
      var k = arr.length;
      while (k-- && !found) {

        // If found in this array, set found to true
        if (mem == arr[k]) {
          found = true;
        }
      }
      // if word wasn't found in this array, remove it from result and 
      // start on next member of result, skip remaining arrays.
      if (!found) {
        result.splice(i,1);
        break;
      }
    }
  }
  return result;
}

var data = [
  ['taco', 'fish', 'apple', 'pizza', 'mango', 'pear'],
  ['apple', 'orange', 'banana', 'pear', 'fish', 'pancake', 'taco', 'pizza'],
  ['banana', 'pizza', 'fish', 'apple'],
  ['banana', 'pizza', 'fish', 'apple', 'mango', 'pear']
];

Edit

Function to find never enumerable properties based on thise on Object.prototype:

// Return an array of Object.prototype property names that are not enumerable
// even when added directly to an object.
// Can be helpful with IE as properties like toString are not enumerable even
// when added to an object.
function getNeverEnumerables() {

    // List of Object.prototype property names plus a random name for testing
    var spNames = 'constructor toString toLocaleString valueOf ' +
                  'hasOwnProperty isPrototypeOf propertyIsEnumerable foo';

    var spObj = {foo:'', 'constructor':'', 'toString':'', 'toLocaleString':'', 'valueOf':'',
                 'hasOwnProperty':'', 'isPrototypeOf':'', 'propertyIsEnumerable':''};

    var re = [];

    // BUild list of enumerable names in spObj
    for (var p in spObj) {
      re.push(p); 
    }

    // Remove enumerable names from spNames and turn into an array
    re = new RegExp('(^|\\s)' + re.join('|') + '(\\s|$)','g');
    return spNames.replace(re, ' ').replace(/(^\s+)|\s\s+|(\s+$)/g,'').split(' ');
}

document.write(getNeverEnumerables().join('<br>'));
查看更多
浪荡孟婆
6楼-- · 2019-01-01 07:48

Here goes a single-line solution. You can split it into two thinking steps:

  1. Calculate join/intersection between two arrays

var arrA = [1,2,3,4,5];
var arrB = [4,5,10];
var innerJoin = arrA.filter(el=>arrB.includes(el));
console.log(`Intersection is: ${innerJoin}`);

  1. Reduce the content: calculate the intersection between the acumulated intersection and the next array.

var arrays = [
 ['apple', 'orange', 'banana', 'pear', 'fish', 'pancake', 'taco', 'pizza'],
 ['taco', 'fish', 'apple', 'pizza'],
 ['banana', 'pizza', 'fish', 'apple']
];
var join = arrays.reduce((join, current) => join.filter(el => current.includes(el)));
console.log(`Intersection is: ${join}`);

查看更多
闭嘴吧你
7楼-- · 2019-01-01 07:54

A couple thoughts- you can compare just the items in the shortest array, and prevent duplicates in the returned array.

function arraysInCommon(arrays){
    var i, common,
    L= arrays.length, min= Infinity;
    while(L){
        if(arrays[--L].length<min){
            min= arrays[L].length;
            i= L;
        }
    }
    common= arrays.splice(i, 1)[0];
    return common.filter(function(itm, indx){
        if(common.indexOf(itm)== indx){
            return arrays.every(function(arr){
                return arr.indexOf(itm)!= -1;
            });
        }
    });
}

var arr1= ['apple', 'orange', 'banana', 'pear', 'fish', 'pancake', 'taco', 'pizza'];
var arr2= ['taco', 'fish', 'apple', 'pizza', 'apple','apple'];
var arr3= ['banana', 'pizza', 'fish', 'apple','fish'];

var allArrays = [arr1,arr2,arr3];

arraysInCommon(allArrays).sort();

returned value: apple,fish,pizza

DEMO - http://jsfiddle.net/kMcud/

查看更多
登录 后发表回答