假设有两个集合 A
和 B
。我正在寻求一种非常快速或优雅的方式来计算它们之间的差集(A - B
或 A \B
,具体取决于您的喜好)。如标题所述,这两个集合被存储和操作为 Javascript 数组。
注意:
- Gecko 特定技巧可行
- 我更喜欢使用本地功能(但如果它更快,我也可以考虑轻量级库)
- 我看过但未测试过JS.Set(见上一点)
编辑: 我发现有评论提到集合包含重复元素。当我说“集合”时,我是指数学定义,这意味着它们不包含重复元素。
假设有两个集合 A
和 B
。我正在寻求一种非常快速或优雅的方式来计算它们之间的差集(A - B
或 A \B
,具体取决于您的喜好)。如标题所述,这两个集合被存储和操作为 Javascript 数组。
注意:
编辑: 我发现有评论提到集合包含重复元素。当我说“集合”时,我是指数学定义,这意味着它们不包含重复元素。
我不知道这是否最有效,但也许是最短的方法:
var A = [1, 2, 3, 4];
var B = [1, 3, 4, 7];
var diff = A.filter(function(x) {
return B.indexOf(x) < 0;
});
console.log(diff); // [2]
更新至ES6:
const A = [1, 2, 3, 4];
const B = [1, 3, 4, 7];
const diff = A.filter(x => !B.includes(x));
console.log(diff); // [2]
7年后,使用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)));
let a_union_b = new Set([...a, ...b]);
console.log(...a_minus_b); // {1}
console.log(...b_minus_a); // {5}
console.log(...a_intersect_b); // {2,3,4}
console.log(...a_union_b); // {1,2,3,4,5}
看到很多这样的解决方案,对于小规模情况表现不错。但是,当它们扩展到一百万项时,时间复杂度开始变得荒谬。
A.filter(v => B.includes(v))
看起来这开始变成O(N^2)的解决方案。既然有O(N)的解决方案,让我们使用它,如果您不熟悉JS运行时,可以轻松修改以不是生成器。
function *setMinus(A, B) {
const setA = new Set(A);
const setB = new Set(B);
for (const v of setB.values()) {
if (!setA.delete(v)) {
yield v;
}
}
for (const v of setA.values()) {
yield v;
}
}
a = [1,2,3];
b = [2,3,4];
console.log(Array.from(setMinus(a, b)));
虽然这个方法比其他解决方案更加复杂,但是当你有大型列表时,这种方法会更快。
让我们快速查看性能差异,运行在包含1,000,000个0到10,000之间的随机整数集合上,我们可以看到以下性能结果。
setMinus time = 181 ms
diff time = 19099 ms
function buildList(count, range) {
result = [];
for (i = 0; i < count; i++) {
result.push(Math.floor(Math.random() * range))
}
return result;
}
function *setMinus(A, B) {
const setA = new Set(A);
const setB = new Set(B);
for (const v of setB.values()) {
if (!setA.delete(v)) {
yield v;
}
}
for (const v of setA.values()) {
yield v;
}
}
function doDiff(A, B) {
return A.filter(function(x) { return B.indexOf(x) < 0 })
}
const listA = buildList(100_000, 100_000_000);
const listB = buildList(100_000, 100_000_000);
let t0 = process.hrtime.bigint()
const _x = Array.from(setMinus(listA, listB))
let t1 = process.hrtime.bigint()
const _y = doDiff(listA, listB)
let t2 = process.hrtime.bigint()
console.log("setMinus time = ", (t1 - t0) / 1_000_000n, "ms");
console.log("diff time = ", (t2 - t1) / 1_000_000n, "ms");
process.hrtime.bigint()
是一个 Node.js 函数,不是浏览器函数,无法在“运行代码片段”引擎中工作。区别在于纳秒和毫秒计时。 - koblas如果您正在使用Set
,那么它可以非常简单和高效:
function setDifference(a, b) {
return new Set(Array.from(a).filter(item => !b.has(item)));
}
由于Set
在底层使用哈希函数*,因此has
函数比indexOf
函数快得多(如果您有超过100个项,则这很重要)。
A
的每个元素线性扫描B
。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()
调用来加快代码执行速度。
最短的方法是使用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>
not
在3.0.0-rc1版本中不再支持通用对象。请参考https://github.com/jquery/jquery/issues/3147。 - Marc-André Lafortuneconst 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 }
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;
}
getDifference(a, b, hashOfB)
,如果没有传递,则会计算,否则将直接重用。 - Christophe Roussy借鉴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);
}
提供两个分别包含50000和66666个元素的集合。在这些值下,A-B操作大约需要75ms,而并集和交集操作各需要约150ms。(在Mac Safari 4.0上使用Javascript Date计时。)
我认为这对于20行代码来说是不错的回报。
hasOwnProperty()
:否则,类似于Object.prototype[42] = true;
的情况意味着42
永远不会出现在结果集中。 - Christoph
indexOf
实现中。 - Crescent Fresh