LinkedList en Python práctica invertir ordenar buscar diagramas

LinkedList en Python — 3 programas reales que consolidan la estructura

La LinkedList en Python práctica real es lo que toca ahora. En el artículo anterior vimos la teoría, qué es un nodo, cómo se enlazan, cómo insertar, recorrer y eliminar. Ahora construimos tres programas que van más allá de las operaciones básicas y que son exactamente el tipo de ejercicio que aparece en el segundo parcial de FP2. Cada operación viene con su diagrama para que puedas ver qué pasa en la cadena en cada paso.

Partimos de la clase LinkedList completa del artículo de teoría, la copiamos en un archivo linked_list.py y la importamos en cada programa.

LinkedList en Python práctica — Programa 1: Buscar y contar elementos con condiciones

Este programa extiende la LinkedList con métodos de búsqueda que van más allá del simple contiene(). En FP2 se pide operaciones como contar cuántos elementos cumplen una condición, encontrar el máximo o el mínimo, o devolver todos los elementos que pasen un filtro.

La clave de todos estos métodos es siempre el mismo patrón de recorrido: actual = primero, avanzar mientras actual is not None. Lo que cambia es lo que haces con cada nodo.

from linked_list import LinkedList

class LinkedListBusqueda(LinkedList):
    """LinkedList extendida con operaciones de búsqueda."""

    def contar(self, condicion=None):
        """
        Cuenta los elementos que cumplen la condición.
        Si no se pasa condición cuenta todos.
        """
        contador = 0
        for valor in self:
            if condicion is None or condicion(valor):
                contador += 1
        return contador

    def maximo(self):
        """Devuelve el valor máximo. Lanza ValueError si la lista está vacía."""
        if self.es_vacia():
            raise ValueError("La lista está vacía")
        maximo = None
        for valor in self:
            if maximo is None or valor > maximo:
                maximo = valor
        return maximo

    def minimo(self):
        """Devuelve el valor mínimo. Lanza ValueError si la lista está vacía."""
        if self.es_vacia():
            raise ValueError("La lista está vacía")
        minimo = None
        for valor in self:
            if minimo is None or valor < minimo:
                minimo = valor
        return minimo

    def filtrar(self, condicion):
        """Devuelve una nueva LinkedList con los elementos que cumplen la condición."""
        resultado = LinkedListBusqueda()
        for valor in self:
            if condicion(valor):
                resultado.insertar_detras(valor)
        return resultado

    def suma(self):
        """Devuelve la suma de todos los elementos."""
        return sum(self)

    def media(self):
        """Devuelve la media. Lanza ValueError si la lista está vacía."""
        if self.es_vacia():
            raise ValueError("La lista está vacía")
        return self.suma() / len(self)

Ahora construimos la lista y probamos cada operación con su estado:

# Construimos la lista con estos valores
lista = LinkedListBusqueda()
for v in [15, 8, 42, 3, 27, 19, 8, 11, 35, 8]:
    lista.insertar_detras(v)

print("Lista inicial:")
print(lista)
# → 15 → 8 → 42 → 3 → 27 → 19 → 8 → 11 → 35 → 8 → None

print(f"\nLongitud: {len(lista)}")         # → 10
print(f"Suma:     {lista.suma()}")         # → 176
print(f"Media:    {lista.media():.2f}")    # → 17.60
print(f"Máximo:   {lista.maximo()}")       # → 42
print(f"Mínimo:   {lista.minimo()}")       # → 3

# Contar con condiciones
print(f"\nMayores que 15:  {lista.contar(lambda x: x > 15)}")   # → 4
print(f"Pares:           {lista.contar(lambda x: x % 2 == 0)}") # → 4
print(f"Cuántos valen 8: {lista.contar(lambda x: x == 8)}")     # → 3
print(f"Total elementos: {lista.contar()}")                       # → 10

# Filtrar — devuelve nueva lista
mayores = lista.filtrar(lambda x: x > 15)
print(f"\nMayores que 15:")
print(mayores)
# → 42 → 27 → 19 → 35 → None

pares = lista.filtrar(lambda x: x % 2 == 0)
print(f"\nSolo pares:")
print(pares)
# → 8 → 42 → 8 → 8 → None

# La lista original no se modifica
print(f"\nLista original intacta:")
print(lista)
# → 15 → 8 → 42 → 3 → 27 → 19 → 8 → 11 → 35 → 8 → None

Fíjate en que filtrar usa insertar_detras para construir la lista resultado, así mantiene el orden original. Si hubiera usado insertar_delante los elementos habrían salido en orden inverso.

