获取n个数组的交集

4
使用 ES6 的 Set,给定两个数组,我们可以像这样获取交集:
let a = new Set([1,2,3])
let b = new Set([1,2,4])
let intersect = new Set([...a].filter(i => b.has(i)));

我们如何获取n个数组的交集?
更新:
我正在尝试理解以下用例。我有一个至少有一个元素的二维数组。
parts.forEach(part => {
  intersection = new Set()
})

你如何获取parts中每个元素(数组)的交集?

你不能只是在循环中重复这个过程吗?如果你的输入是一个数组的数组,也许你可以使用.reduce() - nnnnnn
1
.filter(i => listOfAllOtherSets.every(set => set.has(i))) 怎么样? - Sebastian Simon
@nnnnnn,我添加了更多细节。我正在尝试理解如何在循环中重复这个过程。 - Raphael Rafatpanah
“intersect” 是什么意思? - guest271314
@guest271314,我是指数学意义上的交集。我已经编辑了我的问题,用“交集”代替了我原来的“并集”。 - Raphael Rafatpanah
显示剩余3条评论
5个回答

8
假设您有一个函数function intersect(set1, set2) {...},可以计算两个集合的交集,您可以使用reduce获得一组集合的交集:
function intersect(a, b) {
    return new Set(a.filter(i => b.has(i)));
}

var sets = [new Set([1,2,3]), ...];
var intersection = sets.reduce(intersect);

这是最佳解决方案。简洁而简单。 - Pritam Banerjee
1
几乎完美,但如果a是一个Set,则filter未定义。因此应该是function intersect(a, b) { return new Set(Array.from(a).filter(i => b.has(i))); } - olidem

4
您可以使用.filter().map().every()Array方法的组合来创建一个intersect帮助函数。
这个答案受到了来自Xufox的以上评论的启发,他提到在filter谓词中使用Array#every

function intersect (first = [], ...rest) {
   rest = rest.map(array => new Set(array))
   return first.filter(e => rest.every(set => set.has(e)))
}

let parts = [
  [1, 2, 3],
  [1, 2, 4],
  [1, 5, 2]
]

console.log(
  intersect(...parts)
)


我非常感谢这个更新。虽然我已经给予了正面评价,但我觉得Asad的回答完全适合我的情况。例如,在使用reduce时,你不需要区分第一个和剩余部分。 - Raphael Rafatpanah
1
我更喜欢这种方法而不是使用 reduce,因为它创建的 Set 对象要少得多,但如果您更喜欢另一种实现的简洁性,我也能理解。感谢您至少查看了它 :) - gyre

2
将文本翻译成中文:

交集n个数组的最有效算法是在fast_array_intersect中实现的算法。它运行时间为O(n),其中n是所有数组中元素的总数。

基本原则很简单:遍历所有数组,在一个映射中存储每个元素出现的次数。然后过滤最小的数组,只返回在所有数组中都出现过的元素。(源代码)

您可以使用一个简单的库:

import intersect from 'fast_array_intersect'

intersect([[1,2,3], [1,2,6]]) // --> [1,2]

2

ES6仍然有while

这种类型的函数很容易因为过多的处理而导致长时间的延迟。如果无条件地甚至是优先使用ES6和数组方法,如reduce、filter等,而不是像while和for这样简单的老式循环,那么情况就更加明显了。

当计算许多集合的交集时,如果发现某个项不属于交集,则每次迭代的工作量应该减少。因为forEach不能中断,所以你被迫继续迭代所有元素。添加一些代码来避免搜索当前项是否属于交集,可以提高性能,但这是一个真正的补救措施。

还有一种倾向就是只为了从数组、集合或映射中删除一个单独的项而创建整个新数据集。这是一个非常糟糕的习惯,我看到越来越多的人采用ES5的方式。

获取n个集合的交集。

那么问题来了,如何找到许多集合的交集。

解决方案B

一个典型的ES6解决方案

function intersectB(firstSet, ...sets) {
    // function to intercept two sets
    var intersect = (a,b) => {
        return new Set([...a].filter(item => b.has(item)))
    };

    // iterate all sets comparing the first set to each.
    sets.forEach(sItem => firstSet = intersect(firstSet, sItem));

    // return the result.
    return firstSet;
}

var sets = [new Set([1,2,3,4]), new Set([1,2,4,6,8]), new Set([1,3,4,6,8])];
var inter = intersectB(...sets);
console.log([...inter]);

这个功能在执行简单测试时表现良好,执行时间不到一毫秒。但在我看来,它是一个浪费内存的低效结构。几乎在每一行创建数组和集合,并在已知结果的情况下迭代整个集合。

让我们给它更多的工作。100个集合,每个集合最多有10000个项目,每个测试有10个不同数量的匹配项。大多数拦截将返回空集合。

