Apa cara tercepat atau paling elegan untuk menghitung perbedaan set menggunakan array Javascript?

103

Membiarkan Adan Bmenjadi dua set. Saya mencari cara yang sangat cepat atau elegan untuk menghitung perbedaan set ( A - Batau A \B, tergantung pada preferensi Anda) di antara keduanya. Kedua set disimpan dan dimanipulasi sebagai array Javascript, seperti judulnya.

Catatan:

  • Trik khusus tokek tidak masalah
  • Saya lebih suka tetap menggunakan fungsi asli (tetapi saya terbuka untuk perpustakaan ringan jika jauh lebih cepat)
  • Saya telah melihat, tetapi belum diuji, JS.Set (lihat poin sebelumnya)

Edit: Saya melihat komentar tentang set yang mengandung elemen duplikat. Ketika saya mengatakan "set", saya mengacu pada definisi matematika, yang berarti (antara lain) tidak mengandung elemen duplikat.

Matt Ball
sumber
Apa terminologi "set difference" yang Anda gunakan? Apakah itu dari C ++ atau sesuatu?
Josh Stodola
Apa yang ada di set Anda? Bergantung pada jenis yang Anda targetkan (misalnya Angka), menghitung perbedaan yang ditetapkan dapat dilakukan dengan sangat cepat dan elegan. Jika set Anda berisi (katakanlah) elemen DOM, Anda akan terjebak dengan indexOfimplementasi yang lambat .
Crescent Fresh
@ Bulan Sabit: set saya berisi angka - maaf karena tidak menyebutkannya. @Josh: ini adalah operasi set standar dalam matematika ( en.wikipedia.org/wiki/Set_%28mathematics%29#Complements )
Matt Ball
@JoshStodola itulah notasi matematika untuk perbedaan set
Tepuk
1
@MattBall Tidak, saya melihatnya. Tapi pertanyaan Josh valid dan tidak terjawab jadi saya menjawabnya :)
Pat

Jawaban:

173

jika tidak tahu apakah ini paling efektif, tapi mungkin yang terpendek

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

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

console.log(diff);

Diperbarui ke ES6:

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

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

console.log(diff);
pengguna187291
sumber
8
+1: bukan solusi yang paling efisien, tapi jelas singkat dan mudah dibaca
Christoph
10
Catatan: array.filter tidak didukung lintas browser (mis. Tidak di IE). Sepertinya tidak masalah bagi @Matt karena dia menyatakan bahwa "Trik khusus tokek tidak masalah" tapi saya pikir itu layak untuk disebutkan.
Eric Bréchemier
44
Ini sangat lambat. O (| A | * | B |)
glebm
1
@ EricBréchemier Ini sekarang didukung (sejak IE 9). Array.prototype.filter adalah fitur ECMAScript standar.
Quentin Roy
5
Di ES6, Anda dapat menggunakan !B.includes(x)sebagai pengganti B.indexOf(x) < 0:)
c24w
86

Nah, 7 tahun kemudian, dengan objek Set ES6 itu cukup mudah (tetapi masih tidak sekompak python A - B ), dan dilaporkan lebih cepat daripada indexOfarray besar:

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}

Milan
sumber
1
Juga jauh lebih cepat daripada indexOf untuk array besar.
Estus Flask
100
Mengapa set JavaScript tidak memiliki penyatuan / perpotongan / perbedaan
bawaan
6
Saya sangat setuju; ini harus primitif tingkat yang lebih rendah yang diterapkan di mesin js. Ini di luar kemampuanku juga ...
Rafael
4
@SwiftsNamesake Ada proposal untuk metode bawaan yang diharapkan akan dibicarakan di Januari 2018 github.com/tc39/agendas/blob/master/2018/01.md .
Yohanes
15

Anda dapat menggunakan objek sebagai peta untuk menghindari pemindaian linier Buntuk setiap elemen Aseperti dalam jawaban pengguna187291 :

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

The non-standar toSource()metode yang digunakan untuk mendapatkan nama properti yang unik; jika semua elemen sudah memiliki representasi string unik (seperti halnya angka), Anda dapat mempercepat kode dengan menghilangkan toSource()pemanggilan.

Christoph
sumber
9

Yang terpendek, menggunakan jQuery, adalah:

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>

perhelion
sumber
Ini mengembalikan objek perbedaan.
Drew Baker
2
jQuery nottidak lagi berfungsi dengan objek umum mulai 3.0.0-rc1. Lihat github.com/jquery/jquery/issues/3147
Marc-André Lafortune
2
Bukan ide yang bagus untuk menambahkan dependensi pada ~ 70k pustaka pihak ketiga hanya untuk melakukan ini, karena hal yang sama dapat dicapai hanya dalam beberapa baris kode seperti yang ditunjukkan dalam jawaban lain di sini. Namun, jika Anda sudah menggunakan jQuery pada proyek Anda, ini akan berfungsi dengan baik.
CBarr
Meskipun pendekatan ini memiliki lebih sedikit kode, tetapi tidak memberikan penjelasan apa pun tentang kompleksitas ruang dan waktu dari berbagai algoritme dan struktur data yang digunakan untuk melakukan metode tersebut. Ini adalah kotak hitam bagi pengembang untuk merekayasa perangkat lunak tanpa evaluasi ketika skala data atau dengan memori terbatas diperbolehkan. jika Anda menggunakan pendekatan seperti itu dengan kumpulan data yang besar, kinerja mungkin tetap tidak diketahui sampai penelitian lebih lanjut ke kode sumber.
Downhillski
Ini hanya mengembalikan jumlah (2 dalam kasus ini) elemen A yang tidak ada di B.Mengubah 2 menjadi array tidak ada gunanya ...
Alex
6

Saya akan melakukan hash pada array B, lalu menyimpan nilai dari array A yang tidak ada di 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
sumber
itu persis algoritma yang sama yang saya posting setengah jam yang lalu
Christoph
@ Christoph: Anda benar ... Saya gagal menyadarinya. Saya menemukan implementasi saya lebih sederhana untuk dipahami :)
Eric Bréchemier
Saya pikir lebih baik menghitung diff di luar getDifference sehingga dapat digunakan kembali beberapa kali. Mungkin opsional seperti ini:, getDifference(a, b, hashOfB)jika tidak lulus akan dihitung jika tidak digunakan kembali sebagaimana adanya.
Christophe Roussy
4

Menggabungkan ide dari Christoph dan mengasumsikan beberapa metode iterasi non-standar pada array dan objek / hash ( eachdan teman-teman), kita bisa mendapatkan perbedaan yang ditetapkan, penyatuan dan persimpangan dalam waktu linier dalam total sekitar 20 baris:

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

Ini mengasumsikan bahwa eachdan filterditentukan untuk array, dan kami memiliki dua metode utilitas:

  • myUtils.keys(hash): mengembalikan array dengan kunci hash

  • myUtils.select(hash, fnSelector, fnEvaluator): mengembalikan larik dengan hasil pemanggilan fnEvaluator pasangan kunci / nilai yang fnSelectormengembalikan nilai true.

Ini select()secara longgar terinspirasi oleh Common Lisp, dan hanya filter()dan map()digulung menjadi satu. (Akan lebih baik untuk menetapkannya Object.prototype, tetapi melakukannya akan merusak jQuery, jadi saya memilih metode utilitas statis.)

Kinerja: Menguji dengan

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

memberikan dua set dengan 50.000 dan 66.666 elemen. Dengan nilai-nilai ini AB membutuhkan waktu sekitar 75ms, sedangkan union dan intersection masing-masing sekitar 150ms. (Mac Safari 4.0, menggunakan Javascript Date untuk pengaturan waktu.)

Saya pikir itu hasil yang layak untuk 20 baris kode.

jg-faustus.dll
sumber
1
Anda tetap harus memeriksa hasOwnProperty()meskipun elemennya numerik: jika tidak, sesuatu seperti Object.prototype[42] = true;sarana 42tidak akan pernah terjadi dalam set hasil
Christoph
Memang dimungkinkan untuk menetapkan 42 dengan cara itu, tetapi adakah kasus penggunaan semi-realistis di mana seseorang benar-benar akan melakukannya? Tetapi untuk string umum saya mengambil intinya - itu bisa dengan mudah konflik dengan beberapa variabel atau fungsi Object.prototype.
jg-faustus
3

Menggunakan Underscore.js (Perpustakaan untuk JS fungsional)

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

Beberapa fungsi sederhana, meminjam dari jawaban @ 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]);

Pemakaian:

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
sumber
2

Adapun cara berpuasa, ini tidak begitu elegan tetapi saya telah menjalankan beberapa tes untuk memastikannya. Memuat satu larik sebagai objek jauh lebih cepat untuk diproses dalam jumlah besar:

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

Hasil:

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

Namun, ini hanya berfungsi dengan string . Jika Anda berencana untuk membandingkan set bernomor, Anda akan ingin memetakan hasil dengan parseFloat .

SmujMaiku
sumber
1
Bukankah seharusnya c = b.filter(function(v) { return !A[v]; });dalam fungsi kedua?
fabianmoronzirfas
Anda benar. Entah bagaimana tampaknya menjadi lebih cepat bagi saya
SmujMaiku
1

Ini berfungsi, tetapi saya pikir yang lain jauh lebih pendek, dan juga elegan

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
sumber