Ordenamiento por Cuentas (Counting Sort) en Python
El Counting Sort (ordenamiento por cuentas) es un algoritmo de clasificación lineal que resulta extremadamente rápido cuando el rango de valores a ordenar es limitado y conocido de antemano. En este artículo descubrirás su funcionamiento interno, cómo implementarlo en Python, casos de uso reales y comparativas con otros algoritmos de ordenación.
1. ¿Cómo funciona Counting Sort?
Counting Sort no compara elementos entre sí; en su lugar, cuenta cuántas veces aparece cada valor y luego reconstruye la secuencia ordenada a partir de esas cuentas.
- Recopilación de frecuencias: Se crea un arreglo
countde tamañok(dondekes el valor máximo + 1) y se incrementa la posición correspondiente a cada elemento del arreglo original. - Acumulación (opcional): Si se necesita mantener la estabilidad del algoritmo, se transforma
counten un arreglo acumulativo que indica la posición final de cada clave. - Construcción del resultado: Se recorre el arreglo original (de atrás a delante para estabilidad) y se coloca cada elemento en su posición final usando
count.
2. Complejidad y características
Ventajas
- Tiempo lineal
O(n + k)cuandokno es mucho mayor quen. - Estable (preserva el orden original de elementos con la misma clave).
- Excelente rendimiento en datos con rango pequeño (por ejemplo, edades, puntuaciones).
- Sin comparaciones, lo que lo hace útil en entornos donde las comparaciones son costosas.
Desventajas
- Requiere memoria adicional
O(k)para el arreglo de cuentas. - No es adecuado para rangos muy amplios (p.ej., ordenar números de 0 a 1,000,000 con pocos elementos).
- Solo funciona directamente con datos discretos (enteros o claves mapeables a enteros).
3. Implementación en Python
A continuación, se muestra una implementación "Pythonic" que incluye manejo de argumentos opcionales para estabilidad y soporte de rangos negativos.
def counting_sort(arr, min_val=None, max_val=None, stable=True):
"""Ordenamiento por cuentas (Counting Sort).
Parámetros
----------
arr : list[int]
Lista de enteros a ordenar.
min_val, max_val : int, opcional
Límite inferior y superior del rango. Si no se especifican, se calculan a partir de ``arr``.
stable : bool, default True
Mantener la estabilidad del algoritmo.
"""
if not arr:
return []
# Determinar rango si no se provee
if min_val is None:
min_val = min(arr)
if max_val is None:
max_val = max(arr)
k = max_val - min_val + 1 # tamaño del vector de cuentas
count = [0] * k
# 1️⃣ Contar frecuencias
for num in arr:
count[num - min_val] += 1
if stable:
# 2️⃣ Transformar en acumulativo para estabilidad
for i in range(1, k):
count[i] += count[i - 1]
# 3️⃣ Construir salida estable (recorremos de derecha a izquierda)
output = [0] * len(arr)
for num in reversed(arr):
idx = num - min_val
count[idx] -= 1
output[count[idx]] = num
return output
else:
# Variante no estable (más simple y ligeramente más rápida)
output = []
for i, freq in enumerate(count):
output.extend([i + min_val] * freq)
return output
# Ejemplo de uso
if __name__ == "__main__":
data = [4, 2, 2, 8, 3, 3, 1]
print("Original:", data)
print("Ordenado (estable):", counting_sort(data))
print("Ordenado (no estable):", counting_sort(data, stable=False))
4. Casos de uso reales
- Ordenar edades de una población: Cuando el rango es 0‑120, Counting Sort es prácticamente instantáneo.
- Clasificación de puntuaciones de exámenes: Rangos típicos 0‑100.
- Pre‑procesamiento en algoritmos de radix sort: Counting Sort actúa como sub‑rutina estable para cada dígito.
- Histogramas y visualizaciones: El mismo conteo usado para ordenar sirve para generar histogramas.
5. Comparativa con otros algoritmos de ordenación lineal
Counting Sort vs. Radix Sort
| Aspecto | Counting Sort | Radix Sort |
|---|---|---|
| Tipo de datos | Enteros con rango limitado | Enteros o strings de longitud fija |
| Complejidad | O(n + k) | O(d·(n + b)) (d = dígitos, b = base) |
| Memoria adicional | O(k) | O(n + b) |
| Estabilidad | Opcional (por defecto sí) | Sí, por diseño |
| Ventaja principal | Simplicidad y velocidad para rangos pequeños | Escalable a rangos muy grandes |
Counting Sort vs. Bucket Sort
| Aspecto | Counting Sort | Bucket Sort |
|---|---|---|
| Tipo de datos | Enteros discretos | Datos uniformemente distribuidos (float) |
| Complejidad | O(n + k) | O(n + b) (b = número de cubetas) |
| Memoria adicional | O(k) | O(b) |
| Estabilidad | Sí (con acumulativo) | Depende de la ordenación interna de cubetas |
| Ventaja principal | Exactitud en conteo | Manejo de datos continuos |
6. Buenas prácticas y optimizaciones
- Pre‑calcula el rango: Si el rango es conocido (p.ej., edades 0‑120), evita el cálculo de
minymaxen tiempo de ejecución. - Usa
array.array('I')onumpy.ndarrayparacount: Reduce el uso de memoria y acelera los accesos. - Paraleliza el conteo: En entornos multi‑core, divide el arreglo en bloques y combina los vectores de cuentas con una reducción.
- Gestiona rangos negativos: Desplaza el índice restando el valor mínimo, como se muestra en la función anterior.
- Evita la fase de acumulado cuando la estabilidad no es requerida: Ahorra una pasada O(k).
7. Depuración y troubleshooting
Los errores más comunes al usar Counting Sort son:
- Desbordamiento del vector
count: Ocurre cuando el rangokes demasiado grande para la memoria disponible. Solución: validarkantes de crear el arreglo o recurrir a un algoritmo diferente (p.ej., Radix Sort). - Olvidar el desplazamiento para valores negativos: Sin el ajuste
num - min_valse producirá unIndexError. - Pérdida de estabilidad: Si se omite la fase acumulativa y el algoritmo necesita estabilidad (por ejemplo, al usar Counting Sort dentro de Radix Sort), el resultado será incorrecto.
8. Escalabilidad y rendimiento en producción
En sistemas de alto rendimiento, Counting Sort se emplea en pipelines de procesamiento de datos donde:
- Los registros son streaming y el rango de una columna es finito (p.ej., códigos de estado HTTP 100‑599).
- Se necesita una ordenación estable y de bajo coste de CPU.
Ejemplo de benchmark (Python 3.11, 1 M de enteros en rango 0‑255):
import random, timeit
def benchmark():
data = [random.randint(0, 255) for _ in range(1_000_000)]
t = timeit.timeit(lambda: counting_sort(data), number=3)
print(f"Counting Sort (1M, 0‑255): {t:.3f}s")
benchmark()
# Resultado típico: ~0.45 s (mucho más rápido que Timsort ~0.85 s)
9. Conclusiones
Counting Sort es una herramienta poderosa en el arsenal del desarrollador cuando se enfrentan a datos con rangos limitados y se requiere velocidad lineal. Su implementación en Python es sencilla, extensible y, con las optimizaciones adecuadas, puede superar a los algoritmos de ordenación comparativa en escenarios concretos.
Ordenamiento por Cuentas (Counting Sort) en Python: Guía Completa, Ejemplos y Comparativas