Cách nhanh nhất hoặc thanh lịch nhất để tính toán chênh lệch tập hợp bằng cách sử dụng mảng Javascript là gì?


103

Cho ABlà hai tập hợp. Tôi đang tìm những cách thực sự nhanh chóng hoặc thanh lịch để tính toán sự khác biệt đã đặt ( A - Bhoặc A \B, tùy thuộc vào sở thích của bạn) giữa chúng. Hai tập hợp được lưu trữ và thao tác dưới dạng mảng Javascript, như tiêu đề đã nói.

Ghi chú:

  • Các thủ thuật dành riêng cho tắc kè là được
  • Tôi muốn gắn bó với các hàm gốc (nhưng tôi đang mở một thư viện nhẹ nếu nó nhanh hơn)
  • Tôi đã thấy, nhưng chưa thử nghiệm, JS.Set (xem điểm trước)

Chỉnh sửa: Tôi nhận thấy một nhận xét về các tập hợp chứa các phần tử trùng lặp. Khi tôi nói "set", tôi đang đề cập đến định nghĩa toán học, có nghĩa là (trong số những thứ khác) rằng chúng không chứa các phần tử trùng lặp.


Bạn đang sử dụng thuật ngữ "khác biệt thiết lập" này là gì? Đó là từ C ++ hay gì đó?
Josh Stodola

Bộ của bạn là gì? Tùy thuộc vào loại bạn đang nhắm mục tiêu (ví dụ: Số), việc tính toán sự khác biệt đã đặt có thể được thực hiện thực sự nhanh chóng và thanh lịch. Nếu tập hợp của bạn chứa (giả sử) các phần tử DOM, bạn sẽ gặp khó khăn với indexOfviệc triển khai chậm .
Crescent Fresh

@Crescent: bộ của tôi chứa số - xin lỗi vì không chỉ định. @Josh: đó là hoạt động được thiết lập tiêu chuẩn trong toán học ( en.wikipedia.org/wiki/Set_%28mathearies%29#Complements )
Matt Ball vào


1
@MattBall Không, tôi đã thấy điều đó. Nhưng câu hỏi của Josh là hợp lệ và chưa được trả lời nên tôi đã trả lời nó :)
Pat.

Câu trả lời:


173

nếu không biết liệu cách này có hiệu quả nhất không, nhưng có lẽ là ngắn nhất

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

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

console.log(diff);

Đã cập nhật lên ES6:

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

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

console.log(diff);

8
1: không phải là giải pháp hiệu quả nhất, nhưng chắc chắn ngắn và dễ đọc
Christoph

10
Lưu ý: array.filter không được hỗ trợ trên nhiều trình duyệt (ví dụ: không có trong IE). Có vẻ như không thành vấn đề với @Matt vì anh ấy đã tuyên bố rằng "Các thủ thuật dành riêng cho tắc kè là được" nhưng tôi nghĩ điều đó đáng nói.
Eric Bréchemier

44
Điều này là rất chậm. O (| A | * | B |)
glebm

1
@ EricBréchemier Tính năng này hiện đã được hỗ trợ (kể từ IE 9). Array.prototype.filter là một tính năng ECMAScript tiêu chuẩn.
Quentin Roy

5
Trong ES6, bạn có thể sử dụng !B.includes(x)thay vì B.indexOf(x) < 0:)
c24w

86

Chà, 7 năm sau, với đối tượng Set của ES6, nó khá dễ dàng (nhưng vẫn không nhỏ gọn bằng python A - B ) và nhanh hơn so indexOfvới các mảng lớn:

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
Cũng nhanh hơn đáng kể so với indexOf cho các mảng lớn.
Estus Flask

100
Tại sao bộ JavaScript không có công đoàn / giao nhau / khác biệt được xây dựng trong là ngoài tôi ...
SwiftsNamesake

6
Tôi hoàn toàn đồng ý; chúng phải là các nguyên thủy cấp thấp hơn được thực hiện trong js engine. Đó là ngoài tôi cũng ...
Rafael

4
@SwiftsNamesake Có một đề xuất cho các phương thức tích hợp sẵn mà chúng tôi hy vọng sẽ được nói đến trong Janurary 2018 github.com/tc39/agendas/blob/master/2018/01.md .
John

15

Bạn có thể sử dụng một đối tượng làm bản đồ để tránh quét tuyến tính Bcho từng phần tử Anhư trong câu trả lời của 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;
}

Các phi tiêu chuẩn toSource()phương pháp được sử dụng để có được tên thuộc tính độc đáo; nếu tất cả các phần tử đã có các biểu diễn chuỗi duy nhất (như trường hợp với các số), bạn có thể tăng tốc mã bằng cách bỏ các lệnh toSource()gọi.


9

Ngắn nhất, sử dụng jQuery, là:

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>


Điều này trả về một đối tượng của sự khác biệt.
Drew Baker

2
jQuery notkhông còn hoạt động với các đối tượng chung kể từ 3.0.0-rc1. Xem github.com/jquery/jquery/issues/3147
Marc-André Lauckyne

