Qual é a maneira mais rápida de comparar dois conjuntos em Java?

102

Estou tentando otimizar um trecho de código que compara elementos de lista.

Por exemplo.

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

Por favor, leve em consideração que o número de registros em conjuntos será alto.

obrigado

Shekhar

Shekhar
fonte
7
Não é possível otimizar os loops sem conhecer (e modificar) a lógica de comparação. Você poderia mostrar mais do seu código?
Josefx

Respostas:

161
firstSet.equals(secondSet)

Realmente depende do que você deseja fazer na lógica de comparação ... ou seja, o que acontece se você encontrar um elemento em um conjunto e não no outro? Seu método tem um voidtipo de retorno, portanto, presumo que você fará o trabalho necessário neste método.

Controle mais refinado, se necessário:

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

Se você precisar obter os elementos que estão em um conjunto e não no outro.
EDIT: set.removeAll(otherSet)retorna um booleano, não um conjunto. Para usar removeAll (), você terá que copiar o conjunto e usá-lo.

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

Se os conteúdos de onee twoestiverem vazios, você saberá que os dois conjuntos são iguais. Se não, você tem os elementos que tornam os conjuntos desiguais.

Você mencionou que o número de registros pode ser alto. Se a implementação subjacente for um HashSet, a busca de cada registro é feita a O(1)tempo, então você não pode realmente ficar muito melhor do que isso. TreeSeté O(log n).

Noel M
fonte
3
A implementação de equals () e hashcode () para a classe Record é igualmente importante, ao invocar equals () no Conjunto.
Vineet Reynolds
1
Não tenho certeza se os exemplos removeAll () estão corretos. removeAll () retorna um booleano, não outro Set. Os elementos em secondSet são realmente removidos de firstSet e true é retornado se uma alteração foi feita.
Richard Corfield
4
O exemplo removeAll ainda não está certo porque você não fez cópias (Conjunto um = firstSet; Conjunto dois = secondSet). Eu usaria o construtor de cópia.
Michael Rusch
1
Na verdade, a implementação padrão de equalsé mais rápida do que duas chamadas para containsAllno pior caso; veja minha resposta.
Stephen C
6
Você precisa fazer Set one = new HashSet (firstSet), caso contrário, os itens de firstSet e secondSet serão removidos.
Bonton255 de
61

Se você simplesmente deseja saber se os conjuntos são iguais, o equalsmétodo on AbstractSeté implementado aproximadamente como abaixo:

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

Observe como ele otimiza os casos comuns em que:

  • os dois objetos são iguais
  • o outro objeto não é um conjunto, e
  • os tamanhos dos dois conjuntos são diferentes.

Depois disso, containsAll(...)retornará falseassim que encontrar um elemento no outro conjunto que também não esteja neste conjunto. Mas se todos os elementos estiverem presentes em ambos os conjuntos, será necessário testar todos eles.

O pior caso de desempenho, portanto, ocorre quando os dois conjuntos são iguais, mas não os mesmos objetos. Esse custo é normalmente O(N)ou O(NlogN)dependendo da implementação de this.containsAll(c).

E você obtém desempenho próximo do pior caso se os conjuntos forem grandes e diferirem apenas em uma pequena porcentagem dos elementos.


ATUALIZAR

Se você deseja investir tempo em uma implementação de conjunto customizado, há uma abordagem que pode melhorar o caso "quase o mesmo".

A ideia é que você precisa pré-calcular e armazenar em cache um hash para todo o conjunto, de modo que possa obter o valor do hashcode atual do conjunto O(1). Em seguida, você pode comparar o código hash para os dois conjuntos como uma aceleração.

Como você poderia implementar um hashcode assim? Bem, se o hashcode definido foi:

  • zero para um conjunto vazio, e
  • o XOR de todos os códigos hash do elemento para um conjunto não vazio,

então você poderia atualizar de forma barata o hashcode em cache do conjunto cada vez que você adicionasse ou removesse um elemento. Em ambos os casos, você simplesmente XOR o hashcode do elemento com o conjunto atual de hashcode.

