使用Javascript数组计算集合差异的最快或最优雅的方法是什么?


103

AB为两组。我正在寻找一种非常快速或优雅的方法来计算它们之间的设置差异(A - BA \B,取决于您的偏好)。如标题所示,这两组存储和存储为Javascript数组。

笔记:

  • 壁虎特技可以
  • 我宁愿坚持使用本机函数(但如果速度更快,我可以使用轻量级库)
  • 我已经看到但尚未测试JS.Set(请参阅上一点)

编辑:我注意到有关包含重复元素的集合的评论。当我说“ set”时,是指数学定义,这意味着(除其他外)它们不包含重复的元素。


您正在使用什么“设置差异”术语?那是C ++的东西吗?
乔什·斯托多拉

您的套装中有什么?根据您要定位的类型(例如,数字),可以真正快速,轻松地计算出设置的差异。如果您的集合包含(例如)DOM元素,那么您将陷入缓慢的indexOf实施过程。

@Crescent:我的集合中包含数字-很抱歉,没有指定。@Josh:它在数学(标准设置操作en.wikipedia.org/wiki/Set_%28mathematics%29#Complements
马特·鲍尔

@JoshStodola是设定差
帕特

1
@MattBall不,我看到了。但是乔希(Josh)的问题是有效的并且没有得到回答,所以我回答了:)
帕特

Answers:


173

如果不知道这是否最有效,但可能最短

A = [1, 2, 3, 4];
B = [1, 3, 4, 7];

diff = A.filter(function(x) { return B.indexOf(x) < 0 })

console.log(diff);

更新到ES6:

A = [1, 2, 3, 4];
B = [1, 3, 4, 7];

diff = A.filter(x => !B.includes(x) );

console.log(diff);

8
+1:不是最有效的解决方案,但绝对简短易懂
Christoph

10
注意:跨浏览器不支持array.filter(例如IE中不支持)。@Matt似乎无关紧要,因为他说“壁虎特定的技巧还可以”,但我认为值得一提。
EricBréchemier09年

44
这很慢。O(| A | * | B |)
glebm 2013年

1
@EricBréchemier现在支持(自IE 9开始)。Array.prototype.filter是标准的ECMAScript功能。
昆汀·罗伊

5
在ES6中,您可以使用!B.includes(x)代替B.indexOf(x) < 0:)
c24w

86

好吧,七年后,有了ES6的Set对象,它非常简单(但仍然不如python的 紧凑A - B),并且据报道比indexOf大型数组要快:

console.clear();
let a = new Set([1, 2, 3, 4]);
let b = new Set([5, 4, 3, 2]);


let a_minus_b = new Set([...a].filter(x => !b.has(x)));
let b_minus_a = new Set([...b].filter(x => !a.has(x)));
let a_intersect_b = new Set([...a].filter(x => b.has(x))); 

console.log([...a_minus_b]) // {1}
console.log([...b_minus_a]) // {5}
console.log([...a_intersect_b]) // {2,3,4}


1
对于大型数组,它也比indexOf快得多。
Estus Flask

100
为什么JavaScript集没有内置联合/相交/差异的原因不在我...
SwiftsNamesake 2016年

6
我完全同意; 这些应该是在js引擎中实现的较低级别的原语。这也超越了我……
Rafael

4
@SwiftsNamesake有一套内置方法的建议,希望能在2018年1月github.com/tc39/agendas/blob/master/2018/01.md讨论
约翰

15

您可以使用一个对象作为地图,以避免线性扫描B每个元素的A作为user187291的回答

function setMinus(A, B) {
    var map = {}, C = [];

    for(var i = B.length; i--; )
        map[B[i].toSource()] = null; // any other value would do

    for(var i = A.length; i--; ) {
        if(!map.hasOwnProperty(A[i].toSource()))
            C.push(A[i]);
    }

    return C;
}

非标准toSource()方法用于获取唯一的属性名称。如果所有元素都已经具有唯一的字符串表示形式(与数字一样),则可以通过删除toSource()调用来加快代码的速度。


9

使用jQuery最短的是:

var A = [1, 2, 3, 4];
var B = [1, 3, 4, 7];

var diff = $(A).not(B);

console.log(diff.toArray());
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>


这将返回差异对象。
Drew Baker

2
not从3.0.0-rc1开始,jQuery 不再与通用对象一起使用。见github.com/jquery/jquery/issues/3147
马克-安德烈·Lafortune

2
仅仅为了做到这一点,就添加一个〜70k的第三方库并不是一个好主意,因为只需几行代码就可以完成相同的事情,如此处其他答案所示。但是,如果您已经在项目上使用jQuery,那么它将正常工作。
CBarr

尽管此方法的代码较少,但是它无法提供有关不同算法的时空复杂度以及用于执行该方法的数据结构的任何解释。当开发人员在数据放大或内存有限的情况下进行评估时,无需进行评估即可将其涂黑框。如果您对大型数据集使用这种方法,则在进一步研究源代码之前,性能可能仍然未知。
Downhillski

这只是返回不在B中的A元素的数量(在这种情况下为2)。将2转换为数组毫无意义……
Alex

6

我会哈希数组B,然后保留数组B中不存在的值:

function getHash(array){
  // Hash an array into a set of properties
  //
  // params:
  //   array - (array) (!nil) the array to hash
  //
  // return: (object)
  //   hash object with one property set to true for each value in the array

  var hash = {};
  for (var i=0; i<array.length; i++){
    hash[ array[i] ] = true;
  }
  return hash;
}

function getDifference(a, b){
  // compute the difference a\b
  //
  // params:
  //   a - (array) (!nil) first array as a set of values (no duplicates)
  //   b - (array) (!nil) second array as a set of values (no duplicates)
  //
  // return: (array)
  //   the set of values (no duplicates) in array a and not in b, 
  //   listed in the same order as in array a.

  var hash = getHash(b);
  var diff = [];
  for (var i=0; i<a.length; i++){
    var value = a[i];
    if ( !hash[value]){
      diff.push(value);
    }
  }
  return diff;
}

这与半个小时前发布的算法完全相同
Christoph

@Christoph:你是对的...我没注意到。我发现我的实现方式更容易理解:)
EricBréchemier09年

我认为最好在getDifference之外计算diff,以便可以多次重用。也许像这样是可选的:getDifference(a, b, hashOfB),如果不通过它将被计算,否则将按原样重用。
Christophe Roussy

4

结合Christoph的想法,并假设在数组和对象/哈希(each以及朋友)上使用了几种非标准的迭代方法,我们可以在大约20行的线性时间内获得集合差,并集和交集:

var setOPs = {
  minusAB : function (a, b) {
    var h = {};
    b.each(function (v) { h[v] = true; });
    return a.filter(function (v) { return !h.hasOwnProperty(v); });
  },
  unionAB : function (a, b) {
    var h = {}, f = function (v) { h[v] = true; };
    a.each(f);
    b.each(f);
    return myUtils.keys(h);
  },
  intersectAB : function (a, b) {
    var h = {};
    a.each(function (v) { h[v] = 1; });
    b.each(function (v) { h[v] = (h[v] || 0) + 1; });
    var fnSel = function (v, count) { return count > 1; };
    var fnVal = function (v, c) { return v; };
    return myUtils.select(h, fnSel, fnVal);
  }
};

这假设each并且filter为数组定义了,并且我们有两个实用程序方法:

  • myUtils.keys(hash):返回带有哈希键的数组

  • myUtils.select(hash, fnSelector, fnEvaluator):返回一个数组,该数组具有调用fnEvaluator 键/值对的结果,对于该键/值对 fnSelector返回true。

select()松散的Common Lisp的启发,只是filter()map()集于一身。(最好在上定义它们Object.prototype,但是这样做会对jQuery造成严重破坏,因此我选择了静态实用程序方法。)

性能:测试

var a = [], b = [];
for (var i = 100000; i--; ) {
  if (i % 2 !== 0) a.push(i);
  if (i % 3 !== 0) b.push(i);
}

给出了两个包含50,000和66,666元素的集合。使用这些值,AB大约需要75毫秒,而联合和相交每个大约需要150毫秒。(Mac Safari 4.0,使用Javascript日期作为计时。)

我认为这是20行代码的丰厚回报。


1
您仍然应该检查hasOwnProperty()即使元素是数字:否则,Object.prototype[42] = true;均值之类的东西42就永远不会出现在结果集中
Christoph

可以以这种方式设置42,但是有没有一个半现实的用例,任何人都可以这样做?但是对于一般的字符串,我很重要-它很容易与某些Object.prototype变量或函数发生冲突。
jg-faustus,2009年


3

一些简单的功能,借鉴@milan的答案:

const setDifference = (a, b) => new Set([...a].filter(x => !b.has(x)));
const setIntersection = (a, b) => new Set([...a].filter(x => b.has(x)));
const setUnion = (a, b) => new Set([...a, ...b]);

用法:

const a = new Set([1, 2]);
const b = new Set([2, 3]);

setDifference(a, b); // Set { 1 }
setIntersection(a, b); // Set { 2 }
setUnion(a, b); // Set { 1, 2, 3 }

2

至于禁食的方法,这不是很优雅,但是我已经进行了一些测试来确定。将一个数组作为对象加载要更快地进行大量处理:

var t, a, b, c, objA;

    // Fill some arrays to compare
a = Array(30000).fill(0).map(function(v,i) {
    return i.toFixed();
});
b = Array(20000).fill(0).map(function(v,i) {
    return (i*2).toFixed();
});

    // Simple indexOf inside filter
t = Date.now();
c = b.filter(function(v) { return a.indexOf(v) < 0; });
console.log('completed indexOf in %j ms with result %j length', Date.now() - t, c.length);

    // Load `a` as Object `A` first to avoid indexOf in filter
t = Date.now();
objA = {};
a.forEach(function(v) { objA[v] = true; });
c = b.filter(function(v) { return !objA[v]; });
console.log('completed Object in %j ms with result %j length', Date.now() - t, c.length);

结果:

completed indexOf in 1219 ms with result 5000 length
completed Object in 8 ms with result 5000 length

但是,这适用于字符串。如果您打算比较编号的集合,则需要使用parseFloat映射结果。


1
b.filter(function(v) { return !A[v]; });在第二个函数中不是c = 吗?
fabianmoronzirfas

你是对的。不知何故对我来说它甚至更快
SmujMaiku

1

这行得通,但我认为另一个更短,更优雅

A = [1, 'a', 'b', 12];
B = ['a', 3, 4, 'b'];

diff_set = {
    ar : {},
    diff : Array(),
    remove_set : function(a) { ar = a; return this; },
    remove: function (el) {
        if(ar.indexOf(el)<0) this.diff.push(el);
    }
}

A.forEach(diff_set.remove_set(B).remove,diff_set);
C = diff_set.diff;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.