Ir al contenido
wannacode

Dictionary y HashSet

En resumen: Un mapa (diccionario) guarda pares de clave y valor con claves únicas y encuentra rápido un valor por su clave, y un conjunto guarda valores únicos.

Problema

¿Quién es este usuario?

Los usuarios del sitio están en una lista. En cada inicio de sesión buscas el correo por el nombre de usuario:

static string FindEmail(List<User> users, string username)
{
foreach (User u in users)
{
if (u.Username == username)
{
return u.Email;
}
}
return null;
}

Con tres usuarios es instantáneo. Con 2 000 000, cada inicio de sesión compara hasta 2 000 000 nombres, y hay miles de inicios de sesión por minuto. Una guía telefónica no se lee desde la primera página. Vas directo a la letra.

La idea

Diccionario: clave → valor

Quieres preguntar «¿cuál es el correo de garcia?» y obtener la respuesta al instante. La colección para esto se llama diccionario (mapa). Guarda pares: una clave y un valor. En C# el diccionario es Dictionary:

Dictionary<string, string> emails = new Dictionary<string, string>();
emails["garcia"] = "garcia@example.com";
emails["mlopez"] = "mlopez@example.com";
emails["fernandez"] = "fernandez@example.com";
Console.WriteLine(emails["fernandez"]);
Console.WriteLine(emails.ContainsKey("nadie"));
Console.WriteLine("Usuarios: " + emails.Count);
Salida
fernandez@example.com
False
Usuarios: 3
  • Hay dos parámetros de tipo: uno para la clave y otro para el valor. Dictionary<string, string> es «de texto a texto».
  • emails[clave] = valor guarda un par, y emails[clave] devuelve el valor.
  • ContainsKey(clave) pregunta si la clave está, y Count es el número de pares.
  • Dictionary está en System.Collections.Generic, igual que List. No necesitas un using: viene incluido en todo programa nuevo.

emails[clave] para una clave que no está no da null. Lanza KeyNotFoundException. Cuando no sabes si la clave está, pregunta con TryGetValue: devuelve si encontró la clave y te da el valor a través de out. También puedes escribir un diccionario de una vez, con pares [clave] = valor:

Dictionary<string, string> emails = new Dictionary<string, string>
{
["garcia"] = "garcia@example.com",
["mlopez"] = "mlopez@example.com",
};
if (emails.TryGetValue("garcia", out string email))
{
Console.WriteLine(email);
}
try
{
Console.WriteLine(emails["nadie"]);
}
catch (KeyNotFoundException e)
{
Console.WriteLine("KeyNotFoundException: " + e.Message);
}
Salida
garcia@example.com
KeyNotFoundException: The given key 'nadie' was not present in the dictionary.

emails[clave] no recorre todos los pares. Encuentra el lugar de la clave casi al instante, sin importar cuántos haya. Cómo lo hace es la siguiente lección.

La idea

El almacén: cambios y comprobaciones

Las existencias de un almacén: artículo → cantidad.

Dictionary<string, int> stock = new Dictionary<string, int>();
stock["portátil"] = 4;
stock["ratón"] = 25;
stock["portátil"] = 3;
Console.WriteLine(stock["portátil"]);
Console.WriteLine(stock.ContainsKey("monitor"));
Console.WriteLine(stock.GetValueOrDefault("monitor", 0));
stock.Remove("ratón");
Console.WriteLine(stock.Count);
Salida
3
False
0
1
  • Las claves son únicas. El segundo stock["portátil"] = 3 no agrega una fila nueva. Reemplaza 4 por 3.
  • ContainsKey pregunta si la clave está en el diccionario.
  • GetValueOrDefault(clave, 0) devuelve el valor, o 0 si la clave no está. Esto te protege de KeyNotFoundException.
  • Remove(clave) quita el par.
Más precisamente, para curiosos
  • Los valores se pueden repetir: el portátil y el ratón pueden ser 3 los dos. Las claves no.
  • También existe ContainsValue. Pero recorre todos los valores, porque un diccionario es rápido solo por clave.
  • También puedes guardar un par con Add(clave, valor). La diferencia está en una clave que ya existe: [clave] = reemplaza el valor, pero Add lanza ArgumentException y deja el valor anterior:
