Ilustración conceptual de conjuntos de muestras de puertas con coincidencias, diferencias y entradas repetidas.

Conjuntos: detectar duplicados y comparar colecciones

El catálogo esperaba D01 y D02. La entrega contiene D01, D01 y X99. Hay tres entradas para dos códigos esperados, así que un control basado en contar filas podría declarar que material no falta. Una conclusión magnífica si el objetivo era rellenar una hoja; algo menos brillante si necesitábamos comprobar qué había llegado.

Vamos a separar tres preguntas: qué códigos faltan, cuáles sobran y cuáles aparecen repetidos. Un conjunto permite trabajar con valores distintos y comparar pertenencias. La lista original seguirá siendo necesaria para localizar las filas responsables de una repetición. Me han encargado explicar cómo distinguir una cosa de otra; intentaré contener la emoción sin contener la información que el informe necesita conservar.

Tabla de contenidos

Al quedarte con valores únicos pierdes las repeticiones

Un conjunto reúne elementos sin repetirlos y permite preguntar si un valor pertenece a él. En Python utilizaremos set; en C#, HashSet<T>. No son listas con un aspecto más serio: no representan una secuencia de posiciones que debamos utilizar para identificar «el tercero». Si el proceso necesita conservar el orden de recepción, mantendremos la lista y utilizaremos el conjunto para la pregunta de pertenencia.

Partimos de códigos de texto válidos para el ejercicio. Crear un conjunto no valida su formato ni decide si X99 pertenece al catálogo. Tampoco convierte D01 y d01 en el mismo texto. Nuestra comparación distingue mayúsculas y minúsculas; espacios y otros caracteres siguen formando parte del valor. Normalizar entradas puede ser útil, pero requiere otra decisión que no deberíamos camuflar dentro de una operación que promete eliminar repeticiones.

				
					recibidos = ["D01", "D01", "X99"]
distintos = set(recibidos)
print(len(recibidos))
print(len(distintos))
print("D02" in distintos)
				
			

La salida es 3, 2 y False. Sabemos que había tres entradas y dos valores diferentes, y que D02 no aparece. El conjunto no conserva una anotación que diga en qué posiciones estaba D01 ni cuántas veces llegó. Para ese contexto necesitamos volver a la lista. El tipo de dato ha hecho exactamente lo que le pedimos; sería bastante injusto acusarlo después de no haber guardado la información que acabamos de decidir descartar.

Restar ambas longitudes informa de cuántas apariciones se han colapsado, no de cuántos valores distintos estaban repetidos. Con D01, D01, D01, la diferencia sería dos, pero solo hay un código repetido. Son cantidades relacionadas que responden a preguntas distintas. Antes de poner «duplicados: 2» en un informe, yo dejaría escrito qué está contando ese dos. Una etiqueta breve también puede equivocarse con mucha elegancia.

Las diferencias se calculan en los dos sentidos

Ahora representamos lo esperado mediante otro conjunto: D01 y D02. La diferencia entre esperados y recibidos devuelve los códigos que faltan; invertir los operandos devuelve los que no esperábamos recibir. La intersección contiene los códigos compartidos y la unión reúne todos los valores distintos presentes en cualquiera de los dos. Cada operación conserva una pregunta concreta y evita que el número de filas decida por nosotros.

				
					esperados = {"D01", "D02"}
recibidos = ["D01", "D01", "X99"]
distintos = set(recibidos)

print("Faltan:", ", ".join(sorted(esperados - distintos)))
print("Sobran:", ", ".join(sorted(distintos - esperados)))
print("Coinciden:", ", ".join(sorted(esperados & distintos)))
print("Union:", ", ".join(sorted(esperados | distintos)))
				
			

El informe muestra Faltan: D02, Sobran: X99, Coinciden: D01 y Union: D01, D02, X99. Utilizamos sorted después de calcular para presentar los valores en un orden estable. Ese orden pertenece a la salida que hemos decidido construir, no a una garantía del conjunto. Si una prueba necesita comparar resultados, puede comparar conjuntos directamente o fijar expresamente cómo se mostrarán.