Obviamente, isso pressupõe que os hashcodes do elemento são estáveis, enquanto os elementos são membros de conjuntos. Ele também assume que a função hashcode das classes de elemento oferece uma boa distribuição. Isso ocorre porque, quando os dois conjuntos de códigos de hash são iguais, você ainda precisa recorrer à O(N)comparação de todos os elementos.


Você poderia levar essa ideia um pouco mais longe ... pelo menos em teoria.

AVISO - Isso é altamente especulativo. Um "experimento mental", se quiser.

Suponha que sua classe de elemento definido tenha um método para retornar somas de verificação de criptografia para o elemento. Agora implemente as somas de verificação do conjunto aplicando um XOR nas somas de verificação retornadas para os elementos.

O que isso nos compra?

Bem, se assumirmos que nada secreto está acontecendo, a probabilidade de que quaisquer dois elementos de conjunto desiguais tenham as mesmas somas de verificação de N bits é 2 -N . E a probabilidade de 2 conjuntos desiguais terem as mesmas somas de verificação de N bits também é 2 -N . Então, minha ideia é que você pode implementar equalscomo:

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

De acordo com as premissas acima, isso só dará a resposta errada uma vez no tempo 2- N . Se você tornar N grande o suficiente (por exemplo, 512 bits), a probabilidade de uma resposta errada torna-se insignificante (por exemplo, aproximadamente 10 -150 ).

A desvantagem é que calcular as somas de verificação de criptografia para os elementos é muito caro, especialmente à medida que o número de bits aumenta. Portanto, você realmente precisa de um mecanismo eficaz para memorizar as somas de verificação. E isso pode ser problemático.

E a outra desvantagem é que uma probabilidade diferente de zero de erro pode ser inaceitável, não importa quão pequena seja a probabilidade. (Mas se for esse o caso ... como você lida com o caso em que um raio cósmico vira um bit crítico? Ou se ele simultaneamente vira o mesmo bit em duas instâncias de um sistema redundante?)

Stephen C
fonte
Deve ser if (checksumsDoNotMatch (0)) return false; else return doHeavyComparisonToMakeSureTheSetsReallyMatch (o);
Esko Piirainen
Não necessariamente. Se a probabilidade de duas somas de verificação corresponderem a conjuntos não iguais é pequena o suficiente, suponho que você pode pular a comparação. Faça as contas.
Stephen C de
17

Existe um método no Goiaba Setsque pode ajudar aqui:

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

Você tem a seguinte solução em 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);
}

Ou se você preferir usar uma única instrução de retorno:

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

  return set1 != null 
    && set2 != null 
    && set1.size() == set2.size() 
    && set1.containsAll(set2);
}
ilopezluna
fonte
Ou talvez simplesmente use o equals()método from AbstractSet(fornecido com o JDK), que é quase o mesmo que a solução aqui, exceto para as verificações de nulos adicionais . Java-11 Set Interface
Chaithu Narayana
4

Existe uma solução O (N) para casos muito específicos onde:

  • os conjuntos são classificados
  • ambos classificados na mesma ordem

O código a seguir assume que ambos os conjuntos são baseados em registros comparáveis. Um método semelhante pode ser baseado em um Comparador.

    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;
        }
    }
Philip Couling
fonte
3

Se você estiver usando uma Guavabiblioteca, é possível fazer:

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

E então faça uma conclusão com base nisso.

Riwnodennyk
fonte
2

Eu colocaria o secondSet em um HashMap antes da comparação. Desta forma, você reduzirá o tempo de pesquisa da segunda lista para n (1). Como isso:

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
    }
}
Sahin Habesoglu
fonte
Ou você pode usar array em vez de um hashmap para a segunda lista.
Sahin Habesoglu
E, esta solução assume que os conjuntos não são classificados.
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;
    }
Zahran
fonte
-1

Eu acho que a referência do método com o método equals pode ser usada. Assumimos que o tipo de objeto sem sombra de dúvida tem seu próprio método de comparação. Um exemplo claro e simples está aqui,

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
snr
fonte
1
esta é uma maneira complicada de dizerset.equals(set2)
Alex