2 - Conceptos esenciales previos

Fundamentos antes de codificar

Antes de escribir la primera función de una lista enlazada en Python necesitamos manejar con soltura nociones clave del lenguaje: cómo se construye un nodo con clases, qué implica trabajar con referencias, cómo se gestiona la memoria con el recolector de basura y cuáles son los errores que pueden dejar la estructura inconsistente. Cada subsección profundiza estos pilares para que, al llegar a la implementación, nada resulte misterioso.

2.1 Nodos y estructura básica

El nodo es la unidad mínima de una lista enlazada. Contiene datos y enlaces que definen su posición relativa. Visualmente se puede pensar como dos campos contiguos: a la izquierda el valor y a la derecha la referencia al siguiente nodo (denominado sig). Independientemente del tipo de lista (simple, doble o circular) todos comparten este concepto; en variantes dobles se agrega una referencia ant para retroceder.

class Nodo:
    def __init__(self, valor):
        self.valor = valor
        self.sig = None  # referencia al nodo siguiente

La definición anterior declara un Nodo con dos campos. Si necesitamos una lista doble, bastaría con agregar self.ant = None en el constructor. Como el head de la lista suele ser None cuando está vacía, los nodos se irán creando y enlazando a medida que los insertamos.

2.2 Clases en un solo archivo

Enfoquémonos en un diseño orientado a objetos, pero sin separar en módulos. Un solo archivo Python puede definir la clase Nodo, la clase ListaEnlazada y un bloque principal de ejecución. Esto mantiene el ejemplo auto contenido y fácil de probar con python archivo.py.

# lista.py

class Nodo:
    def __init__(self, valor):
        self.valor = valor
        self.sig = None


class ListaEnlazada:
    def __init__(self):
        self.cabeza = None

    def insertar_inicio(self, valor):
        nuevo = Nodo(valor)
        nuevo.sig = self.cabeza
        self.cabeza = nuevo

    def esta_vacia(self):
        return self.cabeza is None

La clase mantiene el estado interno y ofrece métodos que protegen los invariantes (por ejemplo, no hay forma de perder la cabeza sin que un método la actualice). Puedes seguir extendiendo la clase con operaciones de impresión y borrado, manteniendo todo en un solo archivo ejecutable.

2.3 Referencias y mutabilidad

Una lista enlazada en Python depende de referencias a objetos. Cada campo sig guarda una referencia al siguiente nodo, no una copia. Al reasignar sig conectamos o desconectamos nodos reales. Entender cómo se pasa y se reasigna una referencia es vital para evitar perder nodos.

lista = ListaEnlazada()
lista.insertar_inicio(42)

# las referencias se mueven dentro del método:
# nuevo.sig = self.cabeza
# self.cabeza = nuevo

El método insertar_inicio mueve las referencias en el orden correcto para que ningún nodo quede perdido. Siempre conviene inicializar la cabeza como None para detectar usos indebidos durante la depuración.

2.4 Memoria y recolección de basura

El crecimiento de la lista depende de crear nodos según sea necesario. Python gestiona la memoria automáticamente: cuando un objeto deja de tener referencias vivas, el recolector de basura lo libera. No hay que llamar a free, pero sí conviene romper enlaces para que los nodos queden libres al eliminar elementos.

class ListaEnlazada:
    # ...
    def eliminar_inicio(self):
        if self.cabeza is None:
            return None
        eliminado = self.cabeza
        self.cabeza = eliminado.sig
        eliminado.sig = None  # rompe el enlace para que el GC pueda liberar
        return eliminado.valor

Encapsular la eliminación en métodos que rompan referencias evita retener nodos en ciclos y delega en el recolector. En estructuras enlazadas grandes, cortar referencias circulares acelera la liberación.

2.5 Errores típicos con referencias

El trabajo con referencias también trae riesgos que conviene dominar antes de avanzar:

  • No actualizar sig en el orden correcto: si se pisa la referencia a la cabeza antes de enlazar el nuevo nodo, se pierden elementos.
  • Crear ciclos sin querer: asignar sig al nodo equivocado puede formar bucles que impidan liberar nodos o provoquen recorridos infinitos.
  • Mutar mientras se recorre: eliminar nodos dentro de un while sin guardar la referencia al siguiente rompe el recorrido.
  • Compartir nodos entre listas: reutilizar el mismo nodo en dos listas genera efectos colaterales cuando se modifica una de ellas.
  • Datos mutables compartidos: almacenar listas o diccionarios sin copiarlos puede causar cambios inesperados al modificarlos desde otro lugar.

Para prevenir estos problemas es recomendable encapsular las operaciones de inserción y borrado en funciones probadas, y escribir pruebas pequeñas que verifiquen el estado de la lista tras cada operación.

2.6 Código completo orientado a objetos

Con los conceptos anteriores podemos crear un archivo único listo para ejecutar con python listas_enlazadas.py. El mismo script define las clases y contiene el bloque principal.

# listas_enlazadas.py

class Nodo:
    def __init__(self, valor):
        self.valor = valor
        self.sig = None


class ListaEnlazada:
    def __init__(self):
        self.cabeza = None

    def insertar_inicio(self, valor):
        nuevo = Nodo(valor)
        nuevo.sig = self.cabeza
        self.cabeza = nuevo

    def imprimir(self):
        reco = self.cabeza
        while reco:
            print(reco.valor, end=" -> ")
            reco = reco.sig
        print("None")

    def limpiar(self):
        while self.cabeza:
            siguiente = self.cabeza.sig
            self.cabeza.sig = None
            self.cabeza = siguiente


def main():
    lista = ListaEnlazada()

    for valor in (30, 20, 10):
        lista.insertar_inicio(valor)

    print("Lista actual:")
    lista.imprimir()

    lista.limpiar()


if __name__ == "__main__":
    main()
# listas_enlazadas.py
Archivo único que define las clases, el flujo principal y se ejecuta directo.
insertar_inicio
Crea un nodo nuevo, enlaza su sig con la antigua cabeza y actualiza el estado interno.
imprimir
Recorre la lista acumulando valores y los muestra con flechas.
limpiar
Rompe enlaces uno por uno para permitir que el recolector libere cada nodo.
if __name__ == "__main__":
Permite ejecutar el archivo directamente sin depender de módulos externos.
Diagrama de nodos enlazados