Qual é a maneira mais rápida ou elegante de calcular uma diferença de conjunto usando matrizes Javascript?

103

Deixe Ae Bseja dois conjuntos. Estou procurando maneiras realmente rápidas ou elegantes de calcular a diferença definida ( A - Bou A \B, dependendo de sua preferência) entre eles. Os dois conjuntos são armazenados e manipulados como arrays Javascript, como o título diz.

Notas:

  • Truques específicos do Gecko são permitidos
  • Eu prefiro usar funções nativas (mas estou aberto a uma biblioteca leve se for muito mais rápida)
  • Eu vi, mas não testei, JS.Set (ver ponto anterior)

Edit: Notei um comentário sobre conjuntos contendo elementos duplicados. Quando digo "conjunto", estou me referindo à definição matemática, o que significa (entre outras coisas) que eles não contêm elementos duplicados.

Matt Ball
fonte
Qual é a terminologia de "definir diferenças" que você está usando? É do C ++ ou algo assim?
Josh Stodola
O que está em seus sets? Dependendo do tipo que você está mirando (por exemplo, Números), calcular uma diferença definida pode ser feito de forma muito rápida e elegante. Se seus conjuntos contiverem (digamos) elementos DOM, você terá uma indexOfimplementação lenta .
Crescent Fresh
@Crescent: meus conjuntos contêm números - desculpe por não especificar. @Josh: é a operação de definição padrão em matemática ( en.wikipedia.org/wiki/Set_%28mathematics%29#Complements )
Matt Ball
1
@MattBall Não, eu vi isso. Mas a pergunta de Josh era válida e sem resposta, então eu respondi :)
Pat de

Respostas:

173

se não sei se isso é mais eficaz, mas talvez o mais curto

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

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

console.log(diff);

Atualizado para ES6:

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

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

console.log(diff);
user187291
fonte
8
+1: não é a solução mais eficiente, mas definitivamente curta e legível
Christoph
10
Nota: array.filter não é compatível com navegadores cruzados (por exemplo, não no IE). Parece não ter importância para @Matt, já que ele afirmou que "truques específicos para Gecko estão bem", mas acho que vale a pena mencionar.
Eric Bréchemier
44
Isso é muito lento. O (| A | * | B |)
glebm
1
@ EricBréchemier Agora com suporte (desde o IE 9). Array.prototype.filter é um recurso ECMAScript padrão.
Quentin Roy de
5
No ES6, você pode usar em !B.includes(x)vez de B.indexOf(x) < 0:)
c24w
86

Bem, 7 anos depois, com o objeto Set do ES6 é bastante fácil (mas ainda não tão compacto quanto o do python A - B ) e supostamente mais rápido do que indexOfpara grandes 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}

milão
fonte
1
Também consideravelmente mais rápido do que indexOf para matrizes grandes.
Estus Flask
100
Por que os conjuntos de JavaScript não têm união / interseção / diferença embutidos está além da minha compreensão ...
SwiftsNamesake
6
Eu concordo completamente; estes devem ser primitivos de nível inferior implementados no mecanismo js. Está além de mim também ...
Rafael
4
@SwiftsNamesake Existe uma proposta para definir métodos integrados que, com sorte, será discutida em janeiro de 2018 github.com/tc39/agendas/blob/master/2018/01.md .
John
15

Você pode usar um objeto como um mapa para evitar a verificação linear Bde cada elemento, Acomo na resposta do usuário 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;
}

O toSource()método não padrão é usado para obter nomes de propriedade exclusivos; se todos os elementos já tiverem representações de string exclusivas (como é o caso dos números), você pode acelerar o código eliminando as toSource()invocações.

Christoph
fonte
9

O mais curto, usando 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>