La referencia de conjuntos de Python describe estas operaciones. El orden de la resta importa: «lo que falta» y «lo que sobra» intercambian su significado cuando cambiamos los operandos. Una intersección con un valor tampoco demuestra que la entrega sea correcta; en nuestro caso D01 coincide mientras D02 falta y X99 sobra. Los tres diagnósticos pueden ser ciertos a la vez.

Con una entrega vacía y un catálogo no vacío, todos los esperados faltarán. Con el catálogo vacío y una entrega con valores, todos sobrarán. Si ambos están vacíos, las diferencias quedarán vacías, pero la decisión de aceptar una entrega sin contenido sigue perteneciendo al proceso. No le daría al resultado de una resta de conjuntos el poder de aprobar un proyecto entero. Ya hemos delegado bastantes responsabilidades en una pareja de llaves.

Un segundo recorrido identifica qué valores se repiten

La diferencia de conjuntos no localiza las repeticiones porque trabaja precisamente con valores únicos. Para detectarlas recorreremos las entradas conservando los códigos ya vistos. Si el código actual estaba en ese conjunto, lo añadiremos a otro conjunto de repetidos. Así obtendremos los valores que aparecen más de una vez, sin convertir cada aparición adicional en un código nuevo del informe.

Después volveremos a recorrer la lista original para mostrar todas las filas cuyo código esté entre los repetidos. enumerate proporciona la posición junto al valor; empezaremos a contar por uno para presentar números de fila comprensibles en el ejercicio. Esos números describen esta lista concreta, no identifican elementos de un modelo ni prometen coincidir con la fila de una hoja que todavía no hemos leído.

				
					recibidos = ["D01", "D01", "X99"]
vistos = set()
repetidos = set()
for codigo in recibidos:
    if codigo in vistos:
        repetidos.add(codigo)
    vistos.add(codigo)

print("Repetidos:", ", ".join(sorted(repetidos)))
for fila, codigo in enumerate(recibidos, start=1):
    if codigo in repetidos:
        print("Fila", fila, codigo)
				
			

Obtendrás Repetidos: D01, Fila 1 D01 y Fila 2 D01. El segundo recorrido incluye también la primera aparición. Si hubiéramos guardado únicamente la fila que encontró el valor por segunda vez, faltaría parte del contexto para decidir cuál conservar o cómo corregir la fuente. Detectar un exceso y explicar todas las entradas relacionadas son trabajos diferentes; el informe debería reconocer cuál ha realizado.

Una tercera aparición de D01 añadiría otra fila a la salida, pero el conjunto de códigos repetidos seguiría conteniendo solo D01. Esa es la propiedad que buscamos. No eliminamos registros, ni elegimos el primero como correcto, ni decidimos que dos puertas con el mismo código sean necesariamente un error. En este ejemplo el catálogo exige códigos de entrada no repetidos; en otro proceso un mismo código de tipo podría corresponder legítimamente a muchas puertas.

La regla de negocio necesita aparecer antes de la corrección. Si veinte instancias comparten tipo, que su código se repita puede ser completamente normal. Un detector que borra todo lo que comparte una propiedad no ha limpiado un modelo: ha aplicado una interpretación que nadie revisó. El conjunto sirve para conocer pertenencias; el permiso para descartar información no viene incluido en la biblioteca estándar.

HashSet permite expresar las mismas preguntas en C#

El programa siguiente reproduce los resultados anteriores en una consola de .NET 8. Usamos StringComparer.Ordinal para mantener una comparación exacta de los códigos y creamos copias para calcular diferencia, intersección y unión. Los métodos ExceptWith, IntersectWith y UnionWith modifican el conjunto sobre el que se ejecutan. Reutilizar el catálogo original para todos los cálculos iría cambiando las siguientes preguntas a medida que intentamos responderlas.

				
					using System;
using System.Collections.Generic;

string[] recibidos = { "D01", "D01", "X99" };
var esperados = new HashSet<string>(new[] { "D01", "D02" }, StringComparer.Ordinal);
var distintos = new HashSet<string>(recibidos, StringComparer.Ordinal);
var faltan = new HashSet<string>(esperados, StringComparer.Ordinal);
faltan.ExceptWith(distintos);
var sobran = new HashSet<string>(distintos, StringComparer.Ordinal);
sobran.ExceptWith(esperados);
var coinciden = new HashSet<string>(esperados, StringComparer.Ordinal);
coinciden.IntersectWith(distintos);
var union = new HashSet<string>(esperados, StringComparer.Ordinal);
union.UnionWith(distintos);