LinkedList en Python práctica — Programa 2: Invertir una lista encadenada

Invertir una LinkedList es uno de los ejercicios clásicos de FP2 . La idea parece sencilla, dar la vuelta a la lista, pero la implementación requiere pensar con cuidado qué referencias cambias y en qué orden, porque si lo haces mal puedes perder parte de la lista.

Hay dos estrategias. La primera crea una lista nueva insertando cada elemento al principio, simple y fácil de visualizar. La segunda invierte la lista en su propio espacio, más eficiente pero más difícil de ver.

Estrategia 1 — Crear lista nueva (fácil de entender)

def invertir_nueva(self):
    """Devuelve una nueva LinkedList con los elementos en orden inverso."""
    resultado = LinkedList()
    for valor in self:                    # recorre de izquierda a derecha
        resultado.insertar_delante(valor) # inserta al principio cada vez
    return resultado

El truco es que insertar al principio invierte el orden automáticamente:

Lista original: 10 → 20 → 30 → None

Recorremos: 10, 20, 30
Insertamos al principio cada uno:

Después de insertar 10: 10 → None
Después de insertar 20: 20 → 10 → None
Después de insertar 30: 30 → 20 → 10 → None  ← invertida

Estrategia 2 — Invertir en el mismo lugar (in-place)

Esta estrategia no crea una lista nueva, reusa los mismos nodos cambiando a quién apunta cada uno. Es la que más confunde y la que más vale entender con el diagrama.

La idea es recorrer la lista con tres variables: anterior, actual y siguiente. En cada paso invertimos el puntero de actual, en vez de apuntar hacia adelante, lo hacemos apuntar hacia atrás.

Estado inicial:
anterior=None  actual=[10]  siguiente=[20]
None  ←  [10] → [20] → [30] → None

Paso 1: actual=[10], invertimos su puntero
[10].siguiente = anterior (None)
┌─────────────────────────────────┐
│ None ← [10]   [20] → [30] → None│
└─────────────────────────────────┘
Avanzamos: anterior=[10], actual=[20], siguiente=[30]

Paso 2: actual=[20], invertimos su puntero
[20].siguiente = anterior ([10])
┌─────────────────────────────────┐
│ None ← [10] ← [20]   [30] → None│
└─────────────────────────────────┘
Avanzamos: anterior=[20], actual=[30], siguiente=None

Paso 3: actual=[30], invertimos su puntero
[30].siguiente = anterior ([20])
┌─────────────────────────────────┐
│ None ← [10] ← [20] ← [30]      │
└─────────────────────────────────┘
actual=None → terminamos

primero = anterior = [30]
Resultado: [30] → [20] → [10] → None  ✓
def invertir_inplace(self):
    """Invierte la lista en el mismo lugar, sin crear una nueva."""
    anterior = None
    actual = self._LinkedList__primero    # acceso al atributo privado

    while actual is not None:
        siguiente = actual.siguiente      # guardamos el siguiente ANTES de modificar
        actual.siguiente = anterior       # invertimos el puntero
        anterior = actual                 # avanzamos anterior
        actual = siguiente                # avanzamos actual

    self._LinkedList__primero = anterior  # el nuevo primero es el último original

Veamos ambas en acción:

lista = LinkedList()
for v in [10, 20, 30, 40, 50]:
    lista.insertar_detras(v)

print("Lista original:")
print(lista)
# → 10 → 20 → 30 → 40 → 50 → None

# Estrategia 1 — nueva lista
invertida = lista.invertir_nueva()
print("\nInvertida (nueva lista):")
print(invertida)
# → 50 → 40 → 30 → 20 → 10 → None

print("\nOriginal intacta:")
print(lista)
# → 10 → 20 → 30 → 40 → 50 → None

# Estrategia 2 — in-place
lista.invertir_inplace()
print("\nLista después de invertir_inplace:")
print(lista)
# → 50 → 40 → 30 → 20 → 10 → None

# Casos especiales
lista_vacia = LinkedList()
lista_vacia.invertir_inplace()
print("\nLista vacía invertida:")
print(lista_vacia)
# → None

lista_un_elemento = LinkedList()
lista_un_elemento.insertar_delante(42)
lista_un_elemento.invertir_inplace()
print("\nLista de un elemento invertida:")
print(lista_un_elemento)
# → 42 → None

El error más común al implementar la inversión in-place es olvidar guardar siguiente antes de modificar actual.siguiente. Si modificas el puntero primero, pierdes la referencia al resto de la lista y no puedes avanzar.