Dictionary<string, int> stock = new Dictionary<string, int>();
stock.Add("portátil", 4);
try
{
stock.Add("portátil", 3);
}
catch (ArgumentException e)
{
Console.WriteLine(e.Message);
}
Console.WriteLine(stock["portátil"]);
Salida
An item with the same key has already been added. Key: portátil
4

Pruébalo

¿Cuántos hay en el menú?

¿Qué imprimirá el programa?

Dictionary<string, int> menu = new Dictionary<string, int>();
menu["café"] = 2;
menu["té"] = 2;
menu["café"] = 3;
Console.WriteLine(menu.Count + " " + menu["café"]);
Respuesta

La idea

Recorrido y orden

El precio de envío por ciudad. foreach sobre el diccionario da los pares: cada uno es un KeyValuePair<string, double>, con Key y Value:

Dictionary<string, double> delivery = new Dictionary<string, double>();
delivery["Sofia"] = 4.5;
delivery["Plovdiv"] = 5.2;
delivery["Varna"] = 6.0;
delivery["Burgas"] = 6.0;
delivery["Ruse"] = 5.8;
foreach (KeyValuePair<string, double> e in delivery)
{
Console.WriteLine($"{e.Key}: {e.Value:F2} €");
}
Salida
Sofia: 4.50 €
Plovdiv: 5.20 €
Varna: 6.00 €
Burgas: 6.00 €
Ruse: 5.80 €

Las ciudades salieron en el orden en que se agregaron. Pero Dictionary no promete ningún orden. Mientras solo agregas, suele mantener el orden en que se agregan. Después de Remove, en cambio, la clave nueva ocupa el lugar de la que quitaste. Con las mismas cinco ciudades:

delivery.Remove("Plovdiv");
delivery["Pleven"] = 5.0;
Console.WriteLine(string.Join(", ", delivery.Keys));
Salida
Sofia, Pleven, Varna, Burgas, Ruse

Pleven se agregó al final, pero salió segunda. No escribas código que dependa del orden de un Dictionary. Cuando el orden importa, cambia solo la clase:

  • OrderedDictionary mantiene el orden en que se agregan, y lo promete. Con las mismas ciudades, Remove y agregando Pleven, Pleven va al final:
Salida
Sofia, Varna, Burgas, Ruse, Pleven
  • SortedDictionary ordena las claves:
SortedDictionary<string, double> delivery = new SortedDictionary<string, double>();
delivery["Sofia"] = 4.5;
delivery["Plovdiv"] = 5.2;
delivery["Varna"] = 6.0;
delivery["Burgas"] = 6.0;
delivery["Ruse"] = 5.8;
foreach (KeyValuePair<string, double> e in delivery)
{
Console.WriteLine($"{e.Key}: {e.Value:F2} €");
}
Salida
Burgas: 6.00 €
Plovdiv: 5.20 €
Ruse: 5.80 €
Sofia: 4.50 €
Varna: 6.00 €

{e.Value:F2} imprime el precio con dos cifras después del punto decimal: si no, 6.0 sale como 6.

Más precisamente, para curiosos
  • Si necesitas solo las claves, recorre Keys. Si necesitas solo los valores, recorre Values:
foreach (string city in delivery.Keys)
{
Console.Write(city + " ");
}
Console.WriteLine();
double sum = 0;
foreach (double price in delivery.Values)
{
sum += price;
}
Console.WriteLine($"Media: {sum / delivery.Count:F2} €");
Salida
Sofia Plovdiv Varna Burgas Ruse
Media: 5.50 €
  • SortedDictionary es más lento que Dictionary, porque mantiene las claves ordenadas. Úsalo cuando necesites el orden.
  • OrderedDictionary con parámetros de tipo es nuevo: existe desde .NET 9.

La idea

Conteo

¿Cuántas veces se pidió cada artículo? La clave es el artículo, y el valor es la cantidad. En cada pedido tomas la cantidad anterior (o 0) y le sumas 1:

string orders = "café té café cacao café té";
SortedDictionary<string, int> counts = new SortedDictionary<string, int>();
foreach (string item in orders.Split(' '))
{
counts[item] = counts.GetValueOrDefault(item, 0) + 1;
}
Console.WriteLine(string.Join(", ", counts));
Salida
[cacao, 1], [café, 3], [té, 2]

