Listas enlazadas y el costo de seguir punteros
Una lista enlazada cambia índices por referencias entre nodos. Eso permite insertar sin desplazar elementos, pero vuelve lineal el acceso y empeora la localidad de memoria.
Para este tema conviene tener claro:Arreglos, listas y matrices
En un arreglo, los elementos viven uno al lado del otro. En una lista enlazada, cada elemento puede estar en cualquier lugar y guarda la referencia al siguiente. Ese cambio parece pequeño, pero redefine qué operaciones son baratas.
El nodo: un valor y un enlace
La unidad de una lista es el nodo. Guarda un valor y un enlace:
type ListNode<T> = {
value: T;
next: ListNode<T> | null;
};La lista guarda una referencia a la cabeza. Desde ahí puede llegar al segundo nodo, luego
al tercero y así hasta encontrar null. Los nodos forman un orden lógico aunque no ocupen
posiciones consecutivas en memoria.
No hay salto: para llegar hay que recorrer
Para leer el elemento de posición , hay que seguir enlaces desde la cabeza. El
acceso cuesta y, en el peor caso, . No existe el salto directo que ofrece un
arreglo con values[k] en .
Buscar por valor también cuesta si la lista no tiene otro índice. Una lista enlazada no acelera búsquedas por sí sola; optimiza cambios locales cuando ya conocemos el nodo donde deben hacerse.
Antes de seguir, predecí
Insertar es cambiar una referencia
Insertar al principio requiere crear un nodo y cambiar una referencia:
const prepend = <T>(head: ListNode<T> | null, value: T): ListNode<T> => ({
value,
next: head,
});Eso cuesta , sin importar cuántos nodos existan. Insertar después de un nodo conocido también cuesta : el nuevo apunta al sucesor y el nodo anterior pasa a apuntar al nuevo. Pero encontrar primero la posición sigue costando .
La lista arranca con tres nodos. La cabeza apunta al primero.
Eliminar, y por qué hace falta el anterior
Eliminar la cabeza consiste en reemplazarla por head.next, también en . Para borrar
otro nodo en una lista simplemente enlazada necesitamos conocer el anterior, porque es quien
debe saltar sobre el nodo eliminado.
Una lista doblemente enlazada agrega a cada nodo una referencia previous. Consume más
memoria y exige mantener dos enlaces consistentes, pero permite borrar un nodo conocido o
recorrer hacia atrás en .
Contra el arreglo dinámico
Un arreglo dinámico permite acceso por índice en y suele aprovechar mejor la caché del procesador porque sus elementos están contiguos. Insertar en el medio puede desplazar muchos elementos, aunque agregar al final tiene costo amortizado .
Una lista evita esos desplazamientos, pero cada nodo agrega memoria para enlaces y cada salto puede llevar a otra región de memoria. En hardware real, recorrerla suele ser más lento que recorrer un arreglo incluso cuando ambos recorridos son .
Las invariantes que la sostienen
La implementación depende de invariantes simples: si la lista está vacía, la cabeza es
null; todo nodo salvo el último tiene sucesor; el último apunta a null. Si además se
guarda una cola, debe referir al último nodo y actualizarse al insertar o borrar extremos.
Los errores aparecen al cambiar enlaces en un orden que pierde parte de la cadena, dejar una cola vieja o crear un ciclo accidental. Antes de mutar, conviene identificar qué referencias deben sobrevivir a la operación.
Cuándo una lista enlazada, y cuándo no
| Operación | Arreglo dinámico | Lista enlazada |
|---|---|---|
| Leer la posición i | O(1) | O(n): hay que caminar |
| Insertar o borrar al final | O(1) amortizado | O(1) con puntero a la cola |
| Insertar o borrar al principio | O(n) | O(1) |
| Insertar o borrar teniendo el nodo | O(n) | O(1) |
| Encontrar ese nodo | O(1) si se sabe el índice | O(n) siempre |
| Memoria por elemento | el valor | el valor más uno o dos punteros |
| Recorrido secuencial | rapidísimo: memoria contigua | lento: saltos por todo el heap |
La tabla de arriba explica por qué la lista enlazada suena mejor de lo que es. Las filas de son ciertas y tienen una condición escondida: ya tener el nodo en la mano. Encontrarlo cuesta , así que «insertar en el medio en » sólo se cobra cuando el recorrido ya te dejó parado ahí, que es el caso de un iterador o de una estructura que guarda referencias a sus nodos.
Más a fondo · nivel seniorDónde sí gana, de verdad
Las listas enlazadas siguen vivas donde la ventaja no es la complejidad sino otra cosa.
En una caché LRU, cada entrada del diccionario guarda un puntero a su nodo en una lista doblemente enlazada. Usar una entrada la mueve al frente en , sin mover nada más, y sin esa combinación no hay LRU eficiente.
En un asignador de memoria o un sistema de archivos, la lista de bloques libres se enlaza usando el espacio de los propios bloques libres: no cuesta memoria extra, porque la memoria que usa es la que justamente no se está usando.
Y en estructuras concurrentes sin bloqueos, insertar cambiando un solo puntero con una operación atómica es mucho más simple que mantener un arreglo contiguo correcto entre varios hilos.
En los tres casos el motivo es el mismo y no está en la tabla: modificar la estructura no mueve nada de lo que ya estaba, y por lo tanto no invalida las referencias que otros tengan.
Cierre
Las listas enlazadas cambian acceso aleatorio por edición local. Son útiles cuando se insertan o eliminan nodos ya localizados y sus referencias deben permanecer estables. Si predominan el recorrido y el acceso por posición, un arreglo suele ser más simple, compacto y rápido.
Autoevaluación
¿Lo entendiste?
Práctica