perélio
fonte
Isso retorna um objeto da diferença.
Drew Baker
2
jQuery notnão funciona mais com objetos genéricos desde 3.0.0-rc1. Consulte github.com/jquery/jquery/issues/3147
Marc-André Lafortune
2
Não é uma boa ideia adicionar uma dependência em uma biblioteca de terceiros ~ 70k apenas para fazer isso, uma vez que a mesma coisa pode ser realizada em apenas algumas linhas de código, conforme mostrado nas outras respostas aqui. No entanto, se você já estiver usando jQuery em seu projeto, isso funcionará muito bem.
CBarr
Embora essa abordagem tenha menos código, ela não fornece nenhuma explicação da complexidade de espaço e tempo dos diferentes algoritmos e da estrutura de dados que usa para executar o método. É uma caixa preta para os desenvolvedores projetarem o software sem avaliação quando os dados aumentam de escala ou com memória limitada são permitidos. se você usar essa abordagem com um grande conjunto de dados, o desempenho pode permanecer desconhecido até pesquisas adicionais sobre o código-fonte.
Downhillski
Isso é apenas retornar a quantidade (2 neste caso) de elementos de A que não estão em B. Converter 2 em array não faz sentido ...
Alex
6

Eu faria um hash da matriz B e, em seguida, manteria os valores da matriz A não presentes em 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;
}
Eric Bréchemier
fonte
esse é exatamente o mesmo algoritmo que postei meia hora atrás
Christoph
@Christoph: você tem razão ... não percebi isso. Acho minha implementação mais simples de entender :)
Eric Bréchemier
Acho que é melhor calcular o diff fora de getDifference para que possa ser reutilizado várias vezes. Pode ser opcional como:, getDifference(a, b, hashOfB)se não for aprovado, será calculado, caso contrário, será reutilizado como está.
Christophe Roussy de
4

Incorporando a ideia de Christoph e assumindo alguns métodos de iteração não padrão em arrays e objetos / hashes ( eache amigos), podemos obter a diferença, união e interseção no tempo linear em cerca de 20 linhas no total:

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

Isso pressupõe que eache filtersão definidos para matrizes e que temos dois métodos utilitários:

  • myUtils.keys(hash): retorna uma matriz com as chaves do hash

  • myUtils.select(hash, fnSelector, fnEvaluator): retorna uma matriz com os resultados da chamada fnEvaluator dos pares chave / valor para os quais fnSelectorretorna verdadeiro.

O select()é vagamente inspirado no Common Lisp e é meramente filter()e map()agrupado em um. (Seria melhor tê-los definidos emObject.prototype , mas fazer isso estraga o caos com o jQuery, então me conformei com métodos utilitários estáticos.)

Desempenho: Testando com

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

dá dois conjuntos com 50.000 e 66.666 elementos. Com esses valores, AB leva cerca de 75ms, enquanto a união e a interseção têm cerca de 150ms cada. (Mac Safari 4.0, usando data Javascript para cronometragem.)

Acho que é uma recompensa decente para 20 linhas de código.

jg-faustus
fonte
1
você ainda deve verificar hasOwnProperty()mesmo se os elementos são numéricos: caso contrário, algo como Object.prototype[42] = true;meios 42nunca pode ocorrer no conjunto de resultados
Christoph
Concedido que seria possível definir 42 dessa maneira, mas existe um caso de uso semi-realista em que alguém realmente faria isso? Mas para strings gerais eu entendo o ponto - poderia facilmente entrar em conflito com alguma variável ou função Object.prototype.
jg-faustus
3

Usando Underscore.js (biblioteca para JS funcional)

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

Algumas funções simples, emprestadas da resposta de @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 }
Brian Burns
fonte
2

Quanto ao modo rápido, não é muito elegante, mas fiz alguns testes para ter certeza. Carregar uma matriz como um objeto é muito mais rápido de processar em grandes quantidades:

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

Resultados:

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

No entanto, isso funciona apenas com strings . Se você planeja comparar conjuntos numerados, desejará mapear os resultados com parseFloat .

SmujMaiku
fonte
1
Não deveria ser c = b.filter(function(v) { return !A[v]; });na segunda função?
fabianmoronzirfas
Você está certo. De alguma forma, parece ser ainda mais rápido para mim
SmujMaiku
1

Isso funciona, mas acho que outro é muito mais curto e elegante também

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;
Xavi Ivars
fonte