警告:会导致页面挂起一整秒钟... :(

// Create a set of numbers from 0 and < count
// With a odds for any number occurring to be odds
// return as a new set;
function createLargeSet(count,odds){
    var numbers = new Set();
    while(count-- > 0){
        if(Math.random() < odds){
            numbers.add(count);
        }
    }
    return numbers;
}
// create a array of large sets
function bigArrayOfSets(setCount,setMaxSize,odds){
    var bigSets = [];
    for(var i = 0; i < setCount; i ++){
        bigSets.push(createLargeSet(setMaxSize,odds));
    }
    return bigSets;
}
function intersectB(firstSet, ...sets) {
    var intersect = (a,b) => {
        return new Set([...a].filter(item => b.has(item)))
    };
    sets.forEach(sItem => firstSet = intersect(firstSet, sItem));
    return firstSet;
}
var testSets = [];
for(var i = 0.1; i <= 1; i += 0.1){
    testSets.push(bigArrayOfSets(100,10000,i));
}

var now = performance.now();
testSets.forEach(testDat => intersectB(...testDat));
var time = performance.now() - now;
console.log("Execution time : " + time);

解决方案A

一种更好的方式,不如花哨,但更加高效。

function intersectA(firstSet,...sets) {
    var count = sets.length;
    var result = new Set(firstSet); // Only create one copy of the set
    firstSet.forEach(item => {
        var i = count;
        var allHave = true;
        while(i--){
            allHave = sets[i].has(item)
            if(!allHave) { break }  // loop only until item fails test
        }
        if(!allHave){
            result.delete(item);  // remove item from set rather than
                                  // create a whole new set
        }
    })
    return result;
}

比较

现在让我们来比较一下两者,如果你感到幸运的话,可以尝试猜测性能差异,这是衡量你对JavaScript执行理解的好方法。

// Create a set of numbers from 0 and < count
// With a odds for any number occurring to be odds
// return as a new set;
function createLargeSet(count,odds){
    var numbers = new Set();
    while(count-- > 0){
        if(Math.random() < odds){
            numbers.add(count);
        }
    }
    return numbers;
}
// create a array of large sets
function bigArrayOfSets(setCount,setMaxSize,odds){
    var bigSets = [];
    for(var i = 0; i < setCount; i ++){
        bigSets.push(createLargeSet(setMaxSize,odds));
    }
    return bigSets;
}
function intersectA(firstSet,...sets) {
    var count = sets.length;
    var result = new Set(firstSet); // Only create one copy of the set
    firstSet.forEach(item => {
        var i = count;
        var allHave = true;
        while(i--){
            allHave = sets[i].has(item)
            if(!allHave) { break }  // loop only until item fails test
        }
        if(!allHave){
            result.delete(item);  // remove item from set rather than
                                  // create a whole new set
        }
    })
    return result;
}

function intersectB(firstSet, ...sets) {
    var intersect = (a,b) => {
        return new Set([...a].filter(item => b.has(item)))
    };
    sets.forEach(sItem => firstSet = intersect(firstSet, sItem));
    return firstSet;
}
var testSets = [];
for(var i = 0.1; i <= 1; i += 0.1){
    testSets.push(bigArrayOfSets(100,10000,i));
}

var now = performance.now();
testSets.forEach(testDat => intersectB(...testDat));
var time = performance.now() - now;
console.log("Execution time 'intersectB' : " + time);

var now = performance.now();
testSets.forEach(testDat => intersectA(...testDat));
var time = performance.now() - now;
console.log("Execution time 'intersectA' : " + time);

正如你所看到的,使用简单的while循环可能不如使用filter酷,但性能优势巨大,这是下次编写完美的3行ES6数组操作函数时需要记住的事情。别忘了for和while。


你可以轻松地将 once() 或 some() 作为 forEach() 的替代品,以实现提前终止。 - dandavis
@dandavis 是的(虽然没有for或while循环那么快),我是想说明一个观点。另外,set没有这些方法。 - Blindman67

1

我想最有效的数组交集方法是利用Map或Hash对象。在这里,我测试了1000个数组,每个数组都包含1到175之间的大约1000个随机整数项,以获取交集。结果在不到100毫秒内得到。

function setIntersection(a){
  var m = new Map(),
      r = new Set(),
      l = a.length;
  a.forEach(sa => new Set(sa).forEach(n => m.has(n) ? m.set(n,m.get(n)+1)
                                                    : m.set(n,1)));
  m.forEach((v,k) => v === l && r.add(k));
  return r;
}

var testSets = Array(1000).fill().map(_ => Array(1000).fill().map(_ => ~~(Math.random()*175+1)));
console.time("int");
result = setIntersection(testSets);
console.timeEnd("int");
console.log(JSON.stringify([...result]));


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接