Qual è il modo più veloce o più elegante per calcolare una differenza di set utilizzando gli array Javascript?


103

Siano Ae Bsiano due insiemi. Sto cercando modi molto veloci o eleganti per calcolare la differenza di set ( A - Bo A \B, a seconda delle preferenze) tra di loro. I due set vengono memorizzati e manipolati come array Javascript, come dice il titolo.

Appunti:

  • I trucchi specifici del geco vanno bene
  • Preferirei attenermi alle funzioni native (ma sono aperto a una libreria leggera se è molto più veloce)
  • Ho visto, ma non testato, JS.Set (vedi punto precedente)

Modifica: ho notato un commento sui set contenenti elementi duplicati. Quando dico "set" mi riferisco alla definizione matematica, il che significa (tra le altre cose) che non contengono elementi duplicati.


Qual è la terminologia di "set differenza" che stai utilizzando? È da C ++ o qualcosa del genere?
Josh Stodola

Cosa ci sono nei tuoi set? A seconda del tipo di destinazione (ad es. Numeri), il calcolo di una differenza di set può essere fatto in modo molto veloce ed elegante. Se i tuoi set contengono (diciamo) elementi DOM, sarai bloccato con indexOfun'implementazione lenta .
Crescent Fresh

@ Crescent: i miei set contengono numeri - mi dispiace per non averli specificati. @Josh: è l'operazione di set standard in matematica ( en.wikipedia.org/wiki/Set_%28mathematics%29#Complements )
Matt Ball


1
@ MattBall No, l'ho visto. Ma la domanda di Josh era valida e senza risposta, quindi ho risposto :)
Pat

Risposte:


173

se non so se è il più efficace, ma forse il più breve

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

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

console.log(diff);

Aggiornato a ES6:

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

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

console.log(diff);

8
+1: non la soluzione più efficiente, ma decisamente breve e leggibile
Christoph

10
Nota: array.filter non è supportato cross-browser (ad esempio, non in IE). Sembra non avere importanza per @Matt dal momento che ha affermato che "i trucchi specifici del geco vanno bene", ma penso che valga la pena menzionarlo.
Eric Bréchemier

44
Questo è molto lento. O (| A | * | B |)
glebm

1
@ EricBréchemier Questo è ora supportato (da IE 9). Array.prototype.filter è una funzionalità ECMAScript standard.
Quentin Roy

5
In ES6, potresti usare al !B.includes(x)posto di B.indexOf(x) < 0:)
c24w

86

Bene, 7 anni dopo, con l' oggetto Set di ES6 è abbastanza facile (ma ancora non compatto come quello di Python A - B ) e, a quanto riferito, più veloce rispetto indexOfagli array di grandi dimensioni:

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
Anche notevolmente più veloce di indexOf per array di grandi dimensioni.
Estus Flask

100
Perché i set JavaScript non hanno unione / intersezione / differenza incorporata è al di là di me ...
SwiftsNamesake

6
Sono completamente d'accordo; queste dovrebbero essere primitive di livello inferiore implementate nel motore js. È anche al di là di me ...
Rafael

4
@SwiftsNamesake C'è una proposta per impostare metodi integrati di cui si spera si parlerà a gennaio 2018 github.com/tc39/agendas/blob/master/2018/01.md .
John

15

È possibile utilizzare un oggetto come mappa per evitare la scansione lineare Bper ogni elemento Acome nella risposta di 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;
}

Il toSource()metodo non standard viene utilizzato per ottenere nomi di proprietà univoci; se tutti gli elementi hanno già rappresentazioni di stringa univoche (come nel caso dei numeri), è possibile accelerare il codice eliminando le toSource()chiamate.


9

Il più breve, utilizzando 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>


Ciò restituisce un oggetto della differenza.
Drew Baker

2
jQuery notnon funziona più con oggetti generici a partire dalla versione 3.0.0-rc1. Vedi github.com/jquery/jquery/issues/3147
Marc-André Lafortune