2
Không phải là một ý tưởng tuyệt vời nếu thêm phụ thuộc vào thư viện bên thứ 3 ~ 70k chỉ để làm điều này, vì điều tương tự có thể được thực hiện chỉ trong một vài dòng mã như được hiển thị trong các câu trả lời khác ở đây. Tuy nhiên, nếu bạn đang sử dụng jQuery trong dự án của mình, điều này sẽ hoạt động tốt.
CBarr

Mặc dù cách tiếp cận này có ít mã hơn, nhưng nó không cung cấp bất kỳ giải thích nào về độ phức tạp không gian và thời gian của các thuật toán khác nhau và cấu trúc dữ liệu mà nó sử dụng để thực hiện phương pháp. Nó được đóng hộp đen để các nhà phát triển thiết kế phần mềm mà không cần đánh giá khi dữ liệu được mở rộng quy mô hoặc với bộ nhớ hạn chế được phép. nếu bạn sử dụng cách tiếp cận như vậy với tập dữ liệu lớn, hiệu suất có thể vẫn chưa được biết cho đến khi nghiên cứu sâu hơn về mã nguồn.
Downhillski

Điều này chỉ được trả lại số tiền (2 trong trường hợp này) các yếu tố của A mà không thuộc B. Chuyển đổi 2 thành mảng là vô nghĩa ...
Alex

6

Tôi sẽ băm mảng B, sau đó giữ các giá trị từ mảng A không có trong 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;
}

đó chính xác là thuật toán tôi đã đăng nửa giờ trước
Christoph

@Christoph: bạn nói đúng ... Tôi không nhận thấy điều đó. Tôi thấy thực hiện của tôi đơn giản hơn để hiểu mặc dù :)
Eric Bréchemier

Tôi nghĩ tốt hơn nên tính toán diff bên ngoài getDifference để nó có thể được sử dụng lại nhiều lần. Có thể tùy chọn như vậy:, getDifference(a, b, hashOfB)nếu không được thông qua, nó sẽ được tính nếu không nó sẽ được sử dụng lại nguyên trạng.
Christophe Roussy

4

Kết hợp ý tưởng từ Christoph và giả sử một vài phương pháp lặp không chuẩn trên mảng và đối tượng / hàm băm ( eachvà bạn bè), chúng ta có thể nhận được sự khác biệt được đặt, sự kết hợp và giao điểm theo thời gian tuyến tính trong tổng số khoảng 20 dòng:

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

Điều này giả định rằng eachfilterđược định nghĩa cho các mảng và chúng ta có hai phương thức tiện ích:

  • myUtils.keys(hash): trả về một mảng với các khóa của hàm băm

  • myUtils.select(hash, fnSelector, fnEvaluator): trả về một mảng với kết quả của việc gọi fnEvaluator các cặp khóa / giá trị mà fnSelectortrả về true.

select()được lấy cảm hứng từ Common Lisp, và chỉ đơn thuần filter()map()được cuộn thành một. (Sẽ tốt hơn nếu chúng được xác định trên đó Object.prototype, nhưng làm như vậy sẽ phá hủy jQuery, vì vậy tôi đã giải quyết cho các phương thức tiện ích tĩnh.)

Hiệu suất: Thử nghiệm với

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

cho hai bộ với 50.000 và 66.666 phần tử. Với những giá trị này, AB mất khoảng 75ms, trong khi liên hiệp và giao điểm mỗi đoạn khoảng 150ms. (Mac Safari 4.0, sử dụng Javascript Date để tính thời gian.)

Tôi nghĩ đó là phần thưởng xứng đáng cho 20 dòng mã.


1
bạn vẫn nên kiểm tra hasOwnProperty()ngay cả khi các yếu tố là số: nếu không, một cái gì đó giống như Object.prototype[42] = true;phương tiện 42không bao giờ có thể xảy ra trong tập kết quả
Christoph

Cho rằng có thể đặt 42 theo cách đó, nhưng liệu có trường hợp sử dụng bán thực tế nào mà có ai thực sự làm như vậy không? Nhưng đối với các chuỗi chung, tôi lưu ý - nó có thể dễ dàng xung đột với một số biến hoặc hàm Object.prototype.
jg-faustus

3

Sử dụng Underscore.js (Thư viện cho JS chức năng)

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

3

Một số chức năng đơn giản, mượn từ câu trả lời của @ 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]);

Sử dụng:

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

Đối với cách nhịn ăn, cách này không quá thanh lịch nhưng tôi đã chạy một số thử nghiệm để chắc chắn. Việc tải một mảng dưới dạng một đối tượng nhanh hơn nhiều để xử lý với số lượng lớn:

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

Các kết quả:

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

Tuy nhiên, điều này chỉ hoạt động với chuỗi . Nếu bạn định so sánh các tập hợp được đánh số, bạn sẽ muốn ánh xạ kết quả với parseFloat .


1
Nó không phải là c = b.filter(function(v) { return !A[v]; });trong hàm thứ hai?
fabianmoronzirfas

Bạn nói đúng. Bằng cách nào đó, nó dường như thậm chí còn nhanh hơn đối với tôi
SmujMaiku

1

Cái này hiệu quả, nhưng tôi nghĩ cái khác ngắn hơn nhiều và cũng trang nhã

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;
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.