🚀 ¿Qué son y para qué sirven?
Las estructuras de datos son formas especializadas de organizar, almacenar y procesar información en la memoria del ordenador. Piensa en ellas como diferentes tipos de contenedores físicos en la vida real.
El Archivo
Un armario ordenado alfabéticamente. Fácil buscar, lento añadir nuevos papeles entre medio (Array).
La Cadena
Gente agarrada de la mano. Fácil insertar a alguien en medio, lento encontrar a la persona número 100 (Lista Enlazada).
La Pila de Platos
El último plato que pones es el primero que lavas (Pila / Stack).
La Cola del Súper
El primero en llegar es el primero en ser atendido (Cola / Queue).
🧠 ¿Cómo funciona la memoria RAM?
Antes de hablar de Arrays o Listas, necesitas entender dónde viven. La Memoria RAM es literalmente una cuadrícula inmensa de cajas (como una hoja de Excel gigante).
Cada "cajita" tiene una dirección única (ej. 0x01). El procesador (CPU) puede acceder a cualquier caja del mundo de forma instantánea si conoce su dirección. Todo lo que programamos, acaba guardado en estas cajas.
📦 Arrays (Arreglos)
Un Array almacena elementos en cajas de memoria estrictamente contiguas (una justo al lado de la otra).
Bloque de memoria reservado y contiguo
Búsqueda Matemática (Instantánea)
Como sabemos que el array empieza en 0x01 y cada letra ocupa 1 caja, si quieres el tercer elemento (índice 2), el PC hace: 0x01 + 2 = 0x03. Salta directo. Es hiper-rápido.
Inserción Traumática
Si quieres meter la "Z" al principio del Array, el ordenador tiene que mover "A", "B" y "C" físicamente a las cajas siguientes para hacer hueco. Mover 1 millón de elementos es extremadamente ineficiente.
const letras: string[] = ["A", "B", "C"];
// O(1) -> Acceso instantáneo por matemáticas (0x01 + index)
const primero = letras[0];
// O(n) -> Insertar al principio obliga a desplazar todo a la derecha
letras.unshift("Z");
letras = ["A", "B", "C"]
# O(1) -> Acceso directo en memoria contigua
primero = letras[0]
# O(n) -> El .insert(0) es muy ineficiente en grandes volúmenes
letras.insert(0, "Z")
// NOTA: Arrays.asList es solo un atajo de Java para no hacer .add() 3 veces.
// Lo verdaderamente importante es que ArrayList maneja la memoria por nosotros.
ArrayList<String> letras = new ArrayList<>(Arrays.asList("A", "B", "C"));
// O(1) -> Salto de memoria matemático directo
String primero = letras.get(0);
// O(n) -> ¡Desplazamiento traumático!
letras.add(0, "Z");
⛓️ Listas Enlazadas (Linked Lists)
Para solucionar el problema de mover elementos del Array, nacen las Listas Enlazadas. La memoria ya no está junta. Cada elemento (Nodo) guarda su valor y una flecha (puntero) indicando la caja de memoria donde vive el siguiente.
Inserción Mágica
Insertar/Borrar al principio es instantáneo. Solo creas una caja nueva en cualquier lugar libre (ej. 0x99) y le dices que su flecha apunte al 0x15.
Búsqueda Lenta
Para buscar el tercer elemento, no puedes usar matemáticas. Tienes que ir a "A", leer su flecha, ir a "B", leer su flecha, y llegar a "C". Tienes que dar saltos.
class Nodo {
// Cada nodo tiene el dato y la "flecha" al siguiente
constructor(public valor: string, public siguiente: Nodo | null = null) {}
}
// NOTA PEDAGÓGICA: Creamos los nodos "de atrás hacia adelante" a mano
// solo para que veas cómo se conectan físicamente las flechas.
// En producción, usarías un método automático miLista.insertar("A").
const nodoC = new Nodo("C");
const nodoB = new Nodo("B", nodoC); // B apunta a C
const nodoA = new Nodo("A", nodoB); // A apunta a B
// O(1) -> Mágico: insertar la Z al principio no mueve el resto
const nodoZ = new Nodo("Z", nodoA);
class Nodo:
def __init__(self, valor, siguiente=None):
self.valor = valor
self.siguiente = siguiente
# Replicando el diagrama A -> B -> C
nodoC = Nodo("C")
nodoB = Nodo("B", nodoC)
nodoA = Nodo("A", nodoB)
# O(1) -> La memoria es dispersa, solo enlazamos la flecha
nodoZ = Nodo("Z", nodoA)
class Nodo {
String valor;
Nodo siguiente;
public Nodo(String v, Nodo s) {
valor = v;
siguiente = s;
}
}
// Instanciamos al revés para que cada flecha apunte al que ya existe
Nodo nodoC = new Nodo("C", null);
Nodo nodoB = new Nodo("B", nodoC);
Nodo nodoA = new Nodo("A", nodoB);
// Inserción O(1) en O(1)
Nodo nodoZ = new Nodo("Z", nodoA);
⏱️ Complejidad y Big O Notation
Ahora entiendes el problema real: buscar en una Lista de 1 millón de elementos te obliga a dar 1 millón de saltos. Mover 1 millón de elementos en un Array bloquea tu CPU. Para medir este "dolor", usamos la Notación Big O.
| Notación | Nombre | Ejemplo de la vida real |
|---|---|---|
| O(1) | Constante | Insertar al inicio en Lista Enlazada o leer índice en Array. Tarda lo mismo sin importar el tamaño. |
| O(log n) | Logarítmica | Buscar en un diccionario ordenado. Abres por la mitad y descartas la mitad cada vez. |
| O(n) | Lineal | Buscar un elemento concreto saltando por nodos en una Lista Enlazada. |
| O(n log n)* | Linealítmica | Ordenar un Array enorme con algoritmos eficientes como Merge Sort o Quick Sort. |
| O(n²) | Cuadrática | Comparar cada elemento de un Array con el resto (bucles anidados). |
| O(2ⁿ)* | Exponencial | Romper una contraseña a la fuerza bruta probando combinaciones recursivamente. |
| O(n!)* | Factorial | Calcular todas las rutas posibles del "Problema del Viajante". Matemáticamente infernal. |
* Nota: Estas tres complejidades marcadas con asterisco aparecen principalmente al estudiar Algoritmos (como ordenación o fuerza bruta), no tanto en las operaciones CRUD básicas de Estructuras de Datos.
🥞 Pilas y 🛒 Colas
¿Magia? No, son estructuras conceptuales implementadas "secuestrando" a los Arrays. Una Pila es solo una clase que esconde un Array y prohíbe usar métodos que no sean push() (meter) y pop() (sacar).
Pila (Stack)
LIFO: Último en entrar, primero en salir.
Ej: Botón "Atrás".
Cola (Queue)
FIFO: Primero en entrar, primero en salir.
Ej: Cola de impresión.
class Pila {
private items: string[] = []; // El Array oculto que hace el trabajo sucio
// Solo permitimos meter al final y sacar del final
push(plato: string) { this.items.push(plato); } // O(1)
pop(): string | undefined { return this.items.pop(); } // O(1)
}
const miPila = new Pila();
miPila.push("Plato 1");
miPila.push("Plato 2");
miPila.push("Plato 3");
miPila.pop(); // Saca y devuelve "Plato 3" (LIFO)
class Pila:
def __init__(self):
self.items = [] # Array subyacente
def push(self, plato):
self.items.append(plato) # O(1)
def pop(self):
return self.items.pop() # O(1)
miPila = Pila()
miPila.push("Plato 1")
miPila.push("Plato 2")
miPila.push("Plato 3")
miPila.pop() # Saca "Plato 3"
// Java ya tiene una clase Stack nativa, pero así se construiría
class Pila {
private ArrayList<String> items = new ArrayList<>();
public void push(String plato) { items.add(plato); }
// size() - 1 porque el primer elemento de un Array siempre es el índice 0
public String pop() { return items.remove(items.size() - 1); }
}
Pila miPila = new Pila();
miPila.push("Plato 1");
miPila.push("Plato 2");
miPila.push("Plato 3");
miPila.pop(); // Saca "Plato 3"
🗂️ Hash Tables (Diccionarios)
La estructura de datos más importante de la programación moderna. Permite acceso ultra-rápido `O(1)`. ¿Cómo es posible si no usamos índices numéricos como en los Arrays?
Resolución de colisión mediante Lista Enlazada en la posición 4.
La Función Hash (La Batidora)
Existe una función matemática que coge tu texto (ej. "Ana") y lo transforma irreversiblemente en un número (ej. `4`). Usamos ese número para guardar a Ana en la caja `4` de un Array gigante. Al buscar "Ana", la función vuelve a dar `4` y vas directo. ¡Instantáneo!
El Lado Oscuro: Las Colisiones
¿Qué pasa si al meter "Bob" la función hash también escupe un `4`? ¡Colisión! La caja `4` ya está ocupada por Ana. ¿La solución? Meter una Lista Enlazada dentro de la caja `4`. Ahora la caja `4` apunta a Ana, y la flecha de Ana apunta a Bob.
// En TypeScript/JS, los objetos literales y Maps son Hash Tables
const usuarios = new Map<string, string>();
// MÁGIA OCULTA: Al usar .set(), JS internamente calcula la función Hash.
// Para replicar el diagrama, asumimos que "Ana" y "Bob" colisionan en el mismo bucket.
usuarios.set("Ana", "Programadora");
usuarios.set("Bob", "Diseñador");
// O(1) -> Búsqueda instantánea (en el mejor de los casos)
const rolDeAna = usuarios.get("Ana"); // "Programadora"
# En Python, los diccionarios (dict) son Hash Tables altamente optimizados
usuarios = {}
# O(1) Inserción
usuarios["Ana"] = "Programadora"
usuarios["Bob"] = "Diseñador"
# O(1) Búsqueda inmediata sin iterar
rol_de_ana = usuarios["Ana"] # "Programadora"
// Java tiene HashMap, la implementación clásica de la estructura
HashMap<String, String> usuarios = new HashMap<>();
// MÁGIA OCULTA: .put() ejecuta la batidora matemática (hashCode) por debajo.
// Si "u_123" y "u_999" chocan, Java crea la Lista Enlazada por ti automáticamente.
usuarios.put("Ana", "Programadora");
usuarios.put("Bob", "Diseñador");
// O(1) Búsqueda directa
String rolDeAna = usuarios.get("Ana"); // "Programadora"
🌳 Árboles (Trees)
Los Árboles son simplemente Listas Enlazadas con múltiples flechas. Un TreeNode no tiene un `siguiente`, tiene un `izquierdo` y un `derecho`.
💡 Reglas del Árbol Binario de Búsqueda (BST):
- Todo elemento a la izquierda es siempre menor que el nodo padre.
- Todo elemento a la derecha es siempre mayor que el nodo padre.
- Este patrón permite buscar datos en tiempo ultra rápido O(log n), ya que descartas la mitad entera del árbol en cada salto.
class TreeNode {
constructor(
public valor: number,
public izquierdo: TreeNode | null = null,
public derecho: TreeNode | null = null
) {}
}
// Construyendo el diagrama: Raíz 10, Izq 5, Der 15
const raiz = new TreeNode(10);
raiz.izquierdo = new TreeNode(5);
raiz.derecho = new TreeNode(15);
raiz.izquierdo.izquierdo = new TreeNode(3);
raiz.izquierdo.derecho = new TreeNode(7);
raiz.derecho.izquierdo = new TreeNode(12);
raiz.derecho.derecho = new TreeNode(20);
class TreeNode:
def __init__(self, valor):
self.valor = valor
self.izquierdo = None
self.derecho = None
# El diagrama en código Python
raiz = TreeNode(10)
raiz.izquierdo = TreeNode(5)
raiz.derecho = TreeNode(15)
raiz.izquierdo.izquierdo = TreeNode(3)
raiz.izquierdo.derecho = TreeNode(7)
raiz.derecho.izquierdo = TreeNode(12)
raiz.derecho.derecho = TreeNode(20)
class TreeNode {
int valor;
TreeNode izquierdo;
TreeNode derecho;
public TreeNode(int v) {
valor = v;
}
}
TreeNode raiz = new TreeNode(10);
raiz.izquierdo = new TreeNode(5);
raiz.derecho = new TreeNode(15);
raiz.izquierdo.izquierdo = new TreeNode(3);
raiz.izquierdo.derecho = new TreeNode(7);
raiz.derecho.izquierdo = new TreeNode(12);
raiz.derecho.derecho = new TreeNode(20);
El Árbol Binario de Búsqueda (BST)
Regla de oro: El hijo izquierdo siempre es menor, el derecho es mayor. Esto permite buscar en O(log n) porque descartas la mitad del árbol en cada paso.
🕸️ Grafos (Graphs)
La red definitiva. Nodos conectados sin reglas estrictas arriba-abajo. Internet, rutas GPS, Facebook, todo es un Grafo. Pero un círculo dibujado en una pizarra no se puede programar, necesitamos almacenarlo en memoria.
Grafo no dirigido. Las aristas (líneas) mapean exactamente las conexiones de la lista de adyacencia del código inferior.
Listas de Adyacencia: Cómo se programa un Grafo
La forma más común y eficiente de programar un Grafo es usando un Diccionario (Hash Table) donde cada Nodo es una clave, y su valor es un Array con los Nodos a los que está conectado. ¡Todas las estructuras trabajando juntas!
// 'Record' es jerga de TS para un Diccionario (Objeto) de Texto -> Array
const redSocial: Record<string, string[]> = {
"Ana": ["Bob", "Eva", "Fran", "Carlos", "Zelda"], // Ana es un nodo súper-conector
"Fran": ["Bob", "Ana", "Carlos"],
"Carlos": ["Fran", "Ana", "Zelda"],
"Zelda": ["Carlos", "Ana", "Eva", "Mía"],
// ... y así sucesivamente para todos los nodos del diagrama
};
# En Python usamos un simple Dictionary de Listas
red_social = {
"Ana": ["Bob", "Eva", "Fran", "Carlos", "Zelda"],
"Fran": ["Bob", "Ana", "Carlos"],
"Zelda": ["Carlos", "Ana", "Eva", "Mía"],
"Bob": ["Ana", "Fran"],
"Eva": ["Ana", "Zelda", "Luis"],
"Luis": ["Eva"],
"Carlos": ["Ana", "Fran", "Zelda"],
"Mía": ["Zelda"]
}
// En Java usamos HashMap donde la llave es String y el valor un ArrayList
HashMap<String, List<String>> redSocial = new HashMap<>();
redSocial.put("Ana", Arrays.asList("Bob", "Eva", "Fran", "Carlos", "Zelda"));
redSocial.put("Fran", Arrays.asList("Bob", "Ana", "Carlos"));
redSocial.put("Zelda", Arrays.asList("Carlos", "Ana", "Eva", "Mía"));
redSocial.put("Bob", Arrays.asList("Ana", "Fran"));
redSocial.put("Eva", Arrays.asList("Ana", "Zelda", "Luis"));
redSocial.put("Luis", Arrays.asList("Eva"));
redSocial.put("Carlos", Arrays.asList("Ana", "Fran", "Zelda"));
redSocial.put("Mía", Arrays.asList("Zelda"));
El Eslabón Perdido: Algoritmos de Búsqueda
Ya sabes construir Árboles y Grafos (Los Sustantivos). ¿Pero cómo caminas por ellos para encontrar a una persona en la Red Social? Usamos los Verbos (Algoritmos):
- BFS (Búsqueda en Anchura): Busca primero en tus amigos directos, luego en los amigos de tus amigos. Ideal para encontrar el "camino más corto" (Ej: GPS o Recomendaciones).
- DFS (Búsqueda en Profundidad): Sigue un camino hasta el final sin parar, si no hay salida, retrocede. Ideal para salir de Laberintos o Sudokus.
📋 Cheatsheet Definitivo (Big O)
El "Machete" para tus entrevistas técnicas. Guarda esta tabla para recordar al instante el rendimiento de cada estructura en el peor de los casos (Time Complexity).
| Estructura | Acceso (Index) | Búsqueda (Search) | Inserción (Insert) | Borrado (Delete) |
|---|---|---|---|---|
| Array | O(1) | O(n) | O(n) | O(n) |
| Lista Enlazada | O(n) | O(n) | O(1) | O(1) |
| Hash Table | N/A | O(1) | O(1) | O(1) |
| BST (Árbol Binario)* | N/A | O(log n) | O(log n) | O(log n) |
| Pila (Stack) | N/A | O(n) | O(1) | O(1) |
| Cola (Queue) | N/A | O(n) | O(1) | O(1) |
*Asumiendo un árbol balanceado. Si está desbalanceado, todo se degrada a O(n).
🚀 Casos de Uso en Producción
¿Dónde encontramos estas estructuras en sistemas reales de empresas Top?
Redis (Hash Tables O(1))
El problema: Amazon necesita cargar tu carrito de compras en 1 milisegundo. Si busca en el disco duro (Base de Datos tradicional), es lento y el cliente se va.
La solución: Redis es un software que usa una Hash Table gigante directamente en la RAM. Mete tu User_ID en la batidora matemática, salta directamente a la caja de memoria exacta O(1) y devuelve tu carrito instantáneamente.
PostgreSQL (Árboles B-Tree O(log n))
El problema: Haces un SELECT * FROM users WHERE age = 30 en una tabla con 10 millones de usuarios. Leerlos uno por uno (Array O(n)) congelaría el servidor.
La solución: Cuando le dices a SQL que cree un Index, internamente crea un Árbol Balanceado. Al buscar "30", va saltando por los nodos descartando mitades enteras (Ej: "¿30 es mayor o menor que 50? Menor, me voy por la izquierda"). Llega al dato en nanosegundos O(log n).
RabbitMQ / Kafka (Colas FIFO)
El problema: Salen a la venta las entradas de un concierto top y 100,000 personas hacen clic en "Comprar" a la vez. Si el servidor intenta cobrar 100,000 tarjetas de crédito simultáneamente, colapsa.
La solución: Se meten todas las compras en una Cola (Queue) de memoria. El primero que hizo clic, entra primero (FIFO). Luego, unos servidores "trabajadores" van haciendo pop() de la cola uno a uno, procesando las ventas al ritmo que pueden sin saturarse. Nadie pierde su turno.