Este es el uso más común de un diccionario: contar palabras, votos, errores por tipo, ventas por ciudad. Aquí es un SortedDictionary, así que los artículos salen ordenados. string.Join(", ", counts) imprime cada par como [clave, valor].

string orders = "café té café cacao café té";
SortedDictionary<string, int> counts = new SortedDictionary<string, int>();
foreach (string item in orders.Split(' '))
{
counts[item] = counts.GetValueOrDefault(item, 0) + 1;
}
Console.WriteLine(string.Join(", ", counts));
Paso a paso, en palabras
  1. 6 pedidos y un SortedDictionary vacío.
  2. «café» no está: GetValueOrDefault da 0, y escribir en counts[item] añade la clave con 1.
  3. «té» no está: GetValueOrDefault da 0, y escribir en counts[item] añade la clave con 1.
  4. «café» ya está: GetValueOrDefault da 1, y escribir en counts[item] reemplaza el valor por 2.
  5. «cacao» no está: GetValueOrDefault da 0, y escribir en counts[item] añade la clave con 1. SortedDictionary la pone antes de «café»: las claves están ordenadas.
  6. «café» ya está: GetValueOrDefault da 2, y escribir en counts[item] reemplaza el valor por 3.
  7. «té» ya está: GetValueOrDefault da 1, y escribir en counts[item] reemplaza el valor por 2.
  8. Cada producto es una clave una sola vez, y el valor es cuántas veces se pidió.
Más precisamente, para curiosos
  • split(" ") de la lección sobre texto divide la línea en palabras.
  • En el módulo «C# moderno» verás LINQ. Tiene un conteo listo, CountBy, que da los pares en el orden en que aparecieron los artículos por primera vez:
string orders = "café té café cacao café té";
Console.WriteLine(string.Join(", ", orders.Split(' ').CountBy(item => item)));
Salida
[café, 3], [té, 2], [cacao, 1]

Pruébalo

Encuentra el error

El programa debería contar los pedidos. En cambio, se detiene en el primero:
Error
Unhandled exception. System.Collections.Generic.KeyNotFoundException: The given key 'café' was not present in the dictionary.

¿En qué línea está el error?

Elige la línea con el error.

La idea

HashSet: cada uno una vez

¿Cuántas personas distintas iniciaron sesión hoy en el sitio? Si alguien entra cinco veces, debe contarse una sola. Una colección que guarda cada valor una sola vez se llama conjunto:

string[] visits = { "ana@example.com", "bruno@example.com", "ana@example.com", "vera@example.com", "bruno@example.com" };
HashSet<string> visitors = new HashSet<string>();
foreach (string email in visits)
{
visitors.Add(email);
}
Console.WriteLine("Visitas: " + visits.Length);
Console.WriteLine("Visitantes únicos: " + visitors.Count);
Console.WriteLine(visitors.Contains("vera@example.com"));
Salida
Visitas: 5
Visitantes únicos: 3
True
  • Add sobre un valor que ya está no hace nada y devuelve False. Si es nuevo, devuelve True:
HashSet<string> seen = new HashSet<string>();
Console.WriteLine(seen.Add("F-101"));
Console.WriteLine(seen.Add("F-101"));
Salida
True
False
  • Contains es tan rápido como ContainsKey en un diccionario. Por dentro, un HashSet es como un Dictionary en el que guardas solo las claves.
  • No hay índices. Un conjunto responde solo a la pregunta «¿está aquí?».
Más precisamente, para curiosos
  • El orden de un HashSet no está prometido, igual que con Dictionary. SortedSet ordena. También puedes escribir un conjunto de una vez, entre {} con valores:
SortedSet<string> tags = new SortedSet<string> { "java", "sql", "git", "java" };
Console.WriteLine(string.Join(", ", tags));
HashSet<string> backend = new HashSet<string> { "java", "sql", "docker" };
SortedSet<string> common = new SortedSet<string>(tags);
common.IntersectWith(backend);
Console.WriteLine(string.Join(", ", common));
tags.UnionWith(backend);
Console.WriteLine(string.Join(", ", tags));
Salida
git, java, sql
java, sql
docker, git, java, sql
  • IntersectWith deja solo los elementos que dos conjuntos tienen en común, y UnionWith los une. Los dos cambian el conjunto sobre el que los llamas, así que los comunes van en una copia: new SortedSet<string>(tags). Los necesitarás en el ejercicio.

