Який найшвидший спосіб порівняти два набори на Java?


102

Я намагаюся оптимізувати фрагмент коду, який порівнює елементи списку.

Напр.

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

Будь ласка, врахуйте, що кількість записів у наборах буде високою.

Дякую

Шехар


7
Неможливо оптимізувати цикли, не знаючи (і змінюючи) логіку порівняння. Чи можете ви показати більше свого коду?
josefx

Відповіді:


161
firstSet.equals(secondSet)

Це дійсно залежить від того, що ви хочете зробити в логіці порівняння ... тобто що станеться, якщо ви знайдете елемент в одному наборі, а не в іншому? Ваш метод має voidтип повернення, тому я припускаю, що ви виконаєте необхідну роботу в цьому методі.

Більш тонкий контроль, якщо він вам потрібен:

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

Якщо вам потрібно отримати елементи, які знаходяться в одному наборі, а не в іншому.
EDIT: set.removeAll(otherSet)повертає булевий, а не набір. Щоб використовувати removeAll (), вам доведеться скопіювати набір, а потім використовувати його.

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

Якщо вміст oneі twoобидва порожні, то ви знаєте, що два набори були рівними. Якщо ні, то ви отримали елементи, які зробили набори нерівними.

Ви згадали, що кількість записів може бути великою. Якщо основна реалізація - це HashSetтоді, витяг кожного запису виконується O(1)вчасно, тому ви не можете дійсно отримати набагато краще, ніж це. TreeSetє O(log n).


3
Реалізація рівнянь () та хеш-коду () для класу Record однаково важлива при виклику рівнянь () на множині.
Vineet Reynolds

1
Я не впевнений, що приклади removeAll () є правильними. removeAll () повертає булевий, а не інший Set. Елементи в secondSet фактично видаляються з firstSet і повертається true, якщо було внесено зміни.
Річард Корфілд

4
Приклад RemoveAll як і раніше неправильний, оскільки ви не зробили копії (Set one = firstSet; Set two = secondSet). Я б скористався конструктором копій.
Майкл Раш

1
Насправді реалізація за замовчуванням equalsшвидше, ніж два дзвінки containsAllв гіршому випадку; дивіться мою відповідь.
Стівен C

6
Вам потрібно зробити Встановити один = новий HashSet (firstSet), інакше елементи з firstSet і secondSet будуть видалені.
Bonton255

61

Якщо ви просто хочете знати, чи множини рівні, equalsметод on AbstractSetреалізується приблизно, як показано нижче:

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

Зверніть увагу, як вона оптимізує поширені випадки, коли:

  • два об'єкти однакові
  • інший об'єкт зовсім не є набором, і
  • розміри двох комплектів різні.

Після цього containsAll(...)повернеться, falseяк тільки знайде елемент в іншому наборі, якого також немає в цьому наборі. Але якщо всі елементи присутні в обох наборах, потрібно буде перевірити їх усі.

Отже, найгірша ефективність має місце, коли два набори рівні, але не однакові об'єкти. Ці витрати , як правило , O(N)або в O(NlogN)залежності від реалізації this.containsAll(c).

І ви отримуєте близькі до найгірших показників, якщо набори великі і відрізняються лише невеликим відсотком елементів.


ОНОВЛЕННЯ

Якщо ви готові інвестувати час у реалізацію спеціального набору, існує підхід, який може покращити «майже той самий» випадок.

Ідея полягає в тому, що вам потрібно попередньо обчислити і кешувати хеш для всього набору, щоб ви могли отримати поточне значення хеш-коду набору в O(1). Потім ви можете порівняти хеш-код для двох наборів як прискорення.

Як ти міг реалізувати такий хеш-код? Добре, якщо встановлений хеш-код був:

  • нуль для порожнього набору, і
  • XOR усіх хеш-кодів елементів для порожнього набору,

тоді ви зможете дешево оновити кешований хеш-код набору щоразу, коли ви додавали або видаляли елемент. В обох випадках ви просто XOR хеш-код елемента з поточним встановленим хеш-кодом.

Звичайно, це передбачає, що хеш-коди елементів є стабільними, тоді як елементи є членами множин. Він також передбачає, що функція хеш-коду класів елементів дає гарне поширення. Це тому, що коли два встановлені хеш-коди однакові, вам все одно доведеться повернутися до O(N)порівняння всіх елементів.


Ви можете взяти цю ідею трохи далі ... принаймні теоретично.

ПОПЕРЕДЖЕННЯ - Це дуже спекулятивно. "Думковий експеримент", якщо вам подобається.

Припустимо, у вашому класі встановлених елементів є метод повернення криптовалют контрольних сум для елемента. Тепер реалізуйте контрольні суми набору шляхом XORing контрольних сум, повернутих для елементів.

Що це купує у нас?

Ну, якщо припустити, що нічого не відбувається, вірогідність того, що будь-які два нерівні множинні елементи мають однакові N-бітові контрольні суми, є 2 -N . І ймовірність 2 неоднакових множин мають однакові N-бітові контрольні суми також 2 -N . Тож моя ідея полягає в тому, що ви можете реалізувати equalsяк:

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

Згідно з припущеннями, наведеними вище, це дасть вам неправильну відповідь лише один раз у 2- N час. Якщо ви зробите N досить великим (наприклад, 512 біт), ймовірність неправильної відповіді стає незначною (наприклад, приблизно 10 -150 ).

Мінус полягає в тому, що обчислення криптовалют для елементів є дуже дорогим, тим більше, що збільшується кількість біт. Тож вам справді потрібен дієвий механізм запам'ятовування контрольних сум. І це може бути проблематично.

І інший недолік полягає в тому, що ненульова ймовірність помилки може бути неприйнятною, незалежно від того, наскільки ймовірною є мала. (Але якщо це так ... як ви ставитеся до випадку, коли космічний промінь перевертає критичний біт? Або якщо він одночасно перевертає той самий біт у двох випадках надмірної системи?)


Це повинно бути, якщо (checkumsDoNotMatch (0)) повернути помилково; інакше повернути doHeavyComppareToMakeSureTheSetsReallyMatch (o);
Есько Пірайнен

Не обов'язково. Якщо ймовірність відповідності двох контрольних сум для нерівних множин, я маю на увазі, що ви можете пропустити порівняння. Зробіть математику.
Стівен C

17

У Гуаві є метод, Setsякий тут може допомогти:

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

5

Ви маєте таке рішення від 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);
}

Або якщо ви віддаєте перевагу використовувати одне повернення:

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

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

А може просто скористатися equals()методом AbstractSet(поставляється разом з JDK), який майже такий самий, як рішення тут, за винятком додаткових перевірок нуля . Набір інтерфейсу Java-11
Chaithu Narayana

4

Є рішення O (N) для дуже конкретних випадків, коли:

  • набори обидва сортуються
  • обидва відсортовані в одному порядку

Наступний код передбачає, що обидва набори засновані на записах, порівнянні. Аналогічний метод може базуватися на порівнянні.

    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

Якщо ви використовуєте Guavaбібліотеку, можна зробити:

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

А потім зробіть на цьому висновок.


2

Я б поставив secondSet в HashMap перед порівнянням. Таким чином ви скоротите час пошуку другого списку до n (1). Подобається це:

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

Або ви можете використовувати масив замість хешмапу для другого списку.
Сахін Хабесоглу

І це рішення передбачає, що набори не сортуються.
Сахін Хабесоглу

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

Я думаю, що посилання методу з рівним методом може бути використане. Ми припускаємо, що тип об'єкта без тіні сумнівів має власний метод порівняння. Простий і простий приклад тут

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
це складний спосіб сказатиset.equals(set2)
Алекс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.