Intermedio · Carrera de ordenamiento

Lección 6: Funciones y recursión

Construye en Python el patrón «Carrera de ordenamiento» y aprende por el camino:

  • funciones, y pasar una función como valor
  • la recursión: una función que se llama a sí misma sobre partes más pequeñas
  • un contador guardado en un diccionario
  • assert para comprobar un resultado
  • cómo crece el trabajo: n² frente a n·log n

Míralo primero

Se reproduce desde YouTube tras hacer clic.

sorting-race.py
"""Sorting race: four algorithms on the same shuffled list, counting their comparisons.
Run: python sorting-race.py   (needs: pip install numpy matplotlib)
"""
import math

import numpy as np
import matplotlib.pyplot as plt

plt.style.use("dark_background")

n = 100
data = list(np.random.default_rng().permutation(n))
count = {"comparisons": 0}


def less(a, b):  # every algorithm compares only through this, so we can count
    count["comparisons"] += 1
    return a < b


def bubble(xs):
    xs = xs[:]
    for end in range(len(xs) - 1, 0, -1):
        for i in range(end):
            if less(xs[i + 1], xs[i]):
                xs[i], xs[i + 1] = xs[i + 1], xs[i]
    return xs


def insertion(xs):
    xs = xs[:]
    for i in range(1, len(xs)):
        j = i
        while j > 0 and less(xs[j], xs[j - 1]):
            xs[j], xs[j - 1] = xs[j - 1], xs[j]
            j -= 1
    return xs


def merge(xs):
    if len(xs) < 2:
        return xs
    left, right = merge(xs[: len(xs) // 2]), merge(xs[len(xs) // 2 :])
    out = []
    while left and right:
        out.append(left.pop(0) if not less(right[0], left[0]) else right.pop(0))
    return out + left + right


def quick(xs):
    if len(xs) < 2:
        return xs
    pivot, rest = xs[0], xs[1:]
    smaller = [x for x in rest if less(x, pivot)]
    return quick(smaller) + [pivot] + quick([x for x in rest if x not in smaller])


results = {}
for name, sort in [("bubble", bubble), ("insertion", insertion), ("merge", merge), ("quick", quick)]:
    count["comparisons"] = 0
    assert sort(data) == sorted(data)
    results[name] = count["comparisons"]
print(results, "| at least log2(n!) =", math.ceil(math.log2(math.factorial(n))))

plt.figure(figsize=(7, 4))
plt.bar(list(results), list(results.values()), color=["#f0abfc", "#c4b5fd", "#5eead4", "#5eead4"])
plt.axhline(math.log2(math.factorial(n)), color="white", linestyle="--", label="log2(n!): no comparison sort does better")
plt.ylabel(f"comparisons for n = {n}")
plt.legend()
plt.show()

# Try: n = 1000 (bubble sort needs about n*n/2, merge sort about n*log2(n))

¿No tienes Python en este equipo? El cuaderno ejecuta el mismo script, los ejercicios y las respuestas en tu navegador, en Google Colab. El cuaderno está en inglés.

Línea a línea

  1. def less(a, b):

    Todos los algoritmos comparan a través de esta única función, así que las comparaciones se pueden contar en un solo sitio.

  2. count["comparisons"] += 1

    Un diccionario guarda la cuenta, así que la función puede cambiarla sin la palabra clave global.

  3. def merge(xs):

    Ordenamiento por mezcla: ordena cada mitad y luego mezcla las dos mitades ordenadas tomando siempre el menor de los elementos del principio.

  4. left, right = merge(xs[: len(xs) // 2]), merge(xs[len(xs) // 2 :])

    Recursión: merge se llama a sí misma sobre cada mitad, hasta que una parte tiene menos de dos elementos.

  5. assert sort(data) == sorted(data)

    assert detiene el script si un ordenamiento se equivoca de orden: una prueba integrada en el programa.

  6. math.ceil(math.log2(math.factorial(n)))

    La cota inferior: log₂ del número de órdenes posibles, n!, redondeado hacia arriba.

Pruébalo tú

  1. Para n = 100, ¿qué ordenamiento necesita menos comparaciones, y cuánto se acerca a la cota?

    Pista

    Ejecuta el script; muestra las cuentas y la cota.

    Respuesta

    El ordenamiento por mezcla, unas 540 frente a una cota de 525. El de burbuja necesita exactamente 4950, es decir, 100 · 99 ÷ 2.

  2. ¿Por qué este ordenamiento de burbuja siempre necesita n·(n − 1) ÷ 2 comparaciones?

    Pista

    Mira sus dos bucles.

    Respuesta

    Esta versión siempre recorre los dos bucles hasta el final: (n − 1) + (n − 2) + … + 1 = n·(n − 1) ÷ 2 comparaciones, sea cual sea el orden.

  3. Dale a quicksort una lista que ya está ordenada. ¿Qué le pasa a su cuenta?

    Pista

    data = list(range(n)).

    Respuesta

    Salta a 4950, tan mal como el de burbuja: con el primer elemento como pivote, cada división deja un lado vacío.

Cuestionario

1. ¿Qué es la recursión?
2. ¿Por qué less guarda su cuenta en un diccionario?
3. ¿En cuántos órdenes pueden llegar 4 elementos?
4. ¿Qué hace assert cuando su condición es False?
5. Para 1000 elementos, ¿cuántas comparaciones necesita más o menos el ordenamiento por mezcla?

Tarjetas de repaso

Lee el anverso, di el reverso para ti y luego abre la tarjeta.

  • Recursión

    Una función que resuelve un problema llamándose a sí misma sobre partes más pequeñas de él.

  • assert condition

    Detiene el programa con un error si la condición es False.

  • log₂(n!)

    El mínimo de comparaciones de sí o no que pueden distinguir n! órdenes: el límite de cualquier ordenamiento por comparación.

  • n² frente a n·log n

    Para 1000 elementos: unos 500 000 pasos frente a unos 10 000.

  • Pasar una función

    for name, sort in [("merge", merge), …]: las funciones son valores, como los números.

Estas tarjetas en su propia página →