Pruébalo

Una factura repetida

El contador quiere ver los números de factura que se ingresaron dos veces. ¿Qué método comprueba y agrega a la vez?
string[] invoices = { "F-101", "F-102", "F-101", "F-103" };
HashSet<string> seen = new HashSet<string>();
foreach (string number in invoices)
{
    if (!seen.(number))
    {
        Console.WriteLine("Factura repetida: " + number);
    }
}

Ejercicio

Diccionarios en acción

Resuelve las tareas en tu editor.

1. Una tabla de usuarios. Una clase UserTable con los métodos Add(string username, string email), Get(string username) y Size(). Por dentro es un Dictionary. Para un usuario que no está, Get devuelve null: GetValueOrDefault sin segundo argumento da justo eso. Agrega garcia, mlopez y fernandez con correos de la forma garcia@example.com, y busca mlopez y nadie. En la siguiente lección también construirás tu propia tabla hash, sin Dictionary.

Salida
mlopez: mlopez@example.com
nadie: no existe ese usuario
Usuarios: 3

2. Frecuencia de palabras. Lee una línea de texto, pásala a minúsculas y cuenta cada palabra. Imprime las palabras en orden alfabético.

Entrada
Café con leche y café sin azúcar y café
Salida
azúcar: 1
café: 3
con: 1
leche: 1
sin: 1
y: 2

3. Documentos por palabra clave. Una clase DocumentIndex con los métodos AddDocument(int id, params string[] keywords) y Search(string keyword), que devuelve una lista con los números de los documentos que tienen esa palabra. Por dentro es un Dictionary<string, List<int>>: para cada palabra, una lista de documentos. Agrega el documento 1 con "POO" y "Java", el documento 2 con "SQL" y "bases de datos", y el documento 3 con "Java" y "colecciones". Busca "Java", "SQL" y "Python", e imprime los números entre [], separados por comas.

Salida
Java: [1, 3]
SQL: [2]
Python: []

4. Una red de contactos. Una clase Contact con nombre y correo, y una clase ContactNetwork con los métodos AddContact(string name, string email), AddConnection(string a, string b) (la conexión va en los dos sentidos), FriendsOf(string name), Common(string a, string b) (amigos en común) y EmailOf(string name). Guarda los contactos en un Dictionary<string, Contact> y las conexiones en un Dictionary<string, SortedSet<string>>, para que los nombres salgan ordenados. Conecta a Juan con María y a Juan con Pedro.

Salida
Juan: [María, Pedro]
María: [Juan]
Amigos en común de María y Pedro: [Juan]
Correo de Pedro: pedro@example.com

Más tareas

  • Un ID único. Un método GenerateUserId(string email) da a cada correo nuevo un número, U001, U002 y así sucesivamente, y para un correo que ya vio devuelve el mismo número. Las mayúsculas y las minúsculas del correo cuentan igual. En la siguiente lección hay una versión con una función hash.
  • Un catálogo de libros. La clave es el ISBN, por ejemplo 978-84-376-0494-7, y el valor es el título. Agrega libros, busca uno e imprímelos todos, ordenados por ISBN.
  • Un planificador de viajes. Para cada destino, guarda una lista de actividades: «París» → «la torre Eiffel», «el Louvre». Imprime el plan en el orden en que se agregaron los destinos. ¿Qué diccionario vas a elegir?
  • Un diccionario en línea. Palabra → definición. Lee palabras hasta que llegue «fin». Para una palabra desconocida, imprime «No existe esa palabra.».
  • Notas por asignatura. Lee líneas como «Matemáticas 6», «Física 5», «Matemáticas 4», e imprime la nota media de cada asignatura, ordenadas por nombre.

Test

Ponte a prueba

1. ¿Cuándo es un Dictionary mejor opción que una lista?
2. menu ya tiene la clave "café" con el valor 2. ¿Qué hace menu["café"] = 3?
3. La clave "té" no está en menu. ¿Qué hace menu["té"]?
4. Recorres un Dictionary. ¿En qué orden salen las claves?
5. ¿Qué devuelve Add en un HashSet si el valor ya está?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·