Javascript dizilerini kullanarak küme farkı hesaplamanın en hızlı veya en zarif yolu nedir?


105

İzin verin Ave Biki set olun. Aralarındaki set farkını ( veya tercihinize bağlı olarak) hesaplamanın gerçekten hızlı veya zarif yollarını arıyorum . İki set, başlıktan da anlaşılacağı gibi Javascript dizileri olarak saklanır ve işlenir.A - BA \B

Notlar:

  • Geko'ya özgü numaralar sorun değil
  • Yerel işlevlere bağlı kalmayı tercih ederim (ancak daha hızlıysa hafif bir kitaplığa açığım)
  • JS.Set'i gördüm, ancak test etmedim (önceki noktaya bakın)

Düzenleme: Yinelenen öğeler içeren kümeler hakkında bir yorum fark ettim. "Set" dediğimde matematiksel tanıma atıfta bulunuyorum, bu da (diğer şeylerin yanı sıra) yinelenen öğeler içermediği anlamına gelir.


Kullandığınız bu "fark belirleme" terminolojisi nedir? Bu C ++ 'dan mı yoksa başka bir şey mi?
Josh Stodola

Setlerinde neler var? Hedeflediğiniz türe bağlı olarak (örn. Sayılar), bir set farkı hesaplamak gerçekten hızlı ve zarif bir şekilde yapılabilir . Kümeleriniz DOM öğeleri içeriyorsa (diyelim ki), yavaş bir indexOfuygulama ile sıkışıp kalacaksınız .
Crescent Fresh

