Ir al contenido
wannacode

Map y Set

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 un array. En cada inicio de sesión buscas el correo por el nombre de usuario:

function findEmail(users, username) {
for (const u of 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 JavaScript el diccionario es Map:

const emails = new Map();
emails.set("garcia", "garcia@example.com");
emails.set("mlopez", "mlopez@example.com");
emails.set("fernandez", "fernandez@example.com");
console.log(emails.get("fernandez"));
console.log(emails.get("nadie"));
console.log(`Usuarios: ${emails.size}`);
Salida
fernandez@example.com
undefined
Usuarios: 3
  • new Map() es un diccionario vacío. set(clave, valor) guarda un par, y get(clave) devuelve el valor.
  • Una clave que no está da undefined.
  • size es el número de pares. Se escribe sin paréntesis: es una propiedad, no un método.

En JavaScript un objeto simple {} también parece un diccionario: stock[clave] = valor. Mucho código lo usa así, pero tiene trampas:

const stock = {};
stock[1] = "portátil";
stock["1"] = "ratón";
console.log(stock);
console.log("toString" in stock);
const map = new Map();
map.set(1, "portátil");
map.set("1", "ratón");
console.log(map);
console.log(map.has("toString"));
Salida
{ '1': 'ratón' }
true
Map(2) { 1 => 'portátil', '1' => 'ratón' }
false
  • Las claves de un objeto siempre se convierten en texto: 1 y "1" son la misma clave, y «ratón» reemplazó a «portátil».
  • El objeto también tiene claves que nunca pusiste: toString viene del prototipo común de todos los objetos.
  • Un Map conserva el tipo de la clave y guarda solo lo que pusiste. Para un diccionario, usa Map.

get 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.

const stock = new Map();
stock.set("portátil", 4);
stock.set("ratón", 25);
stock.set("portátil", 3);
console.log(stock.get("portátil"));
console.log(stock.has("monitor"));
console.log(stock.get("monitor") ?? 0);
stock.delete("ratón");
console.log(stock.size);
Salida
3
false
0
1
  • Las claves son únicas. El segundo set("portátil", 3) no agrega una fila nueva. Reemplaza 4 por 3.
  • has pregunta si la clave está en el mapa.
  • get(clave) ?? 0 da el valor, o 0 si la clave no está. a ?? b es a, salvo que a sea undefined o null: entonces es b. Esto te protege de undefined.
  • delete(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.
  • Map no tiene un método «¿está este valor?». Tienes que recorrer values(), porque un diccionario es rápido solo por clave.

Pruébalo

¿Cuántos hay en el menú?

¿Qué imprimirá el programa?

const menu = new Map();
menu.set("café", 2);
menu.set("té", 2);
menu.set("café", 3);
console.log(menu.size, menu.get("café"));
Respuesta

La idea

Recorrido y orden

El precio de envío por ciudad. for...of sobre un Map da pares [clave, valor], y const [city, price] separa cada uno en dos nombres:

const delivery = new Map();
delivery.set("Sofia", 4.5);
delivery.set("Plovdiv", 5.2);
delivery.set("Varna", 6.0);
delivery.set("Burgas", 6.0);
delivery.set("Ruse", 5.8);
for (const [city, price] of delivery) {
console.log(`${city}: ${price.toFixed(2)} €`);
}
Salida
Sofia: 4.50 €
Plovdiv: 5.20 €
Varna: 6.00 €
Burgas: 6.00 €
Ruse: 5.80 €

Las ciudades salen en el orden en que se agregaron. Map mantiene el orden en que se agregan, y eso está prometido. toFixed(2) imprime el precio con dos cifras: si no, 6.0 sale como 6.

Cuando necesitas el orden alfabético, pasa las claves a un array y ordénalo. El Map no cambia:

const delivery = new Map();
delivery.set("Sofia", 4.5);
delivery.set("Plovdiv", 5.2);
delivery.set("Varna", 6.0);
delivery.set("Burgas", 6.0);
delivery.set("Ruse", 5.8);
for (const city of Array.from(delivery.keys()).sort()) {
console.log(`${city}: ${delivery.get(city).toFixed(2)} €`);
}
Salida
Burgas: 6.00 €
Plovdiv: 5.20 €
Ruse: 5.80 €
Sofia: 4.50 €
Varna: 6.00 €
Más precisamente, para curiosos
  • Si necesitas solo las claves, recorre keys(). Si necesitas solo los valores, recorre values():
console.log(Array.from(delivery.keys()).join(" "));
let sum = 0;
for (const price of delivery.values()) {
sum += price;
}
console.log(`Media: ${(sum / delivery.size).toFixed(2)} €`);
Salida
Sofia Plovdiv Varna Burgas Ruse
Media: 5.50 €
  • Array.from(...) crea un array con las claves, para que tengas join y sort.
  • Un objeto simple también mantiene el orden en que se agregan, pero no siempre: las claves que parecen números enteros salen primero, ordenadas como números.
const rooms = {};
rooms["Sala"] = "libre";
rooms["12"] = "ocupada";
rooms["3"] = "libre";
console.log(Object.keys(rooms));
Salida
[ '3', '12', 'Sala' ]

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:

const orders = "café té café cacao café té";
const counts = new Map();
for (const item of orders.split(" ")) {
counts.set(item, (counts.get(item) ?? 0) + 1);
}
console.log(counts);
Salida
Map(3) { 'café' => 3, 'té' => 2, 'cacao' => 1 }

Este es el uso más común de un diccionario: contar palabras, votos, errores por tipo, ventas por ciudad. Los artículos salen en el orden en que aparecieron por primera vez. Los paréntesis alrededor de counts.get(item) ?? 0 son necesarios: sin ellos, + 1 se calcula antes que ??, y la cantidad de un artículo ya visto no aumenta.

const orders = "café té café cacao café té";
const counts = new Map();
for (const item of orders.split(" ")) {
counts.set(item, (counts.get(item) ?? 0) + 1);
}
console.log(counts);
Paso a paso, en palabras
  1. 6 pedidos y un Map vacío.
  2. «café» no está: get da undefined, ?? lo convierte en 0, y set añade la clave con 1.
  3. «té» no está: get da undefined, ?? lo convierte en 0, y set añade la clave con 1. Un Map guarda el orden en que se añaden: la clave nueva es la última.
  4. «café» ya está: get da 1, y set reemplaza el valor por 2.
  5. «cacao» no está: get da undefined, ?? lo convierte en 0, y set añade la clave con 1. Un Map guarda el orden en que se añaden: la clave nueva es la última.
  6. «café» ya está: get da 2, y set reemplaza el valor por 3.
  7. «té» ya está: get da 1, y set 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.
  • JavaScript no tiene un contador listo. Escribirlo con get, ?? 0 y set es la forma habitual.

Pruébalo

Encuentra el error

El programa debería contar los pedidos. No se detiene ni muestra ningún error, pero en lugar de las cantidades imprime esto:
Salida
Map(2) { 'café' => NaN, 'té' => NaN }

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

Elige la línea con el error.

La idea

Set: 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:

const visits = ["ana@example.com", "bruno@example.com", "ana@example.com", "vera@example.com", "bruno@example.com"];
const visitors = new Set();
for (const email of visits) {
visitors.add(email);
}
console.log(`Visitas: ${visits.length}`);
console.log(`Visitantes únicos: ${visitors.size}`);
console.log(visitors.has("vera@example.com"));
Salida
Visitas: 5
Visitantes únicos: 3
true
  • add sobre un valor que ya está no hace nada. En los dos casos devuelve el propio conjunto, así que no te dice si agregó algo:
const seen = new Set();
console.log(seen.add("F-101") === seen);
Salida
true
  • has es tan rápido como has en un Map. size es el número de valores.
  • No hay índices. Un conjunto responde solo a la pregunta «¿está aquí?».
Más precisamente, para curiosos
  • Un Set mantiene el orden en que se agregan, como un Map. También puedes crear uno de una vez a partir de un array, y Array.from lo vuelve a convertir en array, por ejemplo para sort():
const tags = new Set(["java", "sql", "git", "java"]);
console.log(tags);
console.log(Array.from(tags).sort());
const backend = new Set(["java", "sql", "docker"]);
console.log(tags.intersection(backend));
console.log(tags.union(backend));
Salida
Set(3) { 'java', 'sql', 'git' }
[ 'git', 'java', 'sql' ]
Set(2) { 'java', 'sql' }
Set(4) { 'java', 'sql', 'git', 'docker' }
  • intersection deja solo los elementos que dos conjuntos tienen en común, y union los une. Los necesitarás en el ejercicio. Son nuevos: Node los tiene desde la versión 22, y los navegadores, desde 2024.

Pruébalo

Una factura repetida

El contador quiere ver los números de factura que se ingresaron dos veces. ¿Qué método pregunta si el número ya está en el conjunto?
const invoices = ["F-101", "F-102", "F-101", "F-103"];
const seen = new Set();
for (const number of invoices) {
  if (seen.(number)) {
    console.log(`Factura repetida: ${number}`);
  }
  seen.add(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(username, email), get(username) y size(). Por dentro es un Map. 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 Map.

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(id, ...keywords) y search(keyword), que devuelve un array con los números de los documentos que tienen esa palabra. Por dentro es un Map de arrays: para cada palabra, un array 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(name, email), addConnection(a, b) (la conexión va en los dos sentidos), friendsOf(name), common(a, b) (amigos en común) y emailOf(name). Guarda los contactos en un Map nombre → Contact, y las conexiones en un Map nombre → Set de nombres. friendsOf y common devuelven un array ordenado. 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. Una función generateUserId(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 un array de actividades: «París» → «la torre Eiffel», «el Louvre». Imprime el plan en el orden en que se agregaron los destinos. ¿Necesitas algo más que un Map simple?
  • 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 Map mejor opción que un array?
2. El Map menu ya tiene la clave "café" con el valor 2. ¿Qué hace menu.set("café", 3)?
3. ¿Qué devuelve get para una clave que no está?
4. Imprimes un Map. ¿En qué orden salen las claves?
5. En un objeto simple stock escribes stock[1] = "portátil" y después stock["1"] = "ratón". ¿Cuántas claves tiene stock?
6. ¿Qué devuelve add en un Set 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 ·