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: 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
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.
count["comparisons"] += 1Un diccionario guarda la cuenta, así que la función puede cambiarla sin la palabra clave global.
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.
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.
assert sort(data) == sorted(data)assert detiene el script si un ordenamiento se equivoca de orden: una prueba integrada en el programa.
math.ceil(math.log2(math.factorial(n)))La cota inferior: log₂ del número de órdenes posibles, n!, redondeado hacia arriba.
Pruébalo tú
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.
¿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.
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
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.