java - repetidos - Encontrar duplicados en una lista ignorando un campo
eliminar elementos repetidos de una lista java (4)
Tengo una List
de personas y quiero encontrar entradas duplicadas, consiguiendo todos los campos, excepto el id
. Entonces usando el método equals()
(y en consecuencia List.contains()
), porque toman en cuenta id
.
public class Person {
private String firstname, lastname;
private int age;
private long id;
}
La modificación de equals()
y hashCode()
-methods para ignorar el campo id
no es una opción porque otras partes del código se basan en esto.
¿Cuál es la forma más eficiente en Java para ordenar los duplicados si quiero ignorar el campo de id
?
Cree un Comparator<Person>
para implementar su ordenamiento de clave natural y luego use una deduplicación basada en búsqueda binaria. TreeSet
te dará esta habilidad de la caja.
Tenga en cuenta que el Comparator<T>.compare(a, b)
debe cumplir con los requisitos habituales de antisimetría, transitividad, coherencia y reflexividad o que el orden de búsqueda binaria fallará. También debe hacer que sea nulo-aware (por ejemplo, si el primer campo de uno, otro o ambos son nulos).
Un comparador de claves naturales simple para su clase Persona es el siguiente (es una clase de miembro estática ya que no se ha mostrado si tiene accesadores para cada campo).
public class Person {
public static class NkComparator implements Comparator<Person>
{
public int compare(Person p1, Person p2)
{
if (p1 == null || p2 == null) throw new NullPointerException();
if (p1 == p2) return 0;
int i = nullSafeCompareTo(p1.firstname, p2.firstname);
if (i != 0) return i;
i = nullSafeCompareTo(p1.lastname, p2.lastname);
if (i != 0) return i;
return p1.age - p2.age;
}
private static int nullSafeCompareTo(String s1, String s2)
{
return (s1 == null)
? (s2 == null) ? 0 : -1
: (s2 == null) ? 1 : s1.compareTo(s2);
}
}
private String firstname, lastname;
private int age;
private long id;
}
Luego puede usarlo para generar una lista única. Use el método add
que devuelve true
si y solo si el elemento no existía en el conjunto:
List<Person> newList = new ArrayList<Person>();
TreeSet<Person> nkIndex = new TreeSet<Person>(new Person.NkComparator());
for (Person p : originalList)
if (nkIndex.add(p)) newList.add(p); // to generate a unique list
o cambie la línea final de esta línea para dar salida a los duplicados en su lugar
if (nkIndex.add(p)) newList.add(p);
Hagas lo que hagas, no uses remove
en tu lista original mientras lo estás enumerando, es por eso que estos métodos agregan tus elementos únicos a una nueva lista.
Si solo está interesado en una lista única, y quiere usar la menor cantidad de líneas posible:
TreeSet<Person> set = new TreeSet<Person>(new Person.NkComparator());
set.addAll(originalList);
List<Person> newList = new ArrayList<Person>(set);
Puede usar Java HashMap
usando pares <K,V>
. Map<K,V> map = new HashMap<K,V>()
. Además, alguna forma de implementación de Comparator para ir con. Si comprueba con los métodos containsKey o containsValue y descubre que ya tiene algo (es decir, está intentando agregar un duplicado, guárdelo en su lista original. De lo contrario, extráigalos. De esta forma, terminará con una lista con los elementos que fueron duplicados en su lista original. TreeSet <,> será otra opción, pero aún no la he usado, así que no puedo ofrecer consejos.
Aconsejaría no usar un Comparator
para hacer esto. Es bastante difícil escribir un método legal de compare()
basado en los otros campos.
Creo que una mejor solución sería crear una clase PersonWithoutId
como tal:
public PersonWithoutId {
private String firstname, lastname;
private int age;
// no id field
public PersonWithoutId(Person original) { /* copy fields from Person */ }
@Overrides public boolean equals() { /* compare these 3 fields */ }
@Overrides public int hashCode() { /* hash these 3 fields */ }
}
Luego, dada una List<Person>
llamada people
puede hacer esto:
Set<PersonWithoutId> set = new HashSet<>();
for (Iterator<Person> i = people.iterator(); i.hasNext();)
if (!set.add(new PersonWithoutId(i.next())))
i.remove();
Editar
Como otros han señalado en los comentarios, esta solución no es ideal, ya que crea una carga de objetos para que el recolector de basura pueda manejar. Pero esta solución es mucho más rápida que una solución que utiliza un Comparator
y un TreeSet
. Mantener un Set
en orden lleva tiempo y no tiene nada que ver con el problema original. Probé esto en List
s de 1,000,000 instancias de Person
construidas usando
new Person(
"" + rand.nextInt(500), // firstname
"" + rand.nextInt(500), // lastname
rand.nextInt(100), // age
rand.nextLong()) // id
y descubrió que esta solución es aproximadamente el doble de rápida que una solución que utiliza un TreeSet
. (Es cierto que utilicé System.nanoTime()
lugar de benchmarking adecuado).
Entonces, ¿cómo puedes hacer esto de manera eficiente sin crear montones de objetos innecesarios? Java no lo hace fácil. Una forma sería escribir dos nuevos métodos en Person
boolean equalsIgnoringId(Person other) { ... }
int hashCodeIgnoringId() { ... }
y luego escribir una implementación personalizada de Set<Person>
donde básicamente corta y pega el código para HashSet
excepto que reemplaza equals()
y hashCode()
por equalsIgnoringId()
y hashCodeIgnoringId()
.
En mi humilde opinión, el hecho de que pueda crear un TreeSet
que use un Comparator
, pero no un HashSet
que use versiones personalizadas de equals
/ hashCode
es un defecto grave en el lenguaje.
Como @LuiggiMendoza sugirió en los comentarios:
Puede crear una clase Comparator
personalizada que compare dos objetos Person
para igualdad, ignorando sus identificadores.
class PersonComparator implements Comparator<Person> {
// wraps the compareTo method to compare two Strings but also accounts for NPE
int compareStrings(String a, String b) {
if(a == b) { // both strings are the same string or are null
return 0;
} else if(a == null) { // first string is null, result is negative
return -1;
} else if(b == null){ // second string is null, result is positive
return 1;
} else { // no strings are null, return the result of compareTo
return a.compareTo(b);
}
}
@Override
public int compare(Person p1, Person p2) {
// comparisons on Person objects themselves
if(p1 == p2) { // Person 1 and Person 2 are the same Person object
return 0;
}
if(p1 == null && p2 != null) { // Person 1 is null and Person 2 is not, result is negative
return -1;
}
if(p1 != null && p2 == null) { // Person 1 is not null and Person 2 is, result is positive
return 1;
}
int result = 0;
// comparisons on the attributes of the Persons objects
result = compareStrings(p1.firstname, p2.firstname);
if(result != 0) { // Persons differ in first names, we can return the result
return result;
}
result = compareStrings(p1.lastname, p2.lastname);
if(result != 0) { // Persons differ in last names, we can return the result
return result;
}
return Integer.compare(p1.age, p2.age); // if both first name and last names are equal, the comparison difference is in their age
}
}
Ahora puede usar la estructura TreeSet
con este Comparator
personalizado y, por ejemplo, crear un método simple que elimine los valores duplicados.
List<Person> getListWithoutDups(List<Person> list) {
List<Person> newList = new ArrayList<Person>();
TreeSet<Person> set = new TreeSet<Person>(new PersonComparator()); // use custom Comparator here
// foreach Person in the list
for(Person person : list) {
// if the person isn''t already in the set (meaning it''s not a duplicate)
// add it to the set and the new list
if(!set.contains(person)) {
set.add(person);
newList.add(person);
}
// otherwise it''s a duplicate so we don''t do anything
}
return newList;
}
La operación contains
en TreeSet
, como dice la documentación , "proporciona un costo de tiempo de registro (n) garantizado" .
El método que sugerí anteriormente toma el tiempo O(n*log(n))
ya que estamos realizando la operación contains
en cada elemento de lista, pero también usa O(n)
espacio para crear una nueva lista y TreeSet
.
Si su lista es bastante grande (el espacio es bastante importante) pero su velocidad de procesamiento no es un problema, entonces, en lugar de agregar cada uno que no sea duplicado a la lista, puede eliminar cada duplicado que se encuentre:
List<Person> getListWithoutDups(List<Person> list) {
TreeSet<Person> set = new TreeSet<Person>(new PersonComparator()); // use custom Comparator here
Person person;
// for every Person in the list
for(int i = 0; i < list.size(); i++) {
person = list.get(i);
// if the person is already in the set (meaning it is a duplicate)
// remove it from the list
if(set.contains(person) {
list.remove(i);
i--; // make sure to accommodate for the list shifting after removal
}
// otherwise add it to the set of non-duplicates
else {
set.add(person);
}
}
return list;
}
Dado que cada operación de remove
en una lista toma O(n)
tiempo (porque la lista se desplaza cada vez que se elimina un elemento), y cada operación contains
log(n)
, este enfoque sería O(n^2 log(n))
a tiempo.
Sin embargo, la complejidad del espacio se reduciría a la mitad, ya que solo creamos el TreeSet
y no la segunda lista.