Was ist der schnellste oder eleganteste Weg, um einen Satzunterschied mithilfe von Javascript-Arrays zu berechnen?


103

Lass Aund Bsei zwei Sätze. Ich suche nach sehr schnellen oder eleganten Methoden, um den eingestellten Unterschied ( A - Boder A \B, je nach Ihren Vorlieben) zwischen ihnen zu berechnen . Die beiden Sätze werden wie im Titel als Javascript-Arrays gespeichert und bearbeitet.

Anmerkungen:

  • Gecko-spezifische Tricks sind in Ordnung
  • Ich würde mich lieber an native Funktionen halten (aber ich bin offen für eine leichtgewichtige Bibliothek, wenn sie viel schneller ist)
  • Ich habe JS.Set gesehen, aber nicht getestet (siehe vorherigen Punkt).

Bearbeiten: Ich habe einen Kommentar zu Sets mit doppelten Elementen bemerkt. Wenn ich "set" sage, beziehe ich mich auf die mathematische Definition, was (unter anderem) bedeutet, dass sie keine doppelten Elemente enthalten.


Was ist diese von Ihnen verwendete Terminologie "Unterschied einstellen"? Ist das von C ++ oder so?
Josh Stodola

Was sind in deinen Sets? Abhängig von dem Typ, auf den Sie abzielen (z. B. Zahlen), kann die Berechnung eines festgelegten Unterschieds sehr schnell und elegant erfolgen. Wenn Ihre Sets (sagen wir) DOM-Elemente enthalten, werden Sie mit einer langsamen indexOfImplementierung stecken bleiben .
Crescent Fresh

@Crescent: Meine Sets enthalten Zahlen - sorry, dass ich sie nicht angegeben habe. @ Josh: Es ist die Standard-Set-Operation in der Mathematik ( en.wikipedia.org/wiki/Set_%28mathematics%29#Complements )
Matt Ball

@ JoshStodola, das ist die mathematische Notation für
Pat

1
@ MattBall Nein, das habe ich gesehen. Aber Joshs Frage war gültig und unbeantwortet, also habe ich sie beantwortet :)
Pat

Antworten:


173

Wenn Sie nicht wissen, ob dies am effektivsten ist, aber vielleicht am kürzesten

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

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

console.log(diff);

Auf ES6 aktualisiert:

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

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

console.log(diff);

8
+1: nicht die effizienteste Lösung, aber definitiv kurz und lesbar
Christoph

10
Hinweis: array.filter wird browserübergreifend nicht unterstützt (z. B. nicht im IE). Für @Matt scheint es keine Rolle zu spielen, da er sagte, dass "Gecko-spezifische Tricks in Ordnung sind", aber ich denke, es ist erwähnenswert.
Eric Bréchemier

44
Das ist sehr langsam. O (| A | * | B |)
Glebm

1
@ EricBréchemier Dies wird jetzt unterstützt (seit IE 9). Array.prototype.filter ist eine Standardfunktion von ECMAScript.
Quentin Roy

5
In ES6 könnten Sie !B.includes(x)anstelle von B.indexOf(x) < 0:)
c24w

86

Nun, 7 Jahre später ist es mit dem Set- Objekt von ES6 recht einfach (aber immer noch nicht so kompakt wie das von Python A - B ) und angeblich schneller als indexOfbei großen Arrays:

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
Auch erheblich schneller als indexOf für große Arrays.
Estus Flask

100
Warum in JavaScript-Sets keine Vereinigung / Schnittmenge / Differenz eingebaut ist, ist mir ein
Rätsel

6
Ich stimme vollkommen zu; Dies sollten Grundelemente niedrigerer Ebene sein, die in der js-Engine implementiert sind. Es ist mir auch ein
Rafael

4
@SwiftsNamesake Es gibt einen Vorschlag für Satz integrierter Methoden , die hoffentlich über in Janurary 2018 gesprochen werden github.com/tc39/agendas/blob/master/2018/01.md .
John

15

Sie können ein Objekt als Karte verwenden, um ein lineares Scannen Bfür jedes Element von Awie in der Antwort von user187291 zu vermeiden :

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;
}

Die nicht standardmäßige toSource()Methode wird verwendet, um eindeutige Eigenschaftsnamen abzurufen. Wenn alle Elemente bereits eindeutige Zeichenfolgendarstellungen haben (wie dies bei Zahlen der Fall ist), können Sie den Code beschleunigen, indem Sie die toSource()Aufrufe löschen.


9

Die kürzeste Verwendung von jQuery ist:

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>


Dies gibt ein Objekt der Differenz zurück.
Drew Baker

2
jQuery notfunktioniert ab 3.0.0-rc1 nicht mehr mit generischen Objekten. Siehe github.com/jquery/jquery/issues/3147
Marc-André Lafortune

