Cách nhanh nhất để so sánh hai tập hợp trong Java là gì?


101

Tôi đang cố gắng tối ưu hóa một đoạn mã so sánh các phần tử của danh sách.

Ví dụ.

public void compare(Set<Record> firstSet, Set<Record> secondSet){
    for(Record firstRecord : firstSet){
        for(Record secondRecord : secondSet){
            // comparing logic
        }
    }
}

Vui lòng lưu ý rằng số lượng bản ghi trong bộ sẽ nhiều.

Cảm ơn

Shekhar


7
Không thể tối ưu hóa các vòng lặp mà không biết (và sửa đổi) logic so sánh. Bạn có thể hiển thị thêm mã của bạn?
josefx,

Câu trả lời:


160
firstSet.equals(secondSet)

Nó thực sự phụ thuộc vào những gì bạn muốn làm trong logic so sánh ... tức là điều gì sẽ xảy ra nếu bạn tìm thấy một phần tử trong một tập hợp này không nằm trong tập hợp kia? Phương thức của bạn có voidkiểu trả về nên tôi cho rằng bạn sẽ thực hiện công việc cần thiết trong phương thức này.

Kiểm soát chi tiết hơn nếu bạn cần:

if (!firstSet.containsAll(secondSet)) {
  // do something if needs be
}
if (!secondSet.containsAll(firstSet)) {
  // do something if needs be
}

Nếu bạn cần lấy các phần tử nằm trong một tập hợp chứ không phải tập hợp khác.
EDIT: set.removeAll(otherSet)trả về một boolean, không phải một tập hợp. Để sử dụng removeAll (), bạn sẽ phải sao chép tập hợp sau đó sử dụng nó.

Set one = new HashSet<>(firstSet);
Set two = new HashSet<>(secondSet);
one.removeAll(secondSet);
two.removeAll(firstSet);

Nếu nội dung của onetwođều trống, thì bạn biết rằng hai tập hợp là bằng nhau. Nếu không, thì bạn đã có các phần tử làm cho các tập hợp không bằng nhau.

Bạn đã đề cập rằng số lượng bản ghi có thể cao. Nếu triển khai cơ bản là a HashSetthì việc tìm nạp từng bản ghi được thực hiện O(1)đúng lúc, vì vậy bạn không thể thực sự tốt hơn nhiều. TreeSetO(log n).


3
Việc triển khai bằng () và mã băm () cho lớp Bản ghi cũng quan trọng như nhau, khi gọi bằng () trên Tập hợp.
Vineet Reynolds

1
Tôi không chắc rằng các ví dụ removeAll () là đúng. removeAll () trả về một boolean, không phải một Set khác. Các phần tử trong secondSet thực sự bị xóa khỏi firstSet và true được trả về nếu thay đổi đã được thực hiện.
Richard Corfield

4
Ví dụ removeAll vẫn không đúng vì bạn chưa tạo bản sao (Đặt một = bộ đầu tiên; Đặt hai = bộ thứ hai). Tôi sẽ sử dụng hàm tạo bản sao.
Michael Rusch

1
Trên thực tế, việc triển khai mặc định equalsnhanh hơn hai lệnh gọi containsAlltrong trường hợp xấu nhất; xem câu trả lời của tôi.
Stephen C

6
Bạn cần thực hiện Đặt một = new HashSet (firstSet), nếu không các mục từ firstSet và secondSet sẽ bị xóa.
Bonton255

61

Nếu bạn chỉ muốn biết liệu các tập hợp có bằng nhau hay không, equalsphương thức trên AbstractSetđược thực hiện gần như như sau:

    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Set))
            return false;
        Collection c = (Collection) o;
        if (c.size() != size())
            return false;
        return containsAll(c);
    }

Lưu ý cách nó tối ưu hóa các trường hợp phổ biến trong đó:

  • hai đối tượng giống nhau
  • đối tượng kia hoàn toàn không phải là một tập hợp, và
  • kích thước của hai bộ là khác nhau.

Sau đó, containsAll(...)sẽ trả về falsengay khi nó tìm thấy một phần tử trong tập hợp khác mà cũng không nằm trong tập hợp này. Nhưng nếu tất cả các phần tử có mặt trong cả hai tập hợp, nó sẽ cần phải kiểm tra tất cả chúng.

