Lic. Carlos Enrique Loria Beeche.
Secuela técnica de la saga “Estructuras de datos: cuando la memoria se convierte en inteligencia”
Este artículo continúa la saga que inicié con Estructuras de datos: cuando la memoria se convierte en inteligencia, donde presentamos ocho estructuras fundamentales para comprender cómo el software organiza, encuentra y procesa información.
Después recorrimos el array, la linked list, la stack, la queue, la hash table, el binary search tree, el heap y el graph.
Ahora quiero hacer una secuela práctica: hablar de los diccionarios, una de las estructuras más usadas en los lenguajes modernos. En C# los conocemos como Dictionary<TKey, TValue>. En Python aparecen como dict. En C++ los encontramos, entre otras formas, como unordered_map.
Un diccionario es, en la práctica, una forma muy poderosa de usar una tabla hash para asociar una clave con un valor.
Un diccionario nos enseña que no siempre buscamos por posición; muchas veces buscamos por significado.
¿Qué es un diccionario?
Un diccionario es una estructura de datos que guarda información en pares:
- Clave: el identificador que usamos para buscar.
- Valor: la información asociada a esa clave.
Por ejemplo:
Clave: "Carlos"
Valor: "Administrador"
Clave: "Ana"
Valor: "Contabilidad"
Clave: "Luis"
Valor: "Ventas"
Si conocemos la clave "Carlos", el diccionario puede devolver rápidamente el valor asociado: "Administrador".
La idea es muy distinta a recorrer una lista elemento por elemento. En un diccionario, no preguntamos: “¿en qué posición estará este dato?”. Preguntamos: “¿qué valor corresponde a esta clave?”.
La metáfora de la recepción de un hotel
Una forma sencilla de entender un diccionario es pensar en la recepción de un hotel.
Imaginemos un hotel con muchas habitaciones. Si llega una persona a recepción y pregunta por un huésped, el recepcionista no empieza a caminar por todos los pisos, tocando puerta por puerta, hasta encontrarlo.
Lo correcto es consultar el registro.
El visitante da un nombre. Ese nombre funciona como clave. El sistema de recepción devuelve el número de habitación, que funciona como valor.
Clave: "Carlos Loria"
Valor: "Habitación 314"
El recepcionista no busca físicamente por todo el hotel. Consulta una estructura organizada que le permite llegar directamente a la información.
La habitación no se encuentra caminando por todos los pasillos; se obtiene consultando la clave correcta.
Array, lista y diccionario: tres formas de buscar
Para entender mejor el valor del diccionario, conviene compararlo con estructuras que ya vimos.
Buscar en un array
En un array, si conocemos el índice, el acceso es directo:
nombres[3]
Pero si no conocemos la posición y queremos buscar por contenido, posiblemente tengamos que recorrer la colección.
Buscar en una lista enlazada
En una lista enlazada, normalmente avanzamos nodo por nodo:
inicio -> nodo -> nodo -> nodo -> dato buscado
Cada elemento nos lleva al siguiente, pero no podemos saltar directamente a cualquier dato.
Buscar en un diccionario
En un diccionario, usamos una clave:
usuarios["Carlos"]
La clave nos lleva directamente al valor asociado, sin obligarnos a revisar todos los elementos uno por uno.
| Estructura | Forma de acceso | Pregunta típica |
|---|---|---|
| Array | Por índice | ¿Qué hay en esta posición? |
| Linked List | Por recorrido | ¿Cuál es el siguiente nodo? |
| Diccionario | Por clave | ¿Qué valor corresponde a esta clave? |
La relación entre diccionario y hash table
En el artículo sobre hash table vimos que una tabla hash usa una función matemática para convertir una clave en una posición interna.
Un diccionario suele apoyarse en esa idea.
clave -> función hash -> posición interna -> valor
Por ejemplo:
"Carlos" -> hash("Carlos") -> índice interno -> "Administrador"
Desde el punto de vista del programador, esto se ve muy simple:
rol = usuarios["Carlos"]
Pero internamente hay una maquinaria más compleja: claves, funciones hash, posiciones internas, buckets, colisiones y estrategias para resolverlas.
El diccionario nos ofrece una sintaxis sencilla, pero por dentro descansa sobre una estructura cuidadosamente diseñada.
Un vistazo al Dictionary<TKey, TValue> en .NET
Como desarrollador C# —y recordando también mi etapa como Microsoft MVP— me parece importante detenernos un momento en la implementación práctica del diccionario en .NET.
En C#, cuando usamos Dictionary<TKey, TValue>, normalmente lo vemos desde una sintaxis muy sencilla:
roles["Carlos"] = "Administrador";
string rol = roles["Carlos"];
Pero esa simplicidad esconde una estructura interna cuidadosamente diseñada. El diccionario no guarda simplemente una lista de pares clave-valor y luego los recorre uno por uno. Si lo hiciera así, perdería gran parte de su ventaja.
Internamente, un diccionario necesita convertir la clave en una ubicación eficiente de búsqueda. Para eso utiliza el código hash de la clave y una estructura interna de almacenamiento que permite llegar rápidamente al valor asociado.
En términos conceptuales, podemos imaginarlo así:
clave
↓
GetHashCode()
↓
posición interna
↓
entrada donde vive la clave y su valor
El programador escribe una línea sencilla, pero por debajo el runtime coordina una maquinaria de búsqueda, comparación, distribución y manejo de colisiones.
En .NET, el Dictionary<TKey, TValue> convierte una clave de C# en una ruta eficiente hacia un valor almacenado en memoria.
Buckets y entries: dos piezas internas importantes
Una forma útil de entender un diccionario en .NET es pensar en dos piezas internas: los buckets y las entries.
Los buckets funcionan como casillas iniciales de ubicación. Cuando se calcula el hash de una clave, ese resultado ayuda a decidir en qué bucket debe comenzar la búsqueda.
Las entries contienen la información real asociada: el hash calculado, la clave, el valor y, cuando hace falta, una referencia interna hacia otra entrada relacionada con una colisión.
De forma simplificada, una entrada puede imaginarse así:
Entry
{
hashCode
next
key
value
}
El campo key guarda la clave original. El campo value guarda el valor asociado. El campo hashCode conserva el código hash usado para ubicar la entrada. Y el campo next permite enlazar internamente otra entrada cuando varias claves terminan compartiendo una misma zona de búsqueda.
Este detalle es importante porque muestra que el diccionario no es magia. Es una estructura diseñada con mucho cuidado para equilibrar velocidad, memoria y manejo correcto de colisiones.
La sintaxis del diccionario es simple; su implementación interna no lo es.
Colisiones en .NET: cuando varias claves llegan cerca
En teoría, nos gustaría que cada clave terminara en una posición única. En la práctica, eso no siempre ocurre. Dos claves distintas pueden producir ubicaciones internas iguales o cercanas. A eso le llamamos colisión.
Cuando ocurre una colisión, el diccionario debe conservar ambos valores sin confundirlos. Para eso no basta con calcular el hash. También debe comparar las claves reales y seguir la cadena interna correspondiente hasta encontrar la entrada correcta.
Por eso es tan importante que las claves usadas en un diccionario tengan una definición coherente de igualdad y de hash. En C#, esto se vuelve especialmente importante cuando usamos objetos propios como claves.
Si una clase se usa como clave, debemos cuidar métodos como Equals y GetHashCode, o proporcionar un comparador adecuado. De lo contrario, el diccionario podría comportarse de manera inesperada.
Un buen diccionario depende de buenas claves. Y en C#, una buena clave necesita igualdad y hash coherentes.
Claves y valores
La clave debe identificar de manera clara el dato que queremos recuperar.
En un sistema real, una clave puede ser:
- Un nombre de usuario.
- Un código de producto.
- Un número de identificación.
- Un correo electrónico.
- Un identificador de cliente.
- Un código de empresa.
- Una palabra en un conteo de frecuencia.
El valor puede ser cualquier información asociada:
- Un nombre completo.
- Un precio.
- Un objeto de usuario.
- Una configuración.
- Una cadena de conexión.
- Una lista de permisos.
- Un contador.
La fuerza del diccionario está en esa asociación directa.
clave -> valor
Ejemplo sencillo
Supongamos que queremos guardar extensiones telefónicas internas:
Carlos -> 314
Ana -> 201
Luis -> 122
María -> 450
Con una lista, podríamos tener que revisar persona por persona hasta encontrar a Carlos.
Con un diccionario, usamos directamente la clave:
extensiones["Carlos"]
Y obtenemos:
314
Complejidad promedio
Una de las razones por las que los diccionarios son tan importantes es su rendimiento promedio.
En condiciones normales, buscar, insertar o eliminar por clave suele tener costo promedio:
O(1)
Eso significa tiempo constante promedio.
En otras palabras, buscar un valor por clave puede tomar un tiempo muy parecido aunque el diccionario tenga 10, 10.000 o 1.000.000 de elementos.
Pero hay que decirlo con cuidado: ese rendimiento es promedio. Depende de una buena función hash, una buena distribución de claves y una estrategia adecuada para manejar colisiones.
Una advertencia importante: O(1) no significa “gratis”
Cuando decimos que un diccionario ofrece búsqueda promedio O(1), estamos diciendo algo muy importante desde el punto de vista algorítmico: en promedio, la búsqueda por clave no crece linealmente con la cantidad de elementos.
Pero eso no significa que el diccionario sea siempre la estructura más rápida para todo.
Un array o una lista basada en memoria contigua puede ser extraordinariamente eficiente cuando necesitamos recorrer datos secuencialmente. Los procesadores modernos aprovechan muy bien la localidad de memoria y la caché del CPU cuando los datos están colocados uno después del otro.
El diccionario, en cambio, brilla cuando necesitamos encontrar un valor por clave. Su fortaleza no es recorrer todos los elementos en orden, sino llegar rápidamente al valor asociado a una identidad.
| Escenario | Estructura que suele convenir | Razón |
|---|---|---|
| Recorrer muchos elementos consecutivos | Array o lista | Aprovecha mejor la memoria contigua y la caché |
| Buscar rápidamente por clave | Diccionario | Usa hash para llegar al valor asociado |
| Mantener elementos en orden | Árbol o colección ordenada | Permite recorrido ordenado y búsquedas por rango |
El array es fuerte cuando recorremos por posición; el diccionario es fuerte cuando buscamos por identidad.
Colisiones: cuando dos claves llegan al mismo lugar
Una colisión ocurre cuando dos claves distintas terminan apuntando a la misma posición interna.
hash("Carlos") -> posición 5
hash("Celia") -> posición 5
Esto puede pasar porque el número de claves posibles es enorme, pero la tabla interna tiene un tamaño limitado.
Cuando ocurre una colisión, el diccionario debe resolverla sin perder información.
Algunas implementaciones usan encadenamiento. Otras usan direccionamiento abierto. En muchos lenguajes modernos, estos detalles quedan ocultos para el programador, pero siguen siendo esenciales para el rendimiento.
Una buena implementación de diccionario no promete que nunca habrá colisiones; promete manejarlas correctamente.
Ejemplo en C#
En C#, el tipo más común para trabajar con diccionarios es Dictionary<TKey, TValue>.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Dictionary<string, string> roles = new Dictionary<string, string>();
roles["Carlos"] = "Administrador";
roles["Ana"] = "Contabilidad";
roles["Luis"] = "Ventas";
Console.WriteLine("Rol de Carlos: " + roles["Carlos"]);
roles["Carlos"] = "Superusuario";
Console.WriteLine("Rol actualizado de Carlos: " + roles["Carlos"]);
if (roles.ContainsKey("Ana"))
{
Console.WriteLine("Ana existe en el diccionario.");
}
roles.Remove("Luis");
foreach (KeyValuePair<string, string> par in roles)
{
Console.WriteLine(par.Key + " -> " + par.Value);
}
}
}
En este ejemplo, el nombre funciona como clave y el rol funciona como valor.
El diccionario permite insertar, buscar, actualizar y eliminar usando la clave.
Ejemplo en Python
En Python, los diccionarios son parte natural del lenguaje mediante el tipo dict.
roles = {}
roles["Carlos"] = "Administrador"
roles["Ana"] = "Contabilidad"
roles["Luis"] = "Ventas"
print("Rol de Carlos:", roles["Carlos"])
roles["Carlos"] = "Superusuario"
print("Rol actualizado de Carlos:", roles["Carlos"])
if "Ana" in roles:
print("Ana existe en el diccionario.")
del roles["Luis"]
for clave, valor in roles.items():
print(clave, "->", valor)
Python usa diccionarios constantemente. Los vemos en configuraciones, objetos, respuestas JSON, conteos, agrupaciones y estructuras internas del lenguaje.
Ejemplo en C++
En C++, una estructura equivalente es unordered_map.
#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;
int main() {
unordered_map<string, string> roles;
roles["Carlos"] = "Administrador";
roles["Ana"] = "Contabilidad";
roles["Luis"] = "Ventas";
cout << "Rol de Carlos: " << roles["Carlos"] << endl;
roles["Carlos"] = "Superusuario";
cout << "Rol actualizado de Carlos: "
<< roles["Carlos"] << endl;
if (roles.find("Ana") != roles.end()) {
cout << "Ana existe en el mapa." << endl;
}
roles.erase("Luis");
for (const auto& par : roles) {
cout << par.first << " -> " << par.second << endl;
}
return 0;
}
En C++, unordered_map no garantiza orden de recorrido, pero ofrece búsqueda rápida por clave.
Aplicación práctica: contar frecuencias
Una aplicación clásica de un diccionario es contar cuántas veces aparece cada elemento.
Supongamos esta lista:
sol, luna, sol, mar, luna, sol
Queremos obtener:
sol -> 3
luna -> 2
mar -> 1
En Python podríamos escribir:
palabras = ["sol", "luna", "sol", "mar", "luna", "sol"]
conteo = {}
for palabra in palabras:
if palabra in conteo:
conteo[palabra] += 1
else:
conteo[palabra] = 1
print(conteo)
Este patrón aparece en análisis de texto, estadísticas, procesamiento de logs, inventarios, conteos de visitas y muchas tareas cotidianas de programación.
Aplicación práctica: configuraciones por clave
Otra aplicación común es guardar configuraciones.
ServidorCorreo -> smtp.midominio.com
PuertoSMTP -> 587
ModoDebug -> false
Idioma -> es-CR
Un diccionario permite obtener rápidamente una configuración por su nombre:
configuracion["ServidorCorreo"]
Esto resulta útil cuando una aplicación tiene muchos parámetros y necesitamos acceder a ellos por clave.
Aplicación práctica: usuarios y permisos
En sistemas empresariales, un diccionario puede asociar usuarios con permisos, roles o perfiles.
usuario01 -> Administrador
usuario02 -> Consulta
usuario03 -> Contabilidad
usuario04 -> Ventas
En lugar de recorrer una lista completa de usuarios cada vez que alguien inicia sesión, el sistema puede buscar directamente el usuario por su clave.
En aplicaciones reales, por supuesto, intervienen bases de datos, autenticación, seguridad, caché y reglas adicionales. Pero la idea conceptual del diccionario sigue siendo la misma: una clave permite llegar al valor correcto.
Diccionarios y caché
Un uso muy importante de los diccionarios aparece en los sistemas de caché.
Una caché guarda resultados ya calculados para evitar repetir trabajo innecesario.
Por ejemplo:
Clave: "producto:1001"
Valor: datos del producto 1001
Si el sistema ya consultó ese producto antes, puede recuperarlo desde memoria en lugar de volver a consultar una base de datos.
Esto puede mejorar mucho el rendimiento cuando las mismas claves se consultan repetidamente.
Un diccionario bien usado puede evitar que una aplicación repita búsquedas costosas.
Diccionarios, memoria y arquitectura empresarial
En aplicaciones empresariales, el diccionario rara vez aparece como un simple ejercicio académico. Aparece como una herramienta real de arquitectura.
Un sistema puede usar diccionarios para mantener en memoria configuraciones, rutas, permisos, cadenas de conexión, perfiles de usuario, parámetros por empresa o información calculada previamente.
Por ejemplo, en una aplicación multiempresa podríamos tener una estructura conceptual como esta:
EmpresaA -> configuración de EmpresaA
EmpresaB -> configuración de EmpresaB
EmpresaC -> configuración de EmpresaC
Cuando llega una solicitud, el sistema identifica la empresa y busca rápidamente la configuración correspondiente.
var configuracion = configuracionesPorEmpresa["EmpresaA"];
En escenarios con muchas solicitudes simultáneas, esta búsqueda en memoria puede evitar consultas repetidas a base de datos o cálculos innecesarios. Por eso los diccionarios aparecen con frecuencia en cachés, servicios compartidos, middlewares, validadores y componentes de ruteo.
Naturalmente, cuando hay concurrencia real, no basta con usar cualquier diccionario de forma descuidada. En C#, estructuras como ConcurrentDictionary existen precisamente para escenarios donde varios hilos pueden consultar o modificar la colección al mismo tiempo.
En sistemas empresariales, el diccionario muchas veces funciona como una pequeña tabla de decisión en memoria.
Diccionarios en sistemas multiempresa o multi-tenant
En aplicaciones empresariales modernas, especialmente en sistemas multiempresa o multi-tenant, los diccionarios también pueden ayudar a resolver configuraciones por cliente, empresa o inquilino.
Por ejemplo:
EmpresaA -> Cadena de conexión A
EmpresaB -> Cadena de conexión B
EmpresaC -> Cadena de conexión C
Cuando llega una solicitud, el sistema identifica la empresa y busca rápidamente la configuración correspondiente.
conexion = conexionesPorEmpresa["EmpresaA"]
Este patrón puede aparecer en sistemas donde cada cliente tiene configuraciones, permisos, rutas, bases de datos o parámetros distintos.
En ambientes concurrentes, donde varios hilos pueden leer y escribir al mismo tiempo, existen estructuras especializadas como ConcurrentDictionary en C#.
ConcurrentDictionary en C#
Un ConcurrentDictionary está diseñado para escenarios donde múltiples hilos pueden acceder a la colección de forma simultánea.
Esto puede ser importante en servidores web, procesos en segundo plano, cachés compartidas o servicios que atienden muchas solicitudes al mismo tiempo.
using System;
using System.Collections.Concurrent;
class Program
{
static void Main()
{
ConcurrentDictionary<string, string> conexiones =
new ConcurrentDictionary<string, string>();
conexiones.TryAdd("EmpresaA", "Conexion_A");
conexiones.TryAdd("EmpresaB", "Conexion_B");
string conexion = conexiones.GetOrAdd("EmpresaC", "Conexion_C");
Console.WriteLine(conexiones["EmpresaA"]);
Console.WriteLine(conexion);
}
}
La ventaja de esta estructura es que ofrece operaciones seguras para concurrencia, evitando muchos problemas típicos de acceso simultáneo.
Esto no significa que resuelva automáticamente todos los problemas de arquitectura, pero sí ofrece una herramienta más apropiada cuando varios hilos comparten la misma colección.
Diccionario no significa orden
Un punto importante: un diccionario no debe entenderse como una lista ordenada.
Su propósito principal no es conservar un orden alfabético ni numérico. Su fortaleza es encontrar rápido por clave.
Si necesitamos recorrer elementos en orden, quizá convenga usar otra estructura o una variante ordenada, dependiendo del lenguaje.
Esta distinción es importante:
| Necesidad | Estructura posible |
|---|---|
| Acceder por posición | Array o lista |
| Insertar y enlazar elementos | Linked List |
| Buscar rápido por clave | Diccionario / Hash Table |
| Recorrer en orden | Árbol ordenado u otra colección ordenada |
Errores comunes al usar diccionarios
Los diccionarios son muy útiles, pero también pueden usarse mal.
1. Asumir que una clave siempre existe
Si intentamos acceder a una clave inexistente, algunos lenguajes lanzan error.
En C#, por ejemplo, conviene verificar:
if (roles.ContainsKey("Carlos"))
{
Console.WriteLine(roles["Carlos"]);
}
También se puede usar TryGetValue:
if (roles.TryGetValue("Carlos", out string? rol))
{
Console.WriteLine(rol);
}
2. Usar claves inconsistentes
Una clave debe ser estable. Si usamos textos con diferencias de mayúsculas, espacios o formatos, podríamos crear errores difíciles de detectar.
"Carlos"
"carlos"
"Carlos "
Para un humano pueden parecer casi iguales. Para el diccionario pueden ser claves distintas.
3. Usar el diccionario cuando se necesita orden
Si el problema exige recorrer elementos en orden, un diccionario puede no ser la mejor estructura principal.
4. Guardar demasiada lógica dentro de claves improvisadas
Las claves deben diseñarse con cuidado. Una clave mal construida puede producir ambigüedad, duplicación o errores de interpretación.
Un diccionario es muy rápido, pero su claridad depende de la calidad de sus claves.
La lección del diccionario
El diccionario nos enseña una idea fundamental del software moderno: muchas veces la información no se busca por lugar, sino por identidad.
El array pregunta: ¿qué hay en esta posición?
La lista enlazada pregunta: ¿cuál es el siguiente nodo?
La pila pregunta: ¿qué fue lo último que quedó pendiente?
La cola pregunta: ¿quién llegó primero?
La tabla hash pregunta: ¿a qué posición me lleva esta clave?
El diccionario pregunta: ¿qué valor corresponde a este significado?
Por eso aparece en tantas aplicaciones reales. Porque los sistemas no solo manejan números; manejan usuarios, empresas, códigos, permisos, configuraciones, sesiones, productos, rutas, tokens, documentos y relaciones.
El diccionario convierte una clave en una puerta: si la clave es correcta, el valor aparece sin recorrer todo el edificio.
Cierre
Esta secuela nace naturalmente después de estudiar la tabla hash. La tabla hash nos mostró el mecanismo. El diccionario nos muestra la herramienta práctica que usamos todos los días en lenguajes modernos.
Comprender los diccionarios no consiste solo en saber escribir una sintaxis. Consiste en entender que detrás de una línea sencilla como esta:
valor = diccionario[clave]
hay una decisión profunda de diseño: asociar, identificar y recuperar información de forma directa.
Y volvemos una vez más a la idea central de esta saga: cuando la memoria se organiza bien, deja de ser simple almacenamiento y empieza a parecer inteligencia.
Referencias para profundizar
- Carlos Enrique Loría Beeche — Estructuras de datos: cuando la memoria se convierte en inteligencia
- Carlos Enrique Loría Beeche — Hash Table: encontrar sin buscar
- Adolfo Di Mare Hering — Tipos Abstractos de Datos y Programación por Objetos
- Microsoft Learn — Dictionary<TKey,TValue> Class
- Microsoft Learn — ConcurrentDictionary<TKey,TValue> Class