2
Es ist keine gute Idee , eine Abhängigkeit von einer ~ 70k 3rd - Party - Bibliothek hinzufügen nur , dies zu tun, da das gleiche kann in nur wenige Zeilen Code ausgeführt werden , wie hier in den anderen Antworten gezeigt. Wenn Sie jedoch bereits jQuery für Ihr Projekt verwenden, funktioniert dies einwandfrei.
CBarr

Dieser Ansatz enthält zwar weniger Code, liefert jedoch keine Erklärung für die räumliche und zeitliche Komplexität der verschiedenen Algorithmen und die Datenstruktur, die zur Durchführung der Methode verwendet wird. Entwickler können die Software ohne Auswertung entwickeln, wenn die Daten skaliert werden oder wenn nur begrenzter Speicher verfügbar ist. Wenn Sie einen solchen Ansatz mit großen Datenmengen verwenden, bleibt die Leistung möglicherweise bis zur weiteren Untersuchung des Quellcodes unbekannt.
Downhillski

Dies gibt nur die Menge (in diesem Fall 2) von Elementen von A zurück, die nicht in B sind. Das Konvertieren von 2 in ein Array ist sinnlos ...
Alex

6

Ich würde das Array B hashen und dann Werte aus dem Array A behalten, die in B nicht vorhanden sind:

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;
}

Das ist genau der gleiche Algorithmus, den ich vor einer halben Stunde gepostet habe
Christoph

@Christoph: Sie haben Recht ... Ich habe das nicht bemerkt. Ich finde meine Implementierung jedoch einfacher zu verstehen :)
Eric Bréchemier

Ich denke, es ist besser, das Diff außerhalb von getDifference zu berechnen, damit es mehrmals wiederverwendet werden kann. Möglicherweise optional wie getDifference(a, b, hashOfB)folgt : Wenn es nicht bestanden wird, wird es berechnet, andernfalls wird es unverändert wiederverwendet.
Christophe Roussy

4

Wenn eachwir die Idee von Christoph einbeziehen und einige nicht standardmäßige Iterationsmethoden für Arrays und Objekte / Hashes ( und Freunde) annehmen , können wir Differenz, Vereinigung und Schnittmenge in linearer Zeit in insgesamt etwa 20 Zeilen festlegen:

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);
  }
};

Dies setzt voraus, dass eachund filterfür Arrays definiert sind und dass wir zwei Dienstprogrammmethoden haben:

  • myUtils.keys(hash): Gibt ein Array mit den Schlüsseln des Hash zurück

  • myUtils.select(hash, fnSelector, fnEvaluator): Gibt ein Array mit den Ergebnissen des Aufrufs fnEvaluator der Schlüssel / Wert-Paare zurück, für die fnSelectortrue zurückgegeben wird.

Das select()ist lose von Common Lisp inspiriert und ist nur filter()und map()in einem gerollt. (Es wäre besser, sie definiert zu habenObject.prototype , aber dies führt zu einem Chaos mit jQuery, sodass ich mich für statische Dienstprogrammmethoden entschieden habe.)

Leistung: Testen mit

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

gibt zwei Sätze mit 50.000 und 66.666 Elementen. Mit diesen Werten dauert AB ungefähr 75 ms, während Vereinigung und Schnittpunkt jeweils ungefähr 150 ms betragen. (Mac Safari 4.0 mit Javascript-Datum für das Timing.)

Ich denke, das ist eine anständige Auszahlung für 20 Codezeilen.


1
Sie sollten immer noch prüfen hasOwnProperty(), ob die Elemente numerisch sind. Andernfalls kann in der Ergebnismenge niemals so etwas wie Object.prototype[42] = true;Mittel 42auftreten
Christoph

Zugegeben, es wäre möglich, 42 auf diese Weise festzulegen, aber gibt es einen semi-realistischen Anwendungsfall, in dem dies tatsächlich jemand tun würde? Aber für allgemeine Zeichenfolgen nehme ich den Punkt - es könnte leicht zu Konflikten mit einer Object.prototype-Variablen oder -Funktion kommen.
JG-Faustus

3

Verwenden von Underscore.js (Bibliothek für funktionales JS)

>>> var foo = [1,2,3]
>>> var bar = [1,2,4]
>>> _.difference(foo, bar);
[4]

3

Einige einfache Funktionen, die aus der Antwort von @ milan stammen:

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]);

Verwendung:

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

Der fastete Weg ist nicht so elegant, aber ich habe einige Tests durchgeführt, um sicherzugehen. Das Laden eines Arrays als Objekt ist in großen Mengen viel schneller zu verarbeiten:

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);

Ergebnisse:

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

Dies funktioniert jedoch nur mit Zeichenfolgen . Wenn Sie nummerierte Sätze vergleichen möchten, möchten Sie die Ergebnisse mit parseFloat zuordnen .


1
Sollte es nicht c = b.filter(function(v) { return !A[v]; });in der zweiten Funktion sein?
Fabianmoronzirfas

Du hast Recht. Irgendwie scheint es für mich noch schneller zu sein
SmujMaiku

1

Das funktioniert, aber ich denke, ein anderer ist viel kürzer und auch eleganter

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;
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.