@Crescent: Setlerim sayılar içeriyor - belirtmediğim için üzgünüm. @Josh: matematikte standart küme işlemidir ( en.wikipedia.org/wiki/Set_%28mathematics%29#Complements )
Matt Ball


1
@MattBall Hayır, bunu gördüm. Ama Josh'un sorusu geçerli ve cevapsızdı, ben de cevapladım :)
Pat

Yanıtlar:


175

Bunun en etkili olup olmadığını bilmiyorsanız, ancak belki de en kısa olanı

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

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

console.log(diff);

ES6'ya güncellendi:

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

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

console.log(diff);

8
+1: en verimli çözüm değil, ancak kesinlikle kısa ve okunabilir
Christoph

10
Not: array.filter, tarayıcılar arası desteklenmez (örneğin, IE'de desteklenmez). @Matt, "Gecko'ya özgü numaralar tamamdır" dediği için önemli değil gibi görünüyor ama bence bahsetmeye değer.
Eric Bréchemier

45
Bu çok yavaş. O (| A | * | B |)
glebm

1
@ EricBréchemier Bu artık desteklenmektedir (IE 9'dan beri). Array.prototype.filter , standart bir ECMAScript özelliğidir.
Quentin Roy

5
ES6'da, !B.includes(x)yerine kullanabilirsiniz B.indexOf(x) < 0:)
c24w

87

Eh, 7 yıl sonra, ES6'nın Set nesnesiyle oldukça kolay (ancak yine de pitonlar kadar kompakt değil A - B) ve bildirildiğine göre indexOfbüyük dizilerden daha hızlı :

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
Ayrıca büyük diziler için indexOf'dan önemli ölçüde daha hızlıdır.
Estus Flask

103
Neden JavaScript kümelerinde birleşim / kesişme / fark yerleşik değil benden öte ...
SwiftsNamesake

6
Tamamen katılıyorum; bunlar js motorunda uygulanan daha düşük seviyeli ilkeler olmalıdır. Benim de ötesinde ...
Rafael

4
@SwiftsNamesake Ocak 2018'de github.com/tc39/agendas/blob/master/2018/01.md'de bahsedileceğini umduğumuz yerleşik yöntemler için bir teklif var .
John

15

User187291'in cevabındaB olduğu Agibi, her bir elementi doğrusal olarak taramaktan kaçınmak için bir nesneyi harita olarak kullanabilirsiniz :

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

Benzersiz özellik adları elde etmek için standart olmayan toSource()yöntem kullanılır; tüm elemanların zaten benzersiz dize temsilleri varsa (sayılarda olduğu gibi), toSource()çağrıları bırakarak kodu hızlandırabilirsiniz .


9

JQuery kullanan en kısası:

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>


Bu, farkın bir nesnesini döndürür.
Drew Baker

2
jQuery not, 3.0.0-rc1'den itibaren artık genel nesnelerle çalışmamaktadır. Bkz. Github.com/jquery/jquery/issues/3147
Marc-André Lafortune

2
~ 70.000 3. parti kitaplığa bağımlılık eklemek harika bir fikir değil Sadece bunu yapmak , çünkü aynı şey buradaki diğer cevaplarda gösterildiği gibi sadece birkaç satır kodla da gerçekleştirilebilir. Ancak, projenizde zaten jQuery kullanıyorsanız, bu gayet iyi çalışacaktır.
CBarr

Bu yaklaşım daha az koda sahip olsa da, farklı algoritmaların uzay ve zaman karmaşıklığına ve yöntemi gerçekleştirmek için kullandığı veri yapısına ilişkin herhangi bir açıklama sağlamaz. Geliştiricilerin, veri ölçeğinin artmasına veya sınırlı belleğe izin verildiğinde hiçbir değerlendirme yapmadan yazılımı tasarlaması için kara kutudur. Bu tür bir yaklaşımı büyük veri kümesiyle kullanırsanız, performans kaynak kodu üzerinde daha fazla araştırma yapana kadar bilinmeyebilir.
Downhillski

Bu sadece A'nın B'de olmayan elemanlarının miktarını (bu durumda 2) döndürmektir. 2'yi diziye dönüştürmek anlamsızdır ...
Alex

6

B dizisini hash ederdim, sonra A dizisindeki değerleri B'de mevcut değil saklarım:

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

bu tam olarak yarım saat önce yayınladığım algoritmanın aynısı
Christoph

@Christoph: haklısın ... Bunu fark edemedim. Yine de uygulamamı anlamak için daha kolay buluyorum :)
Eric Bréchemier

Farklılığı getDifference dışında hesaplamanın daha iyi olacağını düşünüyorum, bu yüzden birden çok kez yeniden kullanılabilir. Belki de böyle isteğe bağlı: getDifference(a, b, hashOfB)geçilmezse hesaplanır, aksi takdirde olduğu gibi yeniden kullanılır.
Christophe Roussy

4

Christoph'un fikrini birleştirerek ve diziler ve nesneler / hash'ler ( eachve arkadaşlar) üzerinde birkaç standart olmayan yineleme yöntemi varsayarak , toplamda yaklaşık 20 satırda doğrusal zamanda set farkı, birleşim ve kesişim elde edebiliriz:

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

Bu varsayar eachve filterdiziler için tanımlanan ve iki yardımcı program yöntemleri olduğunu:

  • myUtils.keys(hash): hash anahtarlarını içeren bir dizi döndürür

  • myUtils.select(hash, fnSelector, fnEvaluator): true döndüren fnEvaluator anahtar / değer çiftlerinde arama sonuçlarını içeren bir dizi fnSelectordöndürür.

select()Gevşek Common Lisp esinlenerek ve ise sadece filter()vemap() bir arada. (Bunları açıklamak daha iyi olurdu Object.prototype, ancak bunu yapmak jQuery ile hasara yol açtı , bu yüzden statik yardımcı program yöntemlerine karar verdim.)

Performans: ile test etme

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

50.000 ve 66.666 elemanlı iki set verir. Bu değerlerle AB yaklaşık 75ms alırken, birleşim ve kesişme her biri yaklaşık 150ms'dir. (Zamanlama için Javascript Tarihini kullanan Mac Safari 4.0.)

Bence bu 20 satır kod için iyi bir kazanç.


1
hasOwnProperty()öğeler sayısal olsa bile yine de kontrol etmelisiniz : aksi takdirde, sonuç kümesinde Object.prototype[42] = true;araçlar gibi bir şey 42asla gerçekleşemez
Christoph

42'yi bu şekilde ayarlamanın mümkün olacağı kabul edildi, ancak birinin gerçekten bunu yapacağı yarı gerçekçi bir kullanım durumu var mı? Ancak genel dizeler için noktayı alıyorum - bazı Object.prototype değişkenleri veya işlevleriyle kolayca çakışabilir.
jg-faustus

3

Kullanılması Underscore.js (fonksiyonel JS için Kütüphane)

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

3

@ Milan'ın cevabından ödünç alınan bazı basit işlevler:

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

Kullanım:

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

Oruç tutmaya gelince, bu o kadar zarif değil ama emin olmak için bazı testler yaptım. Bir diziyi nesne olarak yüklemek, büyük miktarlarda işlemek için çok daha hızlıdır:

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

Sonuçlar:

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

Ancak, bu yalnızca dizelerle çalışır . Numaralı kümeleri karşılaştırmayı planlıyorsanız, sonuçları parseFloat ile eşleştirmek isteyeceksiniz .


1
b.filter(function(v) { return !A[v]; });İkinci işlevde c = olması gerekmez mi?
fabianmoronzirfas

Haklısın. Her nasılsa benim için daha da hızlı görünüyor
SmujMaiku

1

Bu işe yarıyor, ancak bence başka biri çok daha kısa ve zarif

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;
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.