Каков самый быстрый или самый элегантный способ вычисления разницы множеств с использованием массивов Javascript?

104

Позвольте Aи Bбыть двумя наборами. Я ищу действительно быстрые или элегантные способы вычисления разницы между ними ( A - Bили A \B, в зависимости от ваших предпочтений). Эти два набора хранятся и обрабатываются как массивы Javascript, как сказано в названии.

Ноты:

  • Уловки, специфичные для гекконов, допустимы
  • Я бы предпочел придерживаться собственных функций (но я открыт для облегченной библиотеки, если она намного быстрее)
  • Я видел, но не тестировал JS.Set (см. Предыдущий пункт)

Изменить: я заметил комментарий о наборах, содержащих повторяющиеся элементы. Когда я говорю «установить», я имею в виду математическое определение, которое означает (среди прочего), что они не содержат повторяющихся элементов.

Мэтт Болл
источник
Что это за терминология "различий наборов", которую вы используете? Это из C ++ или что-то в этом роде?
Джош Стодола
Что в твоих наборах? В зависимости от типа, на который вы нацеливаетесь (например, чисел), вычисление разницы между наборами может выполняться очень быстро и элегантно. Если ваши наборы содержат (скажем) элементы DOM, вы застрянете с медленной indexOfреализацией.
Crescent Fresh
@Crescent: мои наборы содержат числа - извините, что не уточняю. @Josh: это стандартная операция над множествами в математике ( en.wikipedia.org/wiki/Set_%28mat Mathematics%29#Complements )
Мэтт Болл
1
@MattBall Нет, я это видел. Но вопрос Джоша был действителен и остался без ответа, поэтому я ответил на него :)
Пэт,

Ответы:

175

если не знаю, наиболее ли это эффективно, но, возможно, самый короткий

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);
user187291
источник
8
+1: не самое эффективное решение, но определенно краткое и удобочитаемое
Кристоф
10
Примечание: array.filter не поддерживает кроссбраузерность (например, не в IE). Кажется, это не имеет значения для @Matt, поскольку он заявил, что «специфические для Gecko уловки - это нормально», но я думаю, что об этом стоит упомянуть.
Эрик Брешемье
45
Это очень медленно. O (| A | * | B |)
glebm 08
1
@ EricBréchemier Теперь это поддерживается (начиная с IE 9). Array.prototype.filter - это стандартная функция ECMAScript.
Quentin Roy
5
В ES6 вы могли бы использовать !B.includes(x)вместо B.indexOf(x) < 0:)
c24w
86

Что ж, 7 лет спустя с объектом Set ES6 это довольно просто (но все же не так компактно, как у 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
103
Я не
понимаю,
6
Я полностью согласен; это должны быть примитивы более низкого уровня, реализованные в движке js. Это тоже выше меня ...
Рафаэль
4
@SwiftsNamesake Есть предложение по установке встроенных методов, о котором, будем надеяться, поговорим в январе 2018 г. 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>

пергелий
источник
Это возвращает объект различия.
Дрю Бейкер
2
jQuery notбольше не работает с универсальными объектами, начиная с версии 3.0.0-rc1. См. Github.com/jquery/jquery/issues/3147
Marc-André Lafortune
2
Не рекомендуется добавлять зависимость от сторонней библиотеки размером ~ 70k просто для этого, поскольку то же самое можно сделать всего в нескольких строках кода, как показано в других ответах здесь. Однако, если вы уже используете jQuery в своем проекте, это будет работать нормально.
CBarr 05
Хотя у этого подхода меньше кода, он не дает никакого объяснения пространственной и временной сложности различных алгоритмов и структуры данных, которые он использует для выполнения метода. Для разработчиков это черный ящик для разработки программного обеспечения без оценки, когда допускается масштабирование данных или с ограниченным объемом памяти. если вы используете такой подход с большим набором данных, производительность может остаться неизвестной до дальнейшего исследования исходного кода.
Downhillski
Это просто возвращает количество (в данном случае 2) элементов A, которых нет в B. Преобразование 2 в массив бессмысленно ...
Alex
6

Я бы хэшировал массив 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;
}
Эрик Брешемье
источник
это точно такой же алгоритм, который я опубликовал полчаса назад
Christoph
@Christoph: ты прав ... Я этого не заметил. Я считаю свою реализацию более простой для понимания :)
Эрик Брешемье
Я думаю, что лучше вычислять разницу вне getDifference, чтобы ее можно было повторно использовать несколько раз. Может быть, необязательно, например, так:, getDifference(a, b, hashOfB)если он не передан, он будет вычислен, в противном случае он будет повторно использован как есть.
Кристоф Русси,
4

Включив идею Кристофа и допустив пару нестандартных методов итерации для массивов и объектов / хэшей ( 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возвращается истина.

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 строк кода это приличная плата.

jg-faustus
источник
1
вам все равно следует проверять, являются hasOwnProperty()ли элементы числовыми: в противном случае в результирующем наборе никогда не может появиться что-то вроде Object.prototype[42] = true;средства42
Кристоф
Допустим, что можно было бы установить 42 таким образом, но существует ли полуреалистичный вариант использования, когда кто-то действительно так поступил бы? Но для общих строк я понимаю, что это может легко конфликтовать с некоторой переменной или функцией Object.prototype.
jg-faustus
3

Использование Underscore.js (библиотека для функционального JS)

>>> var foo = [1,2,3]
>>> var bar = [1,2,4]
>>> _.difference(foo, bar);
[4]
Chribsen
источник
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 .

SmujMaiku
источник
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;
Хави Иварс
источник