Lecciones · #2 de 11

Listas Enlazadas

Nodos dispersos por la memoria, cosidos con punteros

Singly linked list · head → tail

init

Pruébalo: Presiona recorrer para seguir el puntero de cabeza hacia un índice — cuenta los saltos. Luego inserta o elimina en la misma posición — el recableado de punteros es O(1), pero aún tienes que llegar al lugar.

Por qué importa

Las listas enlazadas te enseñan a pensar en términos de punteros — un concepto fundamental en ciencias de la computación. Aunque en la práctica suelen ser más lentas que los arreglos, abren la puerta a entender árboles, grafos y la gestión de memoria.

La idea

Imagina una búsqueda del tesoro. Cada pista te dice dónde encontrar la siguiente pista. No puedes saltar a la pista #5 — debes seguir la cadena desde el inicio.

Eso es una lista enlazada. Cada nodo contiene datos y un puntero al siguiente nodo.

El compromiso está invertido respecto a los arreglos:

Cuando necesitas insertar/eliminar con frecuencia al principio o después de un nodo conocido, ganan las listas enlazadas. Cuando necesitas acceso aleatorio, ganan los arreglos.

Puntos clave

Profundizando

Simple vs doble vs circular

Variante Cada nodo tiene Propiedad notable
Enlace simple solo next menor uso de memoria; solo se puede avanzar
Enlace doble next + prev eliminación O(1) dada una referencia al nodo (no hace falta encontrar el previo); se recorre en ambos sentidos
Circular (simple o doble) la cola → next = head planificación por turnos (round-robin); el iterador nunca termina
Lista XOR (curiosidad) un puntero = prev XOR next costo de memoria de enlace simple con doble enlace. Rompe la prebúsqueda de caché — académica.

La característica estrella de la lista doblemente enlazada es el empalme (splice) O(1): dado un nodo n al que ya tienes un puntero, puedes eliminarlo de la lista con dos escrituras de puntero:

n.prev.next = n.next;
n.next.prev = n.prev;

Sin recorrido. Por esto cada implementación de caché LRU bajo el sol usa una lista doblemente enlazada respaldada por una tabla hash: la tabla hash te da búsqueda O(1) del nodo por clave; la lista doblemente enlazada te permite mover ese nodo O(1) al frente en cada acceso.

Las variantes circulares sustentan los planificadores por turnos (round-robin) — todo planificador de procesos de un sistema operativo que da a cada hilo ejecutable una porción de tiempo tiene una lista circular en alguna parte. El puntero “next” del último hilo apunta de vuelta al primero; simplemente sigues recorriendo.

Sobrecarga de memoria — los números reales

Un nodo de lista de enlace simple que contiene un int32 en un sistema de 64 bits:

[ 4 bytes de datos ][ 4 bytes de relleno ][ 8 bytes de puntero next ] = 16 B

Los datos son 4 bytes, el almacenamiento son 16 bytes — 4× de sobrecarga por elemento. Una lista de enlace doble con los mismos datos:

[ 4 bytes de datos ][ 4 bytes de relleno ][ 8 bytes next ][ 8 bytes prev ] = 24 B

6× de sobrecarga. Y estos nodos no son secuenciales en memoria — cada uno es una asignación de heap separada, así que recorrer la lista es una fiesta de fallos de caché. Los mismos int32 en un Vec<i32> se empaquetan en 4 bytes cada uno, contiguos, prebuscados por la CPU. Para un bucle de un millón de elementos, el arreglo puede ser de 50 a 100× más rápido a pesar de un Big-O idéntico.

La lección: las listas enlazadas son la elección correcta cuando realmente necesitas un empalme (splice) O(1) con una referencia a un nodo activo (caché LRU, planificador del SO, listas intrusivas en estructuras de datos del kernel). Para todo lo demás, prefiere los arreglos — consulta la sección de localidad de caché de la lección de arreglos.

Detalles matemáticos

Complejidad temporal
Access by index: O(n)
Insert at head: O(1)
Insert after node: O(1)
Delete node (given prev): O(1)
Search: O(n)
Espacio
Cada nodo almacena datos + puntero(s)
Memory per node: sizeof(T) + sizeof(pointer)

Implementación

Nodo de lista enlazada

struct Node<T> {
    data: T,
    next: Option<Box<Node<T>>>,
}

// Insertar en la cabeza: O(1)
fn push_front(head: &mut Option<Box<Node<T>>>, data: T) {
    let new_node = Box::new(Node {
        data,
        next: head.take(),
    });
    *head = Some(new_node);
}

Cuándo usarla

Práctica

Tres problemas clásicos de listas enlazadas, cada uno resaltando un patrón de punteros diferente:

→ Explora todos los problemas de práctica