Do đó, hiệu suất trường hợp xấu nhất xảy ra khi hai tập hợp bằng nhau nhưng không cùng các đối tượng. Chi phí đó thường O(N)hoặc O(NlogN)tùy thuộc vào việc thực hiện this.containsAll(c).

Và bạn sẽ có được hiệu suất gần như trường hợp xấu nhất nếu các tập hợp lớn và chỉ khác nhau ở một tỷ lệ phần trăm nhỏ của các phần tử.


CẬP NHẬT

Nếu bạn sẵn sàng đầu tư thời gian vào việc triển khai tập hợp tùy chỉnh, có một cách tiếp cận có thể cải thiện trường hợp "gần như giống nhau".

Ý tưởng là bạn cần tính toán trước và lưu vào bộ nhớ cache một mã băm cho toàn bộ tập hợp để bạn có thể nhận được giá trị mã băm hiện tại của tập hợp O(1). Sau đó, bạn có thể so sánh mã băm cho hai bộ như một gia tốc.

Làm thế nào bạn có thể triển khai một mã băm như vậy? Chà nếu mã băm đã đặt là:

  • 0 cho một tập hợp trống và
  • XOR của tất cả các mã băm phần tử cho một tập hợp không trống,

sau đó, bạn có thể cập nhật mã băm được lưu trong bộ nhớ cache của tập hợp mỗi lần bạn thêm hoặc xóa một phần tử. Trong cả hai trường hợp, bạn chỉ cần XOR mã băm của phần tử với mã băm được đặt hiện tại.

Tất nhiên, điều này giả định rằng mã băm phần tử ổn định trong khi các phần tử là thành viên của tập hợp. Nó cũng giả định rằng hàm băm của các lớp phần tử tạo ra một sự lây lan tốt. Đó là bởi vì khi hai mã băm được đặt giống nhau, bạn vẫn phải quay lại O(N)so sánh tất cả các phần tử.


Bạn có thể đưa ý tưởng này xa hơn một chút ... ít nhất là trên lý thuyết.

CẢNH BÁO - Điều này mang tính đầu cơ cao. Một "thử nghiệm suy nghĩ" nếu bạn thích.

Giả sử rằng lớp phần tử tập hợp của bạn có một phương thức trả về tổng kiểm tra tiền điện tử cho phần tử. Bây giờ triển khai các tổng kiểm tra của tập hợp bằng cách XOR các tổng kiểm tra được trả về cho các phần tử.

Cái này mua chúng ta cái gì?

Chà, nếu chúng ta giả định rằng không có gì ngầm đang xảy ra, thì xác suất để hai phần tử tập hợp không bằng nhau bất kỳ có cùng tổng kiểm tra N-bit là 2 -N . Và xác suất 2 tập không bằng nhau có tổng kiểm tra N bit giống nhau cũng là 2 -N . Vì vậy, ý tưởng của tôi là bạn có thể thực hiện equalsnhư:

    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Set))
            return false;
        Collection c = (Collection) o;
        if (c.size() != size())
            return false;
        return checksums.equals(c.checksums);
    }

Theo giả định ở trên, điều này sẽ chỉ cung cấp cho bạn câu trả lời sai một lần trong thời gian 2 -N . Nếu bạn tạo N đủ lớn (ví dụ 512 bit) thì xác suất trả lời sai trở nên không đáng kể (ví dụ: khoảng 10 -150 ).

Nhược điểm là việc tính toán tổng kiểm tra tiền điện tử cho các phần tử rất tốn kém, đặc biệt là khi số lượng bit tăng lên. Vì vậy, bạn thực sự cần một cơ chế hiệu quả để ghi nhớ tổng kiểm tra. Và đó có thể là vấn đề.

Và nhược điểm khác là xác suất sai số khác 0 có thể không được chấp nhận cho dù xác suất đó có nhỏ đến đâu. (Nhưng nếu đúng như vậy thì ... làm thế nào để đối phó với trường hợp một tia vũ trụ lật một bit tới hạn? Hoặc nếu nó đồng thời lật cùng một bit trong hai trường hợp của một hệ thống dư thừa?)


Nó phải là if (checksumsDoNotMatch (0)) trả về false; khác trả về doHeavyComparisonToMakeSureTheSetsReallyMatch (o);
Esko Piirainen

Không cần thiết. Nếu xác suất của hai tổng kiểm tra khớp cho các tập hợp không bằng nhau, thì tôi cho rằng bạn có thể bỏ qua phép so sánh. Làm toán.
Stephen C

17

Có một phương pháp trong Guava Setscó thể giúp ích ở đây:

public static <E>  boolean equals(Set<? extends E> set1, Set<? extends E> set2){
return Sets.symmetricDifference(set1,set2).isEmpty();
}

5

Bạn có giải pháp sau từ https://www.mkyong.com/java/java-how-to-compare-two-sets/

public static boolean equals(Set<?> set1, Set<?> set2){

    if(set1 == null || set2 ==null){
        return false;
    }

    if(set1.size() != set2.size()){
        return false;
    }

    return set1.containsAll(set2);
}

Hoặc nếu bạn muốn sử dụng một câu lệnh trả lại:

public static boolean equals(Set<?> set1, Set<?> set2){

  return set1 != null 
    && set2 != null 
    && set1.size() == set2.size() 
    && set1.containsAll(set2);
}

Hoặc có thể chỉ cần sử dụng equals()phương pháp from AbstractSet(được vận chuyển với JDK) gần giống như giải pháp ở đây ngoại trừ các kiểm tra rỗng bổ sung . Giao diện bộ Java-11
Chaithu Narayana

4

Có một giải pháp O (N) cho các trường hợp rất cụ thể trong đó:

  • cả hai bộ đều được sắp xếp
  • cả hai được sắp xếp theo cùng một thứ tự

Đoạn mã sau giả định rằng cả hai bộ đều dựa trên các bản ghi có thể so sánh được. Một phương pháp tương tự có thể dựa trên Bộ so sánh.

    public class SortedSetComparitor <Foo extends Comparable<Foo>> 
            implements Comparator<SortedSet<Foo>> {

        @Override
        public int compare( SortedSet<Foo> arg0, SortedSet<Foo> arg1 ) {
            Iterator<Foo> otherRecords = arg1.iterator();
            for (Foo thisRecord : arg0) {
                // Shorter sets sort first.
                if (!otherRecords.hasNext()) return 1;
                int comparison = thisRecord.compareTo(otherRecords.next());
                if (comparison != 0) return comparison;
            }
            // Shorter sets sort first
            if (otherRecords.hasNext()) return -1;
            else return 0;
        }
    }

3

Nếu bạn đang sử dụng Guavathư viện, bạn có thể thực hiện:

        SetView<Record> added = Sets.difference(secondSet, firstSet);
        SetView<Record> removed = Sets.difference(firstSet, secondSet);

Và sau đó đưa ra kết luận dựa trên những điều này.


2

Tôi sẽ đặt SecondSet trong HashMap trước khi so sánh. Bằng cách này, bạn sẽ giảm thời gian tìm kiếm của danh sách thứ hai xuống n (1). Như thế này:

HashMap<Integer,Record> hm = new HashMap<Integer,Record>(secondSet.size());
int i = 0;
for(Record secondRecord : secondSet){
    hm.put(i,secondRecord);
    i++;
}
for(Record firstRecord : firstSet){
    for(int i=0; i<secondSet.size(); i++){
    //use hm for comparison
    }
}

Hoặc bạn có thể sử dụng mảng thay vì một bản đồ băm cho danh sách thứ hai.
Sahin Habesoglu,

Và, giải pháp này giả định rằng các tập hợp không được sắp xếp.
Sahin Habesoglu,

1
public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Set))
            return false;

        Set<String> a = this;
        Set<String> b = o;
        Set<String> thedifference_a_b = new HashSet<String>(a);


        thedifference_a_b.removeAll(b);
        if(thedifference_a_b.isEmpty() == false) return false;

        Set<String> thedifference_b_a = new HashSet<String>(b);
        thedifference_b_a.removeAll(a);

        if(thedifference_b_a.isEmpty() == false) return false;

        return true;
    }

-1

Tôi nghĩ rằng phương pháp tham chiếu với phương thức bằng có thể được sử dụng. Chúng tôi giả định rằng kiểu đối tượng không có chút nghi ngờ nào có phương pháp so sánh riêng. Ví dụ đơn giản và dễ hiểu là ở đây,

Set<String> set = new HashSet<>();
set.addAll(Arrays.asList("leo","bale","hanks"));

Set<String> set2 = new HashSet<>();
set2.addAll(Arrays.asList("hanks","leo","bale"));

Predicate<Set> pred = set::equals;
boolean result = pred.test(set2);
System.out.println(result);   // true

1
đây là một cách phức tạp để nóiset.equals(set2)
Alex
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.