var salidaFaltan = new List<string>(faltan);
var salidaSobran = new List<string>(sobran);
var salidaCoinciden = new List<string>(coinciden);
var salidaUnion = new List<string>(union);
salidaFaltan.Sort(StringComparer.Ordinal);
salidaSobran.Sort(StringComparer.Ordinal);
salidaCoinciden.Sort(StringComparer.Ordinal);
salidaUnion.Sort(StringComparer.Ordinal);
Console.WriteLine($"Faltan: {string.Join(", ", salidaFaltan)}");
Console.WriteLine($"Sobran: {string.Join(", ", salidaSobran)}");
Console.WriteLine($"Coinciden: {string.Join(", ", salidaCoinciden)}");
Console.WriteLine($"Union: {string.Join(", ", salidaUnion)}");

var vistos = new HashSet<string>(StringComparer.Ordinal);
var repetidos = new HashSet<string>(StringComparer.Ordinal);
foreach (string codigo in recibidos)
{
    if (!vistos.Add(codigo)) repetidos.Add(codigo);
}
var salidaRepetidos = new List<string>(repetidos);
salidaRepetidos.Sort(StringComparer.Ordinal);
Console.WriteLine($"Repetidos: {string.Join(", ", salidaRepetidos)}");
for (int i = 0; i < recibidos.Length; i += 1)
{
    if (repetidos.Contains(recibidos[i]))
        Console.WriteLine($"Fila {i + 1} {recibidos[i]}");
}
				
			

Las primeras cuatro líneas coinciden con la comparación de Python; las tres últimas, con el informe de repeticiones. Add devuelve falso cuando el código ya estaba en el HashSet, y Contains consulta su pertenencia. Para presentar las filas utilizamos un for con índice, porque aquí sí necesitamos la posición además del valor. El número mostrado es i + 1; el acceso al array sigue utilizando el índice desde cero.

El programa es más largo porque deja visibles las copias, el orden de presentación y los pasos del informe. Podríamos compactarlo con otras herramientas del lenguaje, pero entonces estaríamos enseñando varias abstracciones antes de haber entendido para qué necesitamos cada una. La referencia de HashSet de .NET permite contrastar sus operaciones. En una herramienta posterior separaríamos responsabilidades cuando ya sepamos qué conviene reutilizar.

Un informe útil conserva el camino de vuelta a la fuente

Ya sabemos que D02 falta, X99 sobra y D01 se repite en las dos primeras entradas. Esa salida orienta una revisión de la fuente sin inventar una corrección. En un archivo real conservaríamos también el identificador del registro o la localización que permita encontrarlo después de ordenar la tabla. El conjunto de códigos repetidos ayuda a seleccionar los casos; la lista y su contexto permiten explicar a quién afecta cada uno.

Prueba a añadir otra aparición de D01. Debe aparecer una tercera fila relacionada y seguir habiendo un único código distinto entre los repetidos. Después utiliza una lista vacía: los faltantes deben ser D01 y D02, y no habrá filas repetidas. Por último recibe exactamente D01 y D02 una vez cada uno; ambas diferencias y el conjunto de repetidos deben quedar vacíos. Son resultados que puedes anticipar antes de ejecutar y comparar después.

Si tienes que enviar esta revisión a otra persona, comparte el criterio junto al informe: qué catálogo se esperaba, cómo se compararon los códigos y qué significa «repetido» en ese proceso. Una captura que solo muestra un número no permite reconstruir esas decisiones. Nos ahorraríamos bastantes discusiones si los informes vinieran con esa mínima cortesía en lugar de una celda roja que exige obediencia.

El conjunto ha resuelto una parte acotada del trabajo: distinguir valores y comparar pertenencias. Conservar las entradas originales ha evitado perder el contexto para investigar. Esa combinación permite señalar qué revisar sin convertir «eliminar duplicados» en una orden de borrar datos a ciegas. El ordenador ya repite instrucciones suficientemente bien; no hace falta que también repita nuestras suposiciones sin dejarlas por escrito.

Artículos relacionados

Respuestas

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *