Каков самый быстрый или самый элегантный способ вычисления разницы в наборах с использованием массивов Javascript?
Пусть A
и B
- два набора. Я ищу очень быстрые или элегантные способы вычислить разницу между наборами (A - B
или A \B
, в зависимости от ваших предпочтений) между ними. Эти два набора хранятся и обрабатываются как массивы Javascript, как говорится в названии.
Примечания:
- Специальные трюки от Gecko в порядке
- Я бы предпочел придерживаться встроенных функций (но я открыт для облегченной библиотеки, если быстрее)
- Я видел, но не тестировался, JS.Set (см. предыдущую точку)
Изменить: Я заметил комментарий о наборах, содержащих повторяющиеся элементы. Когда я говорю "set", я имею в виду математическое определение, которое означает (между прочим), что они не содержат повторяющихся элементов.
Ответы
Ответ 1
если не знаете, если это наиболее эффективно, но, возможно, самый короткий
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);
Ответ 2
Итак, 7 лет спустя, с объектом ES6 Set это довольно легко (но все же не так компактно, как у питонов 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_union_b = new Set([...a].filter(x => b.has(x)));
console.log([...a_minus_b]) // {1}
console.log([...b_minus_a]) // {5}
console.log([...a_union_b]) // {2,3,4}
Ответ 3
Вы можете использовать объект в качестве карты, чтобы избежать линейного сканирования B
для каждого элемента A
, как в user187291 answer:
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()
.
Ответ 4
Самый короткий, используя 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>
Ответ 5
Я бы сделал массив B, а затем сохранил значения из массива A, отсутствующего в 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;
}
Ответ 6
Включая идею от Кристофа и предполагая пару нестандартных методов итерации на массивах и объектах/хэшах (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()
свободно вдохновляется Общим 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 элементами. При этих значениях A-B занимает около 75 мс, а объединение и пересечение - около 150 мс каждый. (Mac Safari 4.0, используя Javascript Date для синхронизации.)
Я думаю, что приличный выигрыш для 20 строк кода.
Ответ 7
Использование Underscore.js (Библиотека для функциональных JS)
>>> var foo = [1,2,3]
>>> var bar = [1,2,4]
>>> _.difference(foo, bar);
[4]
Ответ 8
Что касается голодного способа, это не так элегантно, но я проверил некоторые тесты, чтобы быть уверенным. Загрузка одного массива в виде объекта намного быстрее обрабатывается в больших количествах:
var t, a, b, c, A;
// 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();
A = {};
a.forEach(function(v) { A[v] = true; });
c = b.filter(function(v) { return !a[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
Однако это работает только с строками. Если вы планируете сравнивать нумерованные наборы, вам нужно сопоставить результаты с помощью parseInt.
Ответ 9
Некоторые простые функции, заимствованные из @milan answer:
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 }
Ответ 10
Это работает, но я думаю, что еще один гораздо более короткий и элегантный тоже
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;
Ответ 11
Вы можете использовать этот легкий массив-diff компонент с открытым исходным кодом.
Пример:
diff([1,2,3], [1,2,3,4,5]) // => [4,5]
Он работает путем согласования двух переданных массивов и фильтрации включенных vals, возвращая массив, представляющий разницу между двумя массивами:
function diff(firstArray: any[], secondArray: any[]): any[] {
return firstArray.concat(secondArray).filter((val) => {
return !(firstArray.includes(val) && secondArray.includes(val));
});
};