2
Non è una buona idea aggiungere una dipendenza da una libreria di terze parti ~ 70k solo per farlo, poiché la stessa cosa può essere realizzata in poche righe di codice come mostrato nelle altre risposte qui. Tuttavia, se stai già utilizzando jQuery sul tuo progetto, funzionerà perfettamente.
CBarr

Sebbene questo approccio abbia meno codice, non fornisce alcuna spiegazione della complessità spaziale e temporale dei diversi algoritmi e della struttura dati che utilizza per eseguire il metodo. È una scatola nera per gli sviluppatori progettare il software senza valutazione quando è consentito l'aumento dei dati o con memoria limitata. se si utilizza tale approccio con un set di dati di grandi dimensioni, le prestazioni potrebbero rimanere sconosciute fino a ulteriori ricerche sul codice sorgente.
Downhillski

Questo sta solo restituendo la quantità (2 in questo caso) di elementi di A che non sono in B. Convertire 2 in array è inutile ...
Alex

6

Vorrei hash l'array B, quindi manterrei i valori dell'array A non presente in 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;
}

è esattamente lo stesso algoritmo che ho pubblicato mezz'ora fa
Christoph

@Christoph: hai ragione ... non me ne sono accorto. Trovo la mia implementazione più semplice da capire però :)
Eric Bréchemier

Penso che sia meglio calcolare il diff al di fuori di getDifference in modo che possa essere riutilizzato più volte. Forse facoltativo in questo modo:, getDifference(a, b, hashOfB)se non passato verrà calcolato altrimenti viene riutilizzato così com'è.
Christophe Roussy

4

Incorporando l'idea di Christoph e assumendo un paio di metodi di iterazione non standard su array e oggetti / hash ( eache amici), possiamo ottenere la differenza di set, l'unione e l'intersezione in tempo lineare in circa 20 righe totali:

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

Ciò presuppone che eachefilter siano definiti per gli array e che abbiamo due metodi di utilità:

  • myUtils.keys(hash): restituisce un array con le chiavi dell'hash

  • myUtils.select(hash, fnSelector, fnEvaluator): restituisce un array con i risultati della chiamata fnEvaluator sulle coppie chiave / valore per le quali fnSelectorrestituisce true.

Il select()è vagamente ispirato a Common Lisp, ed è semplicemente filter()e map()arrotolato in uno. (Sarebbe meglio averli definiti Object.prototype, ma così facendo si rovina il caos con jQuery, quindi ho optato per metodi di utilità statici.)

Prestazioni: test con

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

fornisce due set con 50.000 e 66.666 elementi. Con questi valori AB impiega circa 75 ms, mentre l'unione e l'intersezione sono circa 150 ms ciascuna. (Mac Safari 4.0, utilizzando Javascript Date per i tempi.)

Penso che sia un discreto guadagno per 20 righe di codice.


1
dovresti comunque controllare hasOwnProperty()anche se gli elementi sono numerici: altrimenti, qualcosa come Object.prototype[42] = true;mezzi 42non può mai verificarsi nel set di risultati
Christoph

Ammesso che sarebbe possibile impostare 42 in quel modo, ma esiste un caso d'uso semi-realistico in cui qualcuno lo farebbe effettivamente? Ma per le stringhe generali prendo il punto: potrebbe facilmente entrare in conflitto con alcune variabili o funzioni Object.prototype.
jg-faustus

3

Utilizzo di Underscore.js (libreria per JS funzionale)

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

3

Alcune semplici funzioni, prese in prestito dalla risposta di @ 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]);

Uso:

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

Per quanto riguarda il modo a digiuno, questo non è così elegante ma ho eseguito alcuni test per essere sicuro. Il caricamento di un array come oggetto è molto più veloce da elaborare in grandi quantità:

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

risultati:

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

Tuttavia, funziona solo con le stringhe . Se prevedi di confrontare set numerati, ti consigliamo di mappare i risultati con parseFloat .


1
Non dovrebbe essere c = b.filter(function(v) { return !A[v]; });nella seconda funzione?
fabianmoronzirfas

Hai ragione. In qualche modo sembra essere ancora più veloce per me
SmujMaiku

1

Funziona, ma penso che un altro sia molto più corto e anche elegante

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;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.