Algoritmo Factorial en Python: Conceptos, Implementaciones y Buenas Prácticas
1. ¿Qué es el factorial?
El factorial de un número entero no negativo n se define como:
n! = 1 × 2 × 3 × … × n (con 0! = 1)
Se utiliza en combinatoria (permutaciones, combinaciones), análisis de algoritmos y series de Taylor.
1.1. Propiedades matemáticas relevantes
n! = n × (n‑1)!(relación recursiva)(n+1)! = (n+1) × n!n! ≈ sqrt(2πn) (n/e)^n(aproximación de Stirling)
2. Complejidad algorítmica
En cualquier implementación que multiplique los números del 1 al n, la complejidad temporal es O(n) y la complejidad espacial varía según el enfoque:
| Implementación | Tiempo | Espacio |
|---|---|---|
| Iterativa (bucle) | O(n) | O(1) |
| Recursiva simple | O(n) | O(n) (pila) |
| Tail‑recursiva (Python < 3.8 no optimiza) | O(n) | O(n) |
| Memoizada/LRU‑Cache | O(n) (una sola evaluación) | O(n) |
3. Implementaciones en Python
3.1. Versión iterativa (más eficiente en espacio)
def factorial_iter(n: int) -> int:
"""Calcula n! usando un bucle while.
Lanza ValueError si n es negativo.
"""
if n < 0:
raise ValueError("El factorial no está definido para números negativos")
result = 1
while n > 1:
result *= n
n -= 1
return result
3.2. Versión recursiva clásica
def factorial_rec(n: int) -> int:
if n < 0:
raise ValueError("Número negativo")
return 1 if n == 0 else n * factorial_rec(n - 1)
3.3. Memoización con functools.lru_cache
from functools import lru_cache
@lru_cache(maxsize=None)
def factorial_memo(n: int) -> int:
if n < 0:
raise ValueError("Número negativo")
return 1 if n == 0 else n * factorial_memo(n - 1)
3.4. Usando math.factorial (C‑optimizado)
import math
def factorial_stdlib(n: int) -> int:
return math.factorial(n) # lanza ValueError para n < 0
3.5. Con functools.reduce (estilo funcional)
from functools import reduce
def factorial_reduce(n: int) -> int:
if n < 0:
raise ValueError("Número negativo")
return reduce(lambda x, y: x * y, range(1, n + 1), 1)
3.6. Vectorizado con numpy.prod (útil en cálculos masivos)
import numpy as np
def factorial_numpy(n: int) -> int:
if n < 0:
raise ValueError("Número negativo")
return int(np.prod(np.arange(1, n + 1)))
3.7. Paralelismo con multiprocessing (para n muy grandes)
import multiprocessing as mp
def _partial_product(rng):
prod = 1
for i in rng:
prod *= i
return prod
def factorial_parallel(n: int, workers: int = None) -> int:
if n < 0:
raise ValueError("Número negativo")
workers = workers or mp.cpu_count()
# dividir el rango en bloques
chunk = (n // workers) + 1
ranges = [range(i, min(i + chunk, n + 1)) for i in range(1, n + 1, chunk)]
with mp.Pool(workers) as pool:
partials = pool.map(_partial_product, ranges)
result = 1
for p in partials:
result *= p
return result
4. Casos de uso reales
- Combinaciones:
C(n, k) = n! / (k! (n‑k)!) - Permutaciones: número de formas de ordenar n objetos = n!
- Distribuciones de probabilidad: distribución binomial, Poisson, etc.
- Series de Taylor: los coeficientes de la expansión de e^x usan n!
5. Buenas prácticas y troubleshooting
- Validación de entrada: siempre verifica que
nsea entero no negativo. - Manejo de excepciones: captura
ValueErrory muestra mensajes claros al usuario. - Evita recursión profunda: Python tiene límite de recursión (~1000). Para
n > 995prefiere la versión iterativa omath.factorial. - Control de consumo de memoria: la memoización es útil sólo si vas a calcular varios factoriales repetidos; de lo contrario, consume memoria innecesaria.
- Seguridad: nunca confíes en valores de entrada sin validar; un número extremadamente grande puede consumir CPU y provocar Denial‑of‑Service.
- Testing: usa
pytestcon casos límite (0, 1, valores grandes como 1000) y verifica contramath.factorial.
Ejemplo de test con pytest
import pytest, math
from mymodule import factorial_iter, factorial_rec, factorial_memo
@pytest.mark.parametrize("func", [factorial_iter, factorial_rec, factorial_memo])
def test_factorial_small(func):
for n in range(0, 10):
assert func(n) == math.factorial(n)
def test_factorial_negative():
with pytest.raises(ValueError):
factorial_iter(-5)
6. Comparativa con otros lenguajes
| Lenguaje | Implementación típica | Tiempo (n=10⁵) | Observaciones |
|---|---|---|---|
| Python (math.factorial) | C‑optimizado interno | ≈ 0.004 s | Soporta enteros arbitrarios |
| Python (iterativo puro) | Bucle while | ≈ 0.018 s | Más legible, sin dependencias C |
| C (uint64) | Loop for | ≈ 0.001 s | Desbordamiento a partir de 20! |
| JavaScript (Number) | Recursivo | ≈ 0.025 s | Límite de precisión en 21! |
Algoritmo Factorial en Python: Guía Completa, Ejemplos y Buenas Prácticas