Atlasingeniería

Estructuras de datosEstructuras linealesTema 1

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 kk, hay que seguir kk enlaces desde la cabeza. El acceso cuesta O(k)O(k) y, en el peor caso, O(n)O(n). No existe el salto directo que ofrece un arreglo con values[k] en O(1)O(1).

Buscar por valor también cuesta O(n)O(n) 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í

Tenés el nodo del medio de una lista simplemente enlazada y querés borrarlo. ¿Cuánto cuesta?

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 O(1)O(1), sin importar cuántos nodos existan. Insertar después de un nodo conocido también cuesta O(1)O(1): el nuevo apunta al sucesor y el nodo anterior pasa a apuntar al nuevo. Pero encontrar primero la posición sigue costando O(n)O(n).

La lista arranca con tres nodos. La cabeza apunta al primero.

1 / 4
El orden importa: si se pisa la cabeza antes de guardarla, la lista vieja queda sin nadie que la apunte.

Eliminar, y por qué hace falta el anterior

Eliminar la cabeza consiste en reemplazarla por head.next, también en O(1)O(1). 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 O(1)O(1).

Contra el arreglo dinámico

Un arreglo dinámico permite acceso por índice en O(1)O(1) 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 O(1)O(1).

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 O(n)O(n).

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ónArreglo dinámicoLista enlazada
Leer la posición iO(1)O(n): hay que caminar
Insertar o borrar al finalO(1) amortizadoO(1) con puntero a la cola
Insertar o borrar al principioO(n)O(1)
Insertar o borrar teniendo el nodoO(n)O(1)
Encontrar ese nodoO(1) si se sabe el índiceO(n) siempre
Memoria por elementoel valorel valor más uno o dos punteros
Recorrido secuencialrapidísimo: memoria contigualento: saltos por todo el heap
La fila resaltada es la que decide casi todos los casos reales y la que no aparece en la tabla de complejidades.

La tabla de arriba explica por qué la lista enlazada suena mejor de lo que es. Las filas de O(1)O(1) son ciertas y tienen una condición escondida: ya tener el nodo en la mano. Encontrarlo cuesta O(n)O(n), así que «insertar en el medio en O(1)O(1)» 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 O(1)O(1), 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?

¿Qué operación es realmente O(1) en una lista simplemente enlazada?
¿Qué agrega una lista doblemente enlazada?
Recorrer un arreglo y recorrer una lista son los dos O(n). ¿Por qué el arreglo suele ganar en la práctica?
¿Cuál es el error más frecuente al modificar enlaces?