Який найшвидший або найелегантніший спосіб обчислити різницю наборів за допомогою масивів Javascript?


103

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

Примітки:

  • Характерні для геккону трюки - це добре
  • Я вважаю за краще дотримуватися рідних функцій (але я відкритий для легкої бібліотеки, якщо це швидше)
  • Я бачив, але не перевіряв JS.Set (див. Попередній пункт)

Редагувати: я помітив коментар щодо наборів, що містять повторювані елементи. Коли я кажу "встановити", я маю на увазі математичне визначення, яке означає (серед іншого), що вони не містять повторюваних елементів.


Що це за термінологія "встановити різницю", яку ви використовуєте? Це від C ++ чи щось таке?
Джош Стодола

Що у ваших наборах? Залежно від типу, на який ви орієнтуєтесь (наприклад, Numbers), обчислення різниці наборів може бути виконано дуже швидко та елегантно. Якщо ваші набори містять (скажімо) елементи DOM, ви будете зациклюватися на повільній indexOfреалізації.
Півмісяць Свіжий

@Crescent: мої набори містять цифри - вибачте, що не вказали. @Josh: це стандартна операція з математики ( en.wikipedia.org/wiki/Set_%28mathematics%29#Complements )
Matt Ball


1
@MattBall Nope, я це бачив. Але питання Джоша було дійсним і без відповіді, тому я відповів на нього :)
Пат

Відповіді:


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: не найефективніше рішення, але, безумовно, короткий і читабельний
Крістоф

10
Примітка: array.filter не підтримується крос-браузером (наприклад, не в IE). Це, мабуть, не має значення для @Matt, оскільки він заявив, що "прийоми, що стосуються специфічних для Геко, все добре", але я думаю, що це варто згадати.
Eric Bréchemier

44
Це дуже повільно. O (| A | * | B |)
glebm

1
@ EricBréchemier Це зараз підтримується (з IE 9). Array.prototype.filter - це стандартна функція ECMAScript.
Квентін Рой

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 для великих масивів.
колба Естуса

100
Чому набори JavaScript не мають вбудованого союзу / пересічення / різниці - це поза мною ...
SwiftsNamesake

6
Я повністю згоден; це повинні бути примітиви нижчого рівня, реалізовані в js-двигуні. Це теж поза мною ...
Рафаель

4
@SwiftsNamesake Є пропозиція щодо встановлення вбудованих методів, про які, сподіваємось, мова піде у січні Janurary 2018 github.com/tc39/agendas/blob/master/2018/01.md .
Джон

15

Ви можете використовувати об'єкт як карту, щоб уникнути лінійного сканування Bкожного елемента, Aяк у відповіді користувача187291 :

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/isissue/3147
Марк-Андре Лафортун

2
Це не чудова ідея додавати залежність від сторонньої бібліотеки ~ 70k просто для цього, оскільки те саме можна виконати лише за кількома рядками коду, як показано в інших відповідях тут. Однак якщо ви вже використовуєте jQuery у своєму проекті, це буде добре.
CBarr

Хоча цей підхід має менше коду, але він не дає жодного пояснення просторової та часової складності різних алгоритмів та структури даних, які він використовує для виконання методу. Розробники розроблюють чорний ящик, щоб розробляти програмне забезпечення без оцінки, коли дозволено збільшення даних або обмежена пам'ять. якщо ви використовуєте такий підхід із великим набором даних, продуктивність може залишатися невідомою до подальшого дослідження вихідного коду.
Downhillski

Це лише повернення суми (у даному випадку 2) елементів A, яких немає у В. Перетворення 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: ти маєш рацію ... Я цього не помітив. Я вважаю, що моя реалізація є більш простою для розуміння :)
Eric Bréchemier

Я думаю, що краще обчислити різницю поза 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повертає 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ніколи не може виникнути в наборі результатів
Крістоф

Зрозуміло, що таким чином можна було б встановити 42, але чи є напівреалістичний випадок використання, коли хтось насправді так би зробив? Але для загальних рядків я вважаю, що це може легко конфліктувати з якоюсь змінною або функцією Object.prototype.
jg-faustus


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
Чи не повинно бути c = b.filter(function(v) { return !A[v]; });у другій функції?
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;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.