Programación · Estructuras de datos
Lista enlazada explicada: cómo funciona, código y complejidad
Respuesta corta
Una lista enlazada es una secuencia de nodos en la que cada nodo guarda un valor y una referencia (puntero) al siguiente nodo. A diferencia de un arreglo, los nodos no están contiguos en memoria, así que insertar o eliminar un nodo en una posición conocida cuesta O(1), pero encontrar el k-ésimo elemento cuesta O(n) porque hay que recorrer desde la cabeza. Una lista doblemente enlazada guarda además un puntero al nodo anterior.
Qué problema resuelve una lista enlazada
Un arreglo (en Python, una list) guarda sus elementos en un único bloque contiguo de memoria. Leer a[i] es inmediato, pero insertar al principio desplaza todos los demás elementos una posición a la derecha, lo que cuesta O(n). La lista enlazada evita ese desplazamiento: cada elemento vive en su propio nodo y los nodos se conectan mediante referencias. Para insertar solo hay que cambiar un par de referencias.
Las listas enlazadas son la base de muchas otras estructuras: pilas, colas, los cubos de una tabla hash (encadenamiento separado), cachés LRU y listas de adyacencia en grafos.
La idea en palabras sencillas
Piensa en una búsqueda del tesoro: cada pista te dice dónde está la siguiente. No puedes saltar a la pista 7; primero tienes que seguir las pistas 1 a 6. La lista solo recuerda el primer nodo, llamado cabeza (head). El último nodo no apunta a nada (None).
| Tipo | Cada nodo guarda | Uso típico |
|---|---|---|
| Simplemente enlazada | valor, next | pilas, colas simples, cubos de tablas hash |
| Doblemente enlazada | valor, next, prev | deques, cachés LRU, búferes de editores de texto |
| Circular | como la simple o la doble, pero la cola apunta de vuelta a la cabeza | planificación round-robin, búferes circulares |
Ejemplo resuelto: insertar y eliminar
Partimos de la lista 3 -> 7 -> 9. Insertamos 5 después del 3 y luego eliminamos el 7.
- Crea un nodo nuevo con el valor 5.
- Haz que
new.nextapunte al nodo que sigue al 3 (el nodo 7). - Haz que
node3.nextapunte al nodo nuevo. La lista queda3 -> 5 -> 7 -> 9. - Para eliminar el 7, busca el nodo anterior (el 5) recorriendo desde la cabeza.
- Asigna
node5.next = node5.next.next, que se salta el 7. La lista queda3 -> 5 -> 9. El recolector de basura de Python libera el nodo inalcanzable.
Implementación en Python de una lista simplemente enlazada
Esta clase guarda una referencia a la cabeza y un contador de tamaño. Permite añadir al principio y al final, eliminar el primer valor que coincida e iterar.
class Node:
def __init__(self, value, next=None):
self.value = value
self.next = next
class LinkedList:
def __init__(self):
self.head = None
self.size = 0
def push_front(self, value):
self.head = Node(value, self.head)
self.size += 1
def push_back(self, value):
node = Node(value)
if self.head is None:
self.head = node
else:
cur = self.head
while cur.next:
cur = cur.next
cur.next = node
self.size += 1
def remove(self, value):
prev, cur = None, self.head
while cur and cur.value != value:
prev, cur = cur, cur.next
if cur is None:
return False # not found
if prev is None:
self.head = cur.next # removing the head
else:
prev.next = cur.next
self.size -= 1
return True
def __iter__(self):
cur = self.head
while cur:
yield cur.value
cur = cur.nextitems = LinkedList()
for x in [3, 7, 9]:
items.push_back(x)
items.push_front(1)
items.remove(7)
print(list(items), items.size) # [1, 3, 9] 3Aquí push_back recorre toda la lista, por lo que es O(n). Mantener una referencia extra tail lo deja en O(1); es lo que hacen las implementaciones reales de colas.
Operaciones clásicas: invertir y detectar ciclos
Invertir una lista in situ es la pregunta de entrevista más común sobre listas enlazadas. Recorre la lista una vez y da la vuelta a cada puntero next.
def reverse(head):
prev = None
cur = head
while cur:
nxt = cur.next # remember the rest
cur.next = prev # turn the pointer around
prev, cur = cur, nxt
return prev # new head
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False
head = Node(1, Node(2, Node(3)))
head = reverse(head)
print(head.value, head.next.value, head.next.next.value) # 3 2 1
print(has_cycle(head)) # Falsehas_cycle es el algoritmo de la tortuga y la liebre de Floyd: un puntero lento avanza un paso y uno rápido avanza dos. Si hay un bucle, el rápido acaba alcanzando al lento; si no, llega a None. Usa O(1) de memoria adicional.
Complejidad temporal y espacial
| Operación | Lista simplemente enlazada | list de Python (arreglo dinámico) | Motivo |
|---|---|---|---|
| Acceder al k-ésimo elemento | O(n) | O(1) | la lista enlazada debe recorrer k nodos |
| Insertar o eliminar al principio | O(1) | O(n) | un arreglo desplaza todos los elementos |
| Insertar o eliminar tras un nodo conocido | O(1) | O(n) | solo cambian referencias |
| Añadir al final | O(1) con puntero a la cola | O(1) amortizado | ambos son baratos |
| Buscar un valor | O(n) | O(n) | ambos recorren elemento a elemento |
| Memoria adicional | una o dos referencias por nodo | algo de capacidad sobrante | los nodos son objetos independientes |
En la práctica, los arreglos suelen ser más rápidos incluso en operaciones donde la lista enlazada tiene mejor notación Big O, porque sus elementos están contiguos en memoria y aprovechan bien la caché de la CPU. Los nodos de una lista enlazada están dispersos.
Variantes y errores comunes
- Olvidar la lista vacía: comprueba siempre
head is Noneantes de leerhead.next. - Perder el resto de la lista al sobrescribir
nextantes de guardarlo (miranxtenreverse). - No actualizar
headal eliminar el primer nodo, otailal eliminar el último. - Usar recursión en listas muy largas: el límite de recursión por defecto de Python es de unos 1000 marcos, así que el código iterativo es más seguro.
- Un nodo cabeza ficticio (centinela) elimina la mayoría de los casos especiales: todo nodo real tiene entonces un nodo anterior.
- En una lista doblemente enlazada, cada inserción y eliminación debe actualizar cuatro referencias; olvidar una corrompe la lista sin avisar.
Cuándo usar una lista enlazada y qué usar en Python
Usa una lista enlazada cuando insertes y elimines en los extremos o en el medio, en posiciones de las que ya tienes una referencia, y rara vez necesites acceso aleatorio. Ejemplos: una caché LRU (lista doblemente enlazada más diccionario hash), la lista de bloques libres de un asignador de memoria, el historial de deshacer o los cubos de una tabla hash.
En el código Python de todos los días rara vez escribirás la tuya. Para una cola o deque, usa collections.deque, que da O(1) al añadir y extraer por ambos extremos. Para secuencias generales, usa list. Escribe tu propia lista enlazada para aprender, para entrevistas y cuando necesites empalmar nodos en O(1), como en una caché LRU.
from collections import deque
q = deque([1, 2, 3])
q.appendleft(0)
q.append(4)
print(q.popleft(), q.pop(), list(q)) # 0 4 [1, 2, 3]Preguntas frecuentes
¿Una list de Python es una lista enlazada?
No. Una list de Python es un arreglo dinámico de referencias almacenadas de forma contigua, por lo que indexar es O(1) e insertar al principio es O(n). collections.deque es lo más parecido a una lista enlazada que trae Python para usar como cola.
¿Por qué acceder en una lista enlazada es O(n)?
Los nodos no están en direcciones predecibles. La única forma de llegar al k-ésimo nodo es empezar en la cabeza y seguir k referencias next.
¿Cuándo es mejor una lista doblemente enlazada que una simple?
Cuando necesitas eliminar un nodo teniendo solo una referencia a él, o moverte hacia atrás. Con un puntero prev, eliminar es O(1) sin buscar el nodo anterior, a costa de una referencia extra por nodo.
¿Cómo se encuentra el medio de una lista enlazada en una sola pasada?
Usa dos punteros: uno avanza de uno en uno y el otro de dos en dos. Cuando el rápido llega al final, el lento está en el medio.
¿Qué es un nodo centinela?
Un nodo ficticio colocado antes del primer nodo real (y a veces después del último). No guarda datos, pero hace que todo nodo real tenga un vecino, de modo que el código de inserción y eliminación necesita menos casos especiales para la lista vacía y la cabeza.