Las estructuras de datos fundamentales (listas, árboles, colas, pilas, conjuntos, tablas hash) implementan relaciones discretas específicas. Comprender esas relaciones subyacentes permite diseñar estructuras eficientes y resolver problemas complejos.
Cada estructura de datos organiza información según una relación particular entre sus elementos. Una lista enlazada es una relación de precedencia lineal. Un árbol es una relación jerárquica donde cada nodo tiene un padre. Un grafo es una relación arbitraria entre nodos.
Al identificar qué relación discretas modelan nuestro problema, elegimos automáticamente la estructura más adecuada. Una mala elección causa ineficiencia; una buena elección hace que el código sea claro y rápido.
Una lista enlazada implementa una relación de precedencia lineal: cada elemento (nodo) apunta al siguiente. La relación es una función siguiente : Elemento → Elemento ∪ {null}.
// Nodo de la lista enlazada
class Nodo {
constructor(valor) {
this.valor = valor;
this.siguiente = null; // Relación: este nodo → próximo nodo
}
}
// Lista enlazada
class ListaEnlazada {
constructor() {
this.cabeza = null;
}
// Agregar elemento al final
agregar(valor) {
const nuevoNodo = new Nodo(valor);
if (!this.cabeza) {
this.cabeza = nuevoNodo;
return;
}
let actual = this.cabeza;
while (actual.siguiente) {
actual = actual.siguiente;
}
actual.siguiente = nuevoNodo;
}
// Recorrer la relación
recorrer() {
const resultado = [];
let actual = this.cabeza;
while (actual) {
resultado.push(actual.valor);
actual = actual.siguiente; // Seguir la relación
}
return resultado;
}
// Buscar un elemento
buscar(valor) {
let actual = this.cabeza;
while (actual) {
if (actual.valor === valor) return true;
actual = actual.siguiente;
}
return false;
}
// Eliminar un elemento
eliminar(valor) {
if (!this.cabeza) return false;
if (this.cabeza.valor === valor) {
this.cabeza = this.cabeza.siguiente;
return true;
}
let actual = this.cabeza;
while (actual.siguiente) {
if (actual.siguiente.valor === valor) {
actual.siguiente = actual.siguiente.siguiente;
return true;
}
actual = actual.siguiente;
}
return false;
}
}
const lista = new ListaEnlazada();
lista.agregar(10);
lista.agregar(20);
lista.agregar(30);
console.log(lista.recorrer()); // [10, 20, 30]
console.log(lista.buscar(20)); // true
lista.eliminar(20);
console.log(lista.recorrer()); // [10, 30]
Un árbol implementa una relación hijo-padre donde cada nodo tiene cero o más hijos, pero exactamente un padre (excepto la raíz). La relación es: padre : Nodo → Conjunto(Nodo).
// Nodo del árbol binario
class NodoArbol {
constructor(valor) {
this.valor = valor;
this.izquierda = null; // Relación: hijo izquierdo
this.derecha = null; // Relación: hijo derecho
}
}
// Árbol binario de búsqueda
class ArbolBinarioBusqueda {
constructor() {
this.raiz = null;
}
insertar(valor) {
if (this.raiz === null) {
this.raiz = new NodoArbol(valor);
} else {
this._insertarRecursivo(this.raiz, valor);
}
}
_insertarRecursivo(nodo, valor) {
if (valor < nodo.valor) {
if (nodo.izquierda === null) {
nodo.izquierda = new NodoArbol(valor);
} else {
this._insertarRecursivo(nodo.izquierda, valor);
}
} else {
if (nodo.derecha === null) {
nodo.derecha = new NodoArbol(valor);
} else {
this._insertarRecursivo(nodo.derecha, valor);
}
}
}
// Recorrido en orden (izquierda, nodo, derecha)
recorridoEnOrden(nodo = this.raiz) {
if (!nodo) return [];
return [
...this.recorridoEnOrden(nodo.izquierda),
nodo.valor,
...this.recorridoEnOrden(nodo.derecha)
];
}
// Recorrido por niveles (BFS)
recorridoPorNiveles() {
if (!this.raiz) return [];
const resultado = [];
const cola = [this.raiz];
while (cola.length > 0) {
const nodo = cola.shift();
resultado.push(nodo.valor);
if (nodo.izquierda) cola.push(nodo.izquierda);
if (nodo.derecha) cola.push(nodo.derecha);
}
return resultado;
}
// Buscar en árbol binario de búsqueda
buscar(valor, nodo = this.raiz) {
if (!nodo) return false;
if (nodo.valor === valor) return true;
if (valor < nodo.valor) return this.buscar(valor, nodo.izquierda);
return this.buscar(valor, nodo.derecha);
}
// Altura del árbol
altura(nodo = this.raiz) {
if (!nodo) return 0;
return 1 + Math.max(
this.altura(nodo.izquierda),
this.altura(nodo.derecha)
);
}
}
const arbol = new ArbolBinarioBusqueda();
[50, 30, 70, 20, 40, 60, 80].forEach(v => arbol.insertar(v));
console.log(arbol.recorridoEnOrden()); // [20, 30, 40, 50, 60, 70, 80]
console.log(arbol.recorridoPorNiveles()); // [50, 30, 70, 20, 40, 60, 80]
console.log(arbol.buscar(40)); // true
console.log(arbol.altura()); // 3
Una pila (stack) y una cola (queue) implementan relaciones lineales con reglas específicas de acceso:
// Pila (Stack)
class Pila {
constructor() {
this.elementos = [];
}
push(elemento) {
this.elementos.push(elemento);
}
pop() {
return this.elementos.pop();
}
esVacia() {
return this.elementos.length === 0;
}
tamaño() {
return this.elementos.length;
}
}
// Cola (Queue)
class Cola {
constructor() {
this.elementos = [];
}
enqueue(elemento) {
this.elementos.push(elemento);
}
dequeue() {
return this.elementos.shift();
}
esVacia() {
return this.elementos.length === 0;
}
tamaño() {
return this.elementos.length;
}
}
// Aplicación: Validar paréntesis balanceados (pila)
function paréntesesBalanceados(expresión) {
const pila = new Pila();
const pares = { ')': '(', ']': '[', '}': '{' };
for (const char of expresión) {
if (char === '(' || char === '[' || char === '{') {
pila.push(char);
} else if (char === ')' || char === ']' || char === '}') {
if (pila.esVacia() || pila.pop() !== pares[char]) {
return false;
}
}
}
return pila.esVacia();
}
console.log(paréntesesBalanceados("(a + [b * (c - d)])")); // true
console.log(paréntesesBalanceados("(a + [b * c - d)]")); // false
// Aplicación: Procesador de tareas con cola
class ProcesadorTareas {
constructor() {
this.cola = new Cola();
this.tarea_id = 0;
}
agregarTarea(descripción) {
this.tarea_id++;
this.cola.enqueue({
id: this.tarea_id,
descripción,
timestamp: new Date()
});
}
procesarSiguiente() {
if (this.cola.esVacia()) {
return null;
}
return this.cola.dequeue();
}
tareasEnEspera() {
return this.cola.tamaño();
}
}
const procesador = new ProcesadorTareas();
procesador.agregarTarea("Enviar email");
procesador.agregarTarea("Generar reporte");
procesador.agregarTarea("Actualizar base de datos");
console.log(procesador.tareasEnEspera()); // 3
console.log(procesador.procesarSiguiente());
// { id: 1, descripción: "Enviar email", timestamp: ... }
console.log(procesador.tareasEnEspera()); // 2
Un conjunto (set) implementa la relación de pertenencia. Un elemento pertenece o no al conjunto. Las operaciones típicas son unión, intersección y diferencia, que derivan de operaciones lógicas sobre la relación de pertenencia.
// Conjunto como relación de pertenencia
class Conjunto {
constructor(elementos = []) {
this.elementos = new Set(elementos);
}
agregar(elemento) {
this.elementos.add(elemento);
}
pertenece(elemento) {
return this.elementos.has(elemento);
}
eliminar(elemento) {
return this.elementos.delete(elemento);
}
// Unión: todos los elementos en A o en B
union(otro) {
return new Conjunto([...this.elementos, ...otro.elementos]);
}
// Intersección: elementos que están en A y en B
intersección(otro) {
const resultado = new Conjunto();
for (const elem of this.elementos) {
if (otro.pertenece(elem)) {
resultado.agregar(elem);
}
}
return resultado;
}
// Diferencia: elementos en A que no están en B
diferencia(otro) {
const resultado = new Conjunto();
for (const elem of this.elementos) {
if (!otro.pertenece(elem)) {
resultado.agregar(elem);
}
}
return resultado;
}
// Subconjunto: ¿todos los elementos de este están en otro?
esSubconjunto(otro) {
for (const elem of this.elementos) {
if (!otro.pertenece(elem)) return false;
}
return true;
}
tamaño() {
return this.elementos.size;
}
aArray() {
return Array.from(this.elementos);
}
}
// Aplicación: Gestión de permisos
const permisosAna = new Conjunto(["leer", "escribir", "ejecutar"]);
const permisosLuis = new Conjunto(["leer", "ejecutar"]);
console.log(permisosAna.pertenece("escribir")); // true
console.log(permisosLuis.pertenece("escribir")); // false
// Permisos que ambos tienen
console.log(permisosAna.intersección(permisosLuis).aArray());
// ["leer", "ejecutar"]
// Permisos que tiene Ana pero no Luis
console.log(permisosAna.diferencia(permisosLuis).aArray());
// ["escribir"]
// ¿Luis tiene un subconjunto de permisos de Ana?
console.log(permisosLuis.esSubconjunto(permisosAna)); // true
Una tabla hash implementa una función discreta eficiente: función : Clave → Valor. La tabla usa una función de hash para mapear claves a posiciones en un array, logrando acceso promedio O(1).
// Tabla hash simple (sin manejo de colisiones avanzado)
class TablaHash {
constructor(tamaño = 10) {
this.tamaño = tamaño;
this.tabla = Array(tamaño).fill(null).map(() => []);
}
_funcionHash(clave) {
let hash = 0;
for (let i = 0; i < clave.length; i++) {
hash += clave.charCodeAt(i);
}
return hash % this.tamaño;
}
// Insertar: mapear clave a valor
insertar(clave, valor) {
const indice = this._funcionHash(clave);
const bucket = this.tabla[indice];
// Buscar si la clave ya existe
for (let i = 0; i < bucket.length; i++) {
if (bucket[i][0] === clave) {
bucket[i][1] = valor; // Actualizar
return;
}
}
bucket.push([clave, valor]); // Agregar nuevo
}
// Obtener: recuperar valor dado la clave
obtener(clave) {
const indice = this._funcionHash(clave);
const bucket = this.tabla[indice];
for (const [k, v] of bucket) {
if (k === clave) return v;
}
return undefined;
}
// Existe: verificar si una clave está mapeada
existe(clave) {
return this.obtener(clave) !== undefined;
}
// Eliminar: remover mapeo
eliminar(clave) {
const indice = this._funcionHash(clave);
const bucket = this.tabla[indice];
for (let i = 0; i < bucket.length; i++) {
if (bucket[i][0] === clave) {
bucket.splice(i, 1);
return true;
}
}
return false;
}
}
// Aplicación: Caché de consultas
class Caché {
constructor() {
this.tabla = new TablaHash(50);
}
almacenar(query, resultado) {
this.tabla.insertar(query, resultado);
}
buscar(query) {
return this.tabla.obtener(query);
}
limpiar(query) {
this.tabla.eliminar(query);
}
}
const cache = new Caché();
cache.almacenar("SELECT * FROM usuarios", [{ id: 1, nombre: "Ana" }]);
cache.almacenar("SELECT * FROM productos", [{ id: 10, nombre: "Laptop" }]);
console.log(cache.buscar("SELECT * FROM usuarios"));
// [{ id: 1, nombre: "Ana" }]
console.log(cache.buscar("SELECT * FROM pedidos"));
// undefined
Un grafo (dirigido o no dirigido) es la estructura más general para modelar relaciones discretas. Permite cualquier relación binaria entre sus nodos, sin restricciones de linealidad ni jerarquía.
// Grafo no dirigido
class Grafo {
constructor() {
this.adyacencia = {};
}
agregarNodo(nodo) {
if (!this.adyacencia[nodo]) {
this.adyacencia[nodo] = [];
}
}
// Agregar arista (relación entre dos nodos)
agregarArista(nodo1, nodo2) {
this.agregarNodo(nodo1);
this.agregarNodo(nodo2);
if (!this.adyacencia[nodo1].includes(nodo2)) {
this.adyacencia[nodo1].push(nodo2);
}
if (!this.adyacencia[nodo2].includes(nodo1)) {
this.adyacencia[nodo2].push(nodo1);
}
}
// Recorrido en profundidad (DFS)
dfs(inicio, visitados = new Set()) {
visitados.add(inicio);
const resultado = [inicio];
for (const vecino of this.adyacencia[inicio] || []) {
if (!visitados.has(vecino)) {
resultado.push(...this.dfs(vecino, visitados));
}
}
return resultado;
}
// Recorrido en amplitud (BFS)
bfs(inicio) {
const visitados = new Set([inicio]);
const resultado = [];
const cola = [inicio];
while (cola.length > 0) {
const nodo = cola.shift();
resultado.push(nodo);
for (const vecino of this.adyacencia[nodo] || []) {
if (!visitados.has(vecino)) {
visitados.add(vecino);
cola.push(vecino);
}
}
}
return resultado;
}
// Encontrar el camino más corto (BFS)
caminoMásCorto(origen, destino) {
if (origen === destino) return [origen];
const visitados = new Set([origen]);
const cola = [[origen]];
while (cola.length > 0) {
const camino = cola.shift();
const ultimo = camino[camino.length - 1];
for (const vecino of this.adyacencia[ultimo] || []) {
if (vecino === destino) {
return [...camino, destino];
}
if (!visitados.has(vecino)) {
visitados.add(vecino);
cola.push([...camino, vecino]);
}
}
}
return null; // No hay camino
}
}
// Aplicación: Red de ciudades
const red = new Grafo();
red.agregarArista("Madrid", "Barcelona");
red.agregarArista("Madrid", "Sevilla");
red.agregarArista("Barcelona", "Bilbao");
red.agregarArista("Bilbao", "Sevilla");
console.log(red.dfs("Madrid"));
// ["Madrid", "Barcelona", "Bilbao", "Sevilla"] o similar
console.log(red.bfs("Madrid"));
// ["Madrid", "Barcelona", "Sevilla", "Bilbao"]
console.log(red.caminoMásCorto("Madrid", "Bilbao"));
// ["Madrid", "Barcelona", "Bilbao"]
Las estructuras de datos no son solo formas de almacenar información. Son implementaciones concretas de relaciones matemáticas discretas. Reconocer la estructura relacional subyacente permite elegir la herramienta adecuada para cada problema.
En el próximo tema exploraremos cómo las relaciones discretas se aplican específicamente en bases de datos, donde las tablas, claves foráneas e índices son todos reflejos de relaciones y funciones discretas.