# MAL — perdemos la lista
actual.siguiente = anterior    # ← ya no podemos llegar al siguiente nodo
siguiente = actual.siguiente   # ← esto ahora da anterior, no el siguiente real

# BIEN — guardamos el siguiente ANTES de modificar
siguiente = actual.siguiente   # ← guardamos primero
actual.siguiente = anterior    # ← luego modificamos

LinkedList en Python práctica — Programa 3: Ordenar una LinkedList por inserción

La ordenación por inserción en una LinkedList es el algoritmo de ordenación más natural para esta estructura. La idea: recorres la lista de izquierda a derecha y, para cada elemento, lo insertas en la posición correcta dentro de la parte ya ordenada.

Hay dos implementaciones. La primera construye una lista nueva ordenada. La segunda ordena en el mismo lugar.

Implementación — construir lista ordenada nueva

Para cada valor de la lista original, lo insertamos en la posición correcta dentro de la lista resultado:

def insertar_ordenado(self, valor):
    """
    Inserta el valor en la posición correcta manteniendo el orden ascendente.
    Método auxiliar para la ordenación.
    """
    nuevo = self.Nodo(valor)

    # Caso 1 — lista vacía o valor menor que el primero
    if self.__primero is None or valor <= self.__primero.valor:
        nuevo.siguiente = self.__primero
        self.__primero = nuevo
        self.__longitud += 1
        return

    # Caso 2 — buscar la posición correcta
    actual = self.__primero
    while actual.siguiente is not None and actual.siguiente.valor < valor:
        actual = actual.siguiente

    # Insertar después de actual
    nuevo.siguiente = actual.siguiente
    actual.siguiente = nuevo
    self.__longitud += 1


def ordenar(self):
    """Devuelve una nueva LinkedList con los elementos ordenados."""
    resultado = LinkedList()
    for valor in self:
        resultado.insertar_ordenado(valor)
    return resultado

Veamos cómo funciona el insertar_ordenado paso a paso con la lista [3, 1, 4, 1, 5]:

Insertamos 3:
[] → insertar 3 → lista vacía, es el primero
3 → None

Insertamos 1:
3 → None → insertar 1
1 < 3 → va antes del primero
1 → 3 → None

Insertamos 4:
1 → 3 → None → insertar 4
4 > 1, 4 > 3, fin de lista → va al final
1 → 3 → 4 → None

Insertamos 1:
1 → 3 → 4 → None → insertar 1
1 <= 1 → va antes del primero
1 → 1 → 3 → 4 → None

Insertamos 5:
1 → 1 → 3 → 4 → None → insertar 5
5 > todos → va al final
1 → 1 → 3 → 4 → 5 → None  ✓
lista = LinkedList()
for v in [15, 3, 42, 8, 27, 1, 19]:
    lista.insertar_detras(v)

print("Lista original:")
print(lista)
# → 15 → 3 → 42 → 8 → 27 → 1 → 19 → None

ordenada = lista.ordenar()
print("\nLista ordenada:")
print(ordenada)
# → 1 → 3 → 8 → 15 → 19 → 27 → 42 → None

print("\nOriginal intacta:")
print(lista)
# → 15 → 3 → 42 → 8 → 27 → 1 → 19 → None

# Casos especiales
lista_vacia = LinkedList()
print(f"\nLista vacía ordenada: {lista_vacia.ordenar()}")
# → None

lista_un_elemento = LinkedList()
lista_un_elemento.insertar_delante(42)
print(f"Lista de un elemento ordenada: {lista_un_elemento.ordenar()}")
# → 42 → None

lista_iguales = LinkedList()
for v in [5, 5, 5, 5]:
    lista_iguales.insertar_detras(v)
print(f"Lista de iguales ordenada: {lista_iguales.ordenar()}")
# → 5 → 5 → 5 → 5 → None

# Demostración completa con insertar_ordenado
print("\nInsertar_ordenado paso a paso:")
resultado = LinkedList()
for v in [15, 3, 42, 8, 27]:
    resultado.insertar_ordenado(v)
    print(f"Después de insertar {v:2d}: {resultado}")

Salida del paso a paso:

Después de insertar 15:  15 → None
Después de insertar  3:   3 → 15 → None
Después de insertar 42:   3 → 15 → 42 → None
Después de insertar  8:   3 →  8 → 15 → 42 → None
Después de insertar 27:   3 →  8 → 15 → 27 → 42 → None

El patrón que debes llevarte

De los tres programas hay tres patrones que se repiten en todos los ejercicios de FP2 con LinkedList:

# PATRÓN 1 — recorrido con acumulador
# Para contar, sumar, buscar máximo/mínimo
acumulador = valor_inicial
actual = primero
while actual is not None:
    acumulador = operacion(acumulador, actual.valor)
    actual = actual.siguiente
return acumulador

# PATRÓN 2 — tres variables para invertir
# Para cualquier operación que invierta punteros
anterior = None
actual = primero
while actual is not None:
    siguiente = actual.siguiente    # guardar ANTES de modificar
    actual.siguiente = anterior     # invertir
    anterior = actual               # avanzar
    actual = siguiente              # avanzar
primero = anterior

# PATRÓN 3 — inserción en posición correcta
# Para cualquier inserción que mantenga orden
actual = primero
while actual.siguiente is not None and condicion(actual.siguiente.valor):
    actual = actual.siguiente
nuevo.siguiente = actual.siguiente  # conectar al resto
actual.siguiente = nuevo            # conectar al nuevo

Visualízalo con Python Tutor

La inversión in-place es donde Python Tutor más brilla para LinkedList, puedes ver exactamente cómo las flechas cambian de dirección en cada paso. Ve a pythontutor.com y pega:

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

# Lista: 1 → 2 → 3 → None
n1 = Nodo(1)
n2 = Nodo(2)
n3 = Nodo(3)
n1.siguiente = n2
n2.siguiente = n3

primero = n1
anterior = None
actual = primero

while actual is not None:
    siguiente = actual.siguiente
    actual.siguiente = anterior
    anterior = actual
    actual = siguiente

primero = anterior    # nuevo primero: n3

Ejecuta paso a paso y observa cómo en cada iteración una flecha cambia de dirección. Cuando el bucle termina todas las flechas apuntan hacia la izquierda en vez de hacia la derecha.

Resumen y siguiente paso

En este artículo practicaste la LinkedList con tres programas reales. El programa de búsqueda demuestra que el patrón de recorrido es siempre el mismo, lo que cambia es la lógica dentro del bucle. La inversión muestra el patrón de tres variables que aparece en muchos algoritmos de LinkedList. La ordenación por inserción demuestra cómo insertar en la posición correcta manteniendo el orden.

En el siguiente artículo encontrarás ejercicios propuestos con solución para practicar por tu cuenta.


Python LinkedList — 3 real programs that consolidate the structure

In the previous article we covered the theory. Now we build three programs that go beyond basic operations, exactly the kind of exercise that appears in FP2 exams. Each operation comes with its diagram so you can see what happens in the chain at each step.

Program 1 — Search and count with conditions

from linked_list import LinkedList

class SearchLinkedList(LinkedList):

    def count(self, condition=None):
        """Count elements matching condition. Counts all if no condition."""
        counter = 0
        for value in self:
            if condition is None or condition(value):
                counter += 1
        return counter

    def maximum(self):
        if self.is_empty():
            raise ValueError("List is empty")
        max_val = None
        for value in self:
            if max_val is None or value > max_val:
                max_val = value
        return max_val

    def minimum(self):
        if self.is_empty():
            raise ValueError("List is empty")
        min_val = None
        for value in self:
            if min_val is None or value < min_val:
                min_val = value
        return min_val

    def filter(self, condition):
        """Returns new LinkedList with elements matching condition."""
        result = SearchLinkedList()
        for value in self:
            if condition(value):
                result.insert_back(value)
        return result

    def total(self):
        return sum(self)

    def average(self):
        if self.is_empty():
            raise ValueError("List is empty")
        return self.total() / len(self)
lst = SearchLinkedList()
for v in [15, 8, 42, 3, 27, 19, 8, 11, 35, 8]:
    lst.insert_back(v)

print(lst)
# → 15 → 8 → 42 → 3 → 27 → 19 → 8 → 11 → 35 → 8 → None

print(f"Sum:     {lst.total()}")         # → 176
print(f"Average: {lst.average():.2f}")   # → 17.60
print(f"Max:     {lst.maximum()}")       # → 42
print(f"Min:     {lst.minimum()}")       # → 3

print(f"Greater than 15: {lst.count(lambda x: x > 15)}")   # → 4
print(f"Even numbers:    {lst.count(lambda x: x % 2 == 0)}")# → 4
print(f"Count of 8s:     {lst.count(lambda x: x == 8)}")    # → 3

greater = lst.filter(lambda x: x > 15)
print(f"Greater than 15: {greater}")
# → 42 → 27 → 19 → 35 → None

evens = lst.filter(lambda x: x % 2 == 0)
print(f"Even only: {evens}")
# → 8 → 42 → 8 → 8 → None

Program 2 — Reverse a linked list

Two strategies:

Strategy 1 — New list (easy to understand)

def reverse_new(self):
    result = LinkedList()
    for value in self:
        result.insert_front(value)    # insert at front reverses order
    return result
Original:  10 → 20 → 30 → 40 → 50 → None

Insert 10 at front: 10 → None
Insert 20 at front: 20 → 10 → None
Insert 30 at front: 30 → 20 → 10 → None
Insert 40 at front: 40 → 30 → 20 → 10 → None
Insert 50 at front: 50 → 40 → 30 → 20 → 10 → None  ✓

Strategy 2 — In-place (more efficient)

Initial state:
previous=None  current=[10]  next_node=[20]
None  ←  [10] → [20] → [30] → None

Step 1: reverse [10]'s pointer
[10].next = None (previous)
None ← [10]   [20] → [30] → None
Advance: previous=[10], current=[20], next_node=[30]

Step 2: reverse [20]'s pointer
[20].next = [10] (previous)
None ← [10] ← [20]   [30] → None
Advance: previous=[20], current=[30], next_node=None

Step 3: reverse [30]'s pointer
[30].next = [20] (previous)
None ← [10] ← [20] ← [30]
current=None → done

first = previous = [30]
Result: 30 → 20 → 10 → None  ✓
def reverse_inplace(self):
    previous = None
    current = self._LinkedList__first

    while current is not None:
        next_node = current.next_node      # save BEFORE modifying
        current.next_node = previous       # reverse pointer
        previous = current                 # advance
        current = next_node                # advance

    self._LinkedList__first = previous
lst = LinkedList()
for v in [10, 20, 30, 40, 50]:
    lst.insert_back(v)

print(lst)                    # → 10 → 20 → 30 → 40 → 50 → None

reversed_new = lst.reverse_new()
print(reversed_new)           # → 50 → 40 → 30 → 20 → 10 → None
print(lst)                    # → 10 → 20 → 30 → 40 → 50 → None (unchanged)

lst.reverse_inplace()
print(lst)                    # → 50 → 40 → 30 → 20 → 10 → None

Most common mistake: modifying current.next_node before saving next_node, you lose the rest of the list.

# WRONG
current.next_node = previous   # ← pointer now lost
next_node = current.next_node  # ← this now gives previous, not the real next

# RIGHT
next_node = current.next_node  # ← save first
current.next_node = previous   # ← then modify

Program 3 — Sort by insertion

def insert_sorted(self, value):
    """Insert value in correct position maintaining ascending order."""
    new = self.Node(value)

    if self._LinkedList__first is None or value <= self._LinkedList__first.value:
        new.next_node = self._LinkedList__first
        self._LinkedList__first = new
        self._LinkedList__length += 1
        return

    current = self._LinkedList__first
    while current.next_node is not None and current.next_node.value < value:
        current = current.next_node

    new.next_node = current.next_node
    current.next_node = new
    self._LinkedList__length += 1


def sort(self):
    """Returns new sorted LinkedList."""
    result = LinkedList()
    for value in self:
        result.insert_sorted(value)
    return result

Step-by-step with [15, 3, 42, 8, 27]:

After inserting 15:   15 → None
After inserting  3:    3 → 15 → None
After inserting 42:    3 → 15 → 42 → None
After inserting  8:    3 →  8 → 15 → 42 → None
After inserting 27:    3 →  8 → 15 → 27 → 42 → None
lst = LinkedList()
for v in [15, 3, 42, 8, 27, 1, 19]:
    lst.insert_back(v)

print(lst)         # → 15 → 3 → 42 → 8 → 27 → 1 → 19 → None
sorted_lst = lst.sort()
print(sorted_lst)  # → 1 → 3 → 8 → 15 → 19 → 27 → 42 → None
print(lst)         # → 15 → 3 → 42 → 8 → 27 → 1 → 19 → None (unchanged)

The three patterns to take away

# PATTERN 1 — traversal with accumulator
# For count, sum, max, min, filter
accumulator = initial_value
current = first
while current is not None:
    accumulator = operation(accumulator, current.value)
    current = current.next_node

# PATTERN 2 — three variables for reversing
previous = None
current = first
while current is not None:
    next_node = current.next_node    # save BEFORE modifying
    current.next_node = previous     # reverse
    previous = current               # advance
    current = next_node              # advance
first = previous

# PATTERN 3 — insert in correct position
current = first
while current.next_node is not None and condition(current.next_node.value):
    current = current.next_node
new.next_node = current.next_node    # connect to rest
current.next_node = new              # connect new node

Publicaciones Similares

Un comentario

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *