Los algoritmos de sorting basados únicamente en comparaciones tienen un límite de Ω(n log n) en el peor caso para ordenar claves arbitrarias.
Counting sort y radix sort pueden superar ese límite porque usan información adicional sobre las claves: rango numérico, dígitos o representación. No contradicen el límite; trabajan bajo un modelo diferente.
Un algoritmo por comparación debe distinguir entre n! órdenes posibles. Cada comparación tiene al menos dos resultados y puede verse como una bifurcación en un árbol de decisiones.
Para tener al menos n! hojas, la altura debe satisfacer:
Texto
2^h ≥ n!
h ≥ log₂(n!) = Ω(n log n)
Merge sort y heap sort alcanzan O(n log n) en el peor caso. Quick sort lo alcanza en promedio, pero puede degradarse a O(n²).
Counting sort funciona cuando las claves son enteros dentro de un rango manejable.
Si los valores están entre min y max, define:
Texto
k = max - min + 1
Cuenta cuántas veces aparece cada clave y reconstruye el resultado.
TypeScript
functioncountingSort(values:number[]):number[]{if(values.length <=1)return[...values];let min = values[0];let max = values[0];for(const value of values){if(!Number.isInteger(value)){thrownewTypeError("Counting sort requires integers");}
min = Math.min(min, value);
max = Math.max(max, value);}const range = max - min +1;const counts =Array(range).fill(0);for(const value of values){
counts[value - min]+=1;}const result:number[]=[];for(let offset =0; offset < counts.length; offset +=1){for(let count =0; count < counts[offset]; count +=1){
result.push(offset + min);}}return result;}
Cada pasada debe usar un sorting estable. Si no, una pasada posterior destruye el orden establecido por las anteriores.
TypeScript
functionradixSortNonNegative(values:number[]):number[]{if(values.some((value)=>!Number.isSafeInteger(value)|| value <0)){thrownewTypeError("This version requires non-negative safe integers");}let result =[...values];const max = result.length ===0?0: Math.max(...result);for(let divisor =1; Math.floor(max / divisor)>0; divisor *=10){const buckets:number[][]=Array.from({ length:10},()=>[]);for(const value of result){const digit = Math.floor(value / divisor)%10;
buckets[digit].push(value);}
result = buckets.flat();}return result;}
Esta implementación es educativa. Math.max(...result) puede fallar con arrays enormes por la cantidad de argumentos y flat() crea arrays adicionales. Una versión de producción usaría counting sort estable por dígito y un máximo calculado con loop.
Distribuye valores en buckets según un rango, ordena cada bucket y concatena.
Su buen rendimiento depende de una distribución aproximadamente uniforme. Con una distribución adversarial, todos los elementos pueden caer en el mismo bucket y el costo depende del algoritmo interno.
La estabilidad importa al ordenar varias veces por criterios. Puedes ordenar primero por nombre y luego de forma estable por departamento para conservar el orden secundario.
En trabajo real, no reimplementes sorting sin una razón. La implementación educativa enseña invariantes; la biblioteca del runtime suele ser mejor para producción.
¿Por qué counting sort puede ser peor que merge sort aunque su fórmula parezca lineal?
Respuesta
Porque su costo incluye k, el tamaño del rango. Si k es mucho mayor que n, inicializar y recorrer el array de frecuencias puede consumir más tiempo y memoria que O(n log n).