Counting sort (ordenación por cuentas)
Ordena enteros de un rango pequeño sin compararlos: cuenta cuántos hay de cada valor y los coloca en orden según esas cuentas. O(n + k), lineal, y estable; es la base del radix sort.
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Counting sort
Escribe números del 0 al 15 y mira cómo se ordenan contando cuántos hay de cada uno, sin hacer ni una comparación.
- en la zona de trabajo
- fuera de juego
Paso 1
El mayor es 8: se crea cuenta con 9 casillas a cero, una por cada valor posible del 0 al 8.
1static int[] countingSort(int[] a, int max) {
2 int[] cuenta = new int[max + 1];
3 for (int x : a) cuenta[x]++;
4 for (int v = 1; v <= max; v++)
5 cuenta[v] += cuenta[v - 1];
6 int[] res = new int[a.length];
7 for (int i = a.length - 1; i >= 0; i--) {
8 cuenta[a[i]]--;
9 res[cuenta[a[i]]] = a[i];
10 }
11 return res;
12}Variables
- max
- 8
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Todos los algoritmos anteriores comparan elementos entre sí, y se puede demostrar que ninguno que compare puede bajar de n log n. Counting sort se salta esa barrera porque no compara: usa los propios valores como posiciones de un array.
Si los valores son enteros de 0 a k (notas de 0 a 10, edades de 0 a 120), basta un array cuenta de k + 1 casillas. Primero se recorre la entrada sumando uno en cuenta[valor]. Con eso ya se sabe, por ejemplo, que hay tres sietes; para tener los números ordenados se escribe cada valor tantas veces como indique su cuenta.
Si lo que se ordena son objetos con una clave (personas por edad), no se pueden «escribir tres sietes»: hay que llevar cada objeto a su sitio. Para eso se acumulan las cuentas (cada casilla pasa a decir cuántos hay con ese valor o menor, que es justo dónde termina ese valor en el resultado) y se recorre la entrada de atrás adelante colocando cada objeto en --cuenta[clave]. Recorrerla al revés hace que sea estable.
El coste es O(n + k): una pasada por los datos y otra por las cuentas. Si k es pequeño comparado con n, es lineal. Si el rango es enorme (números de 0 a mil millones), el array de cuentas no cabe: ahí entra el radix sort, que hace un counting sort por cada cifra.
Cuándo usarlo
- Enteros (o claves que se pueden convertir en enteros) en un rango pequeño: notas, edades, días del mes, códigos de 0 a 255.
- Cuando hay muchísimos datos y pocos valores distintos: millones de notas de 0 a 10.
- Como paso estable de radix sort, para ordenar números grandes, fechas o cadenas de longitud fija cifra a cifra o carácter a carácter.
Cuándo no
- Si el rango de valores es mucho mayor que el número de datos: crear y recorrer el array de cuentas cuesta más que ordenar.
- Con números decimales, cadenas arbitrarias u objetos que solo se pueden comparar.
Paso a paso
- Contar. Con
cuentade tamañomax + 1a ceros, se recorre la entrada sumando uno encuenta[a[i]]. - Acumular. Para cada valor
vdesde 1,cuenta[v] += cuenta[v − 1]: ahoracuenta[v]es cuántos elementos son menores o iguales quev, es decir, la posición donde terminaven el resultado. - Colocar. Se recorre la entrada de la última posición a la primera: cada elemento va a
resultado[--cuenta[a[i]]]. Al ir al revés, de dos iguales el último se coloca más a la derecha: es estable. - Radix sort. Para números con varias cifras se hace un counting sort estable por las unidades, luego por las decenas, luego por las centenas… Gracias a la estabilidad, cada pasada respeta el orden de las anteriores.
El código
Personas ordenadas por edad
Los dos de 25 y los dos de 31 conservan el orden en que estaban: es estable.
1public class Main {
2 record Persona(String nombre, int edad) { }
3
4 /** Ordena por edad (de 0 a max) contando: estable y sin comparar a nadie con nadie. */
5 static Persona[] porEdad(Persona[] p, int max) {
6 int[] cuenta = new int[max + 1];
7 for (Persona x : p) cuenta[x.edad()]++; // 1. cuántos hay de cada edad
8 for (int e = 1; e <= max; e++) cuenta[e] += cuenta[e - 1]; // 2. cuántos con esa edad o menos
9 Persona[] res = new Persona[p.length];
10 for (int i = p.length - 1; i >= 0; i--) // 3. colocar, recorriendo al revés
11 res[--cuenta[p[i].edad()]] = p[i]; // para que sea estable
12 return res;
13 }
14
15 public static void main(String[] args) {
16 Persona[] p = {
17 new Persona("Ana", 31), new Persona("Luis", 25), new Persona("Eva", 31),
18 new Persona("Pablo", 19), new Persona("Marta", 25),
19 };
20 for (Persona x : porEdad(p, 120)) System.out.println(x.edad() + " " + x.nombre());
21 }
22}from collections import namedtuple
Persona = namedtuple("Persona", "nombre edad")
def por_edad(p, maximo):
"""Ordena por edad (de 0 a maximo) contando: estable y sin comparar a nadie con nadie."""
cuenta = [0] * (maximo + 1)
for x in p: # 1. cuántos hay de cada edad
cuenta[x.edad] += 1
for e in range(1, maximo + 1): # 2. cuántos con esa edad o menos
cuenta[e] += cuenta[e - 1]
res = [None] * len(p)
for x in reversed(p): # 3. colocar, recorriendo al revés
cuenta[x.edad] -= 1 # para que sea estable
res[cuenta[x.edad]] = x
return res
p = [Persona("Ana", 31), Persona("Luis", 25), Persona("Eva", 31), Persona("Pablo", 19), Persona("Marta", 25)]
for x in por_edad(p, 120):
print(x.edad, x.nombre)/** Ordena por edad (de 0 a max) contando: estable y sin comparar a nadie con nadie. */
function porEdad(p, max) {
const cuenta = new Array(max + 1).fill(0);
for (const x of p) cuenta[x.edad]++; // 1. cuántos hay de cada edad
for (let e = 1; e <= max; e++) cuenta[e] += cuenta[e - 1]; // 2. cuántos con esa edad o menos
const res = new Array(p.length);
for (let i = p.length - 1; i >= 0; i--) // 3. colocar, recorriendo al revés
res[--cuenta[p[i].edad]] = p[i]; // para que sea estable
return res;
}
const p = [
{ nombre: "Ana", edad: 31 }, { nombre: "Luis", edad: 25 }, { nombre: "Eva", edad: 31 },
{ nombre: "Pablo", edad: 19 }, { nombre: "Marta", edad: 25 },
];
for (const x of porEdad(p, 120)) console.log(x.edad + " " + x.nombre);using System;
record Persona(string Nombre, int Edad);
class Program {
// Ordena por edad (de 0 a max) contando: estable y sin comparar a nadie con nadie.
static Persona[] PorEdad(Persona[] p, int max) {
int[] cuenta = new int[max + 1];
foreach (var x in p) cuenta[x.Edad]++; // 1. cuántos hay de cada edad
for (int e = 1; e <= max; e++) cuenta[e] += cuenta[e - 1]; // 2. cuántos con esa edad o menos
var res = new Persona[p.Length];
for (int i = p.Length - 1; i >= 0; i--) // 3. colocar, recorriendo al revés
res[--cuenta[p[i].Edad]] = p[i]; // para que sea estable
return res;
}
static void Main() {
Persona[] p = { new("Ana", 31), new("Luis", 25), new("Eva", 31), new("Pablo", 19), new("Marta", 25) };
foreach (var x in PorEdad(p, 120)) Console.WriteLine(quot;{x.Edad} {x.Nombre}");
}
}<?php
/** Ordena por edad (de 0 a $max) contando: estable y sin comparar a nadie con nadie. */
function porEdad(array $p, int $max): array {
$cuenta = array_fill(0, $max + 1, 0);
foreach ($p as [$nombre, $edad]) $cuenta[$edad]++; // 1. cuántos hay de cada edad
for ($e = 1; $e <= $max; $e++) $cuenta[$e] += $cuenta[$e - 1]; // 2. cuántos con esa edad o menos
$res = array_fill(0, count($p), null);
for ($i = count($p) - 1; $i >= 0; $i--) // 3. colocar, recorriendo al revés
$res[--$cuenta[$p[$i][1]]] = $p[$i]; // para que sea estable
return $res;
}
$p = [["Ana", 31], ["Luis", 25], ["Eva", 31], ["Pablo", 19], ["Marta", 25]];
foreach (porEdad($p, 120) as [$nombre, $edad]) echo "$edad $nombre\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
19 Pablo 25 Luis 25 Marta 31 Ana 31 Eva
Radix sort: un counting sort por cifra
Tres pasadas de counting sort (unidades, decenas y centenas) ordenan números de hasta tres cifras sin comparar ninguno.
1import java.util.Arrays;
2
3public class Main {
4 /** Counting sort estable por la cifra de valor exp (1 = unidades, 10 = decenas, 100 = centenas). */
5 static void porCifra(int[] a, int exp) {
6 int[] cuenta = new int[10];
7 for (int x : a) cuenta[x / exp % 10]++;
8 for (int d = 1; d < 10; d++) cuenta[d] += cuenta[d - 1];
9 int[] res = new int[a.length];
10 for (int i = a.length - 1; i >= 0; i--) res[--cuenta[a[i] / exp % 10]] = a[i];
11 System.arraycopy(res, 0, a, 0, a.length);
12 }
13
14 public static void main(String[] args) {
15 int[] a = {170, 45, 75, 90, 802, 24, 2, 66};
16 String[] nombre = {"unidades", "decenas", "centenas"};
17 for (int exp = 1, k = 0; exp <= 100; exp *= 10, k++) { // radix sort: una pasada por cifra
18 porCifra(a, exp);
19 System.out.println("Por " + nombre[k] + ": " + Arrays.toString(a));
20 }
21 }
22}def por_cifra(a, exp):
"""Counting sort estable por la cifra de valor exp (1 = unidades, 10 = decenas, 100 = centenas)."""
cuenta = [0] * 10
for x in a:
cuenta[x // exp % 10] += 1
for d in range(1, 10):
cuenta[d] += cuenta[d - 1]
res = [0] * len(a)
for x in reversed(a):
cuenta[x // exp % 10] -= 1
res[cuenta[x // exp % 10]] = x
a[:] = res
a = [170, 45, 75, 90, 802, 24, 2, 66]
for exp, nombre in [(1, "unidades"), (10, "decenas"), (100, "centenas")]: # una pasada por cifra
por_cifra(a, exp)
print(f"Por {nombre}: {a}")/** Counting sort estable por la cifra de valor exp (1 = unidades, 10 = decenas, 100 = centenas). */
function porCifra(a, exp) {
const cifra = (x) => Math.floor(x / exp) % 10;
const cuenta = new Array(10).fill(0);
for (const x of a) cuenta[cifra(x)]++;
for (let d = 1; d < 10; d++) cuenta[d] += cuenta[d - 1];
const res = new Array(a.length);
for (let i = a.length - 1; i >= 0; i--) res[--cuenta[cifra(a[i])]] = a[i];
a.splice(0, a.length, ...res);
}
const a = [170, 45, 75, 90, 802, 24, 2, 66];
for (const [exp, nombre] of [[1, "unidades"], [10, "decenas"], [100, "centenas"]]) { // una pasada por cifra
porCifra(a, exp);
console.log(`Por ${nombre}: [${a.join(", ")}]`);
}using System;
class Program {
// Counting sort estable por la cifra de valor exp (1 = unidades, 10 = decenas, 100 = centenas).
static void PorCifra(int[] a, int exp) {
int[] cuenta = new int[10];
foreach (int x in a) cuenta[x / exp % 10]++;
for (int d = 1; d < 10; d++) cuenta[d] += cuenta[d - 1];
int[] res = new int[a.Length];
for (int i = a.Length - 1; i >= 0; i--) res[--cuenta[a[i] / exp % 10]] = a[i];
Array.Copy(res, a, a.Length);
}
static void Main() {
int[] a = { 170, 45, 75, 90, 802, 24, 2, 66 };
string[] nombre = { "unidades", "decenas", "centenas" };
for (int exp = 1, k = 0; exp <= 100; exp *= 10, k++) { // radix sort: una pasada por cifra
PorCifra(a, exp);
Console.WriteLine(quot;Por {nombre[k]}: [{string.Join(", ", a)}]");
}
}
}<?php
/** Counting sort estable por la cifra de valor $exp (1 = unidades, 10 = decenas, 100 = centenas). */
function porCifra(array &$a, int $exp): void {
$cuenta = array_fill(0, 10, 0);
foreach ($a as $x) $cuenta[intdiv($x, $exp) % 10]++;
for ($d = 1; $d < 10; $d++) $cuenta[$d] += $cuenta[$d - 1];
$res = array_fill(0, count($a), 0);
for ($i = count($a) - 1; $i >= 0; $i--) $res[--$cuenta[intdiv($a[$i], $exp) % 10]] = $a[$i];
$a = $res;
}
$a = [170, 45, 75, 90, 802, 24, 2, 66];
foreach ([1 => "unidades", 10 => "decenas", 100 => "centenas"] as $exp => $nombre) { // una pasada por cifra
porCifra($a, $exp);
echo "Por $nombre: [" . implode(", ", $a) . "]\n";
}Salida al ejecutarlo (la misma en los 5 lenguajes)
Por unidades: [170, 90, 802, 2, 24, 45, 75, 66] Por decenas: [802, 2, 24, 45, 66, 170, 75, 90] Por centenas: [2, 24, 45, 66, 75, 90, 170, 802]
Traza: counting sort de {4, 2, 2, 8, 3, 3, 1}
| Fase | cuenta[0..8] | Resultado |
|---|---|---|
| Contar | 0 1 2 2 1 0 0 0 1 | — |
| Acumular | 0 1 3 5 6 6 6 6 7 | — |
| Colocar a[6] = 1 | 0 0 3 5 6 6 6 6 7 | 1 · · · · · · |
| Colocar a[5] = 3 | 0 0 3 4 6 6 6 6 7 | 1 · · · 3 · · |
| Colocar a[4] = 3 | 0 0 3 3 6 6 6 6 7 | 1 · · 3 3 · · |
| Colocar a[3] = 8 | 0 0 3 3 6 6 6 6 6 | 1 · · 3 3 · 8 |
| Colocar a[2] = 2 | 0 0 2 3 6 6 6 6 6 | 1 · 2 3 3 · 8 |
| Colocar a[1] = 2 | 0 0 1 3 6 6 6 6 6 | 1 2 2 3 3 · 8 |
| Colocar a[0] = 4 | 0 0 1 3 5 6 6 6 6 | 1 2 2 3 3 4 8 |
Tras acumular, cuenta[3] = 5: hay cinco números menores o iguales que 3, así que el último 3 va en la posición 4.
Complejidad
| Datos (n) | Rango (k) | Counting sort (n + k) | Mergesort (≈ n log₂ n) |
|---|---|---|---|
| 1.000.000 notas | 11 (0 a 10) | ≈ 1.000.000 | ≈ 20.000.000 |
| 1.000 edades | 121 (0 a 120) | ≈ 1.100 | ≈ 10.000 |
| 1.000 números | 1.000.000.000 | ≈ 1.000 millones | ≈ 10.000 |
Tiempo y memoria O(n + k). Es lineal cuando k es pequeño; con un rango enorme es peor que cualquier algoritmo de comparación.
- Mejor caso: O(n)
- Caso medio: O(n)
- Peor caso: O(n)
En realidad O(n + k), con k el rango de valores: lineal solo si k no es mucho mayor que n. Las curvas grises son las demás clases, para comparar.
En la práctica
- Radix sort ordena enteros de 32 bits en cuatro pasadas de counting sort de un byte cada una (k = 256): en GPUs y bases de datos es más rápido que quicksort.
- Los histogramas (de notas, de colores de una imagen, de edades) son la primera fase del counting sort.
- Las ordenaciones de cadenas por prefijos (MSD radix sort) se usan en compresores y en la construcción de índices de texto.
Errores típicos
- Crear el array de cuentas de tamaño
maxen lugar demax + 1: el valor máximo se sale del array. - No tener en cuenta los negativos o un mínimo distinto de 0: hay que restar el mínimo para usarlo como índice (
cuenta[valor − min]). - Colocar recorriendo la entrada de principio a fin: los iguales salen al revés y deja de ser estable (y radix sort deja de funcionar).
- Usarlo con un rango enorme: un array de cuentas de mil millones de posiciones para ordenar diez números.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. Histograma de notas
La entrada son notas enteras de 0 a 10, separadas por espacios o saltos de línea. Cuenta cuántas hay de cada una, muestra un histograma con asteriscos (solo de las notas que aparecen), las notas ordenadas sacadas de las cuentas y la moda (la nota más repetida; si empatan, la menor). Completa contar y ordenadas.
- Entrada:
7 5 10 7 3 7 5. - Salida: líneas como
7: *** (3)(la nota ocupa dos caracteres),Ordenadas: [3, 5, 5, 7, 7, 7, 10]yModa: 7 (3 veces). - Errores:
Nota no válida: «x»(se salta) yNo hay notas.
Ejemplo
7 5 10 7 3 7 5
3: * (1) 5: ** (2) 7: *** (3) 10: * (1) Ordenadas: [3, 5, 5, 7, 7, 7, 10] Moda: 7 (3 veces)
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 /** cuenta[n] = cuántas veces aparece la nota n (de 0 a 10). */
5 static int[] contar(List<Integer> notas) {
6 int[] cuenta = new int[11];
7 for (int n : notas) cuenta[n]++;
8 return cuenta;
9 }
10
11 /** Las notas ordenadas de menor a mayor, sacadas solo de las cuentas. */
12 static List<Integer> ordenadas(int[] cuenta) {
13 List<Integer> r = new ArrayList<>();
14 for (int n = 0; n <= 10; n++)
15 for (int k = 0; k < cuenta[n]; k++) r.add(n);
16 return r;
17 }
18
19 public static void main(String[] args) {
20 Scanner sc = new Scanner(System.in);
21 List<Integer> notas = new ArrayList<>();
22 while (sc.hasNext()) {
23 String t = sc.next();
24 if (t.matches("10|\\d")) notas.add(Integer.parseInt(t));
25 else System.out.println("Nota no válida: «" + t + "»");
26 }
27 if (notas.isEmpty()) {
28 System.out.println("No hay notas");
29 return;
30 }
31 int[] cuenta = contar(notas);
32 int moda = 0;
33 for (int n = 0; n <= 10; n++) {
34 if (cuenta[n] > 0) System.out.printf("%2d: %s (%d)%n", n, "*".repeat(cuenta[n]), cuenta[n]);
35 if (cuenta[n] > cuenta[moda]) moda = n;
36 }
37 System.out.println("Ordenadas: " + ordenadas(cuenta));
38 System.out.println("Moda: " + moda + " (" + cuenta[moda] + (cuenta[moda] == 1 ? " vez)" : " veces)"));
39 }
40}Las cuentas resumen todos los datos en 11 números: a partir de ellas sale el histograma, la moda y la lista ordenada, sin haber comparado dos notas entre sí.
Con un millón de notas son un millón de sumas y once casillas: lineal.
2. Fechas ordenadas con radix sort
Ordena fechas dd/mm/aaaa cronológicamente con radix sort: un counting sort estable por el día, luego otro por el mes y luego otro por el año. Como cada pasada es estable, la última deja las fechas ordenadas por año, dentro de cada año por mes y dentro de cada mes por día. Completa porCampo, el counting sort estable de las fechas por uno de sus campos.
- Entrada: fechas separadas por espacios o saltos de línea, como
03/05/2024 15/01/2023. Los años van de 1900 a 2100. - Salida:
Por día: …,Por mes: …yPor año: …, las fechas en formatodd/mm/aaaaseparadas por espacios. - Errores:
Fecha no válida: «x»(se salta) yNo hay fechas.
Ejemplo
03/05/2024 15/01/2023 03/01/2024 28/02/2023 01/05/2024
Por día: 01/05/2024 03/05/2024 03/01/2024 15/01/2023 28/02/2023 Por mes: 03/01/2024 15/01/2023 28/02/2023 01/05/2024 03/05/2024 Por año: 15/01/2023 28/02/2023 03/01/2024 01/05/2024 03/05/2024
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 static final int PRIMER_ANIO = 1900, ULTIMO_ANIO = 2100;
5
6 /** Una fecha como {día, mes, año}. */
7 static String texto(List<int[]> f) {
8 StringJoiner sj = new StringJoiner(" ");
9 for (int[] x : f) sj.add(String.format("%02d/%02d/%d", x[0], x[1], x[2]));
10 return sj.toString();
11 }
12
13 /** Counting sort ESTABLE de las fechas por uno de sus campos (0 = día, 1 = mes, 2 = año), cuyo
14 valor va de min a max. Devuelve una lista nueva. */
15 static List<int[]> porCampo(List<int[]> f, int campo, int min, int max) {
16 int[] cuenta = new int[max - min + 1];
17 for (int[] x : f) cuenta[x[campo] - min]++;
18 for (int v = 1; v < cuenta.length; v++) cuenta[v] += cuenta[v - 1];
19 int[][] res = new int[f.size()][];
20 for (int i = f.size() - 1; i >= 0; i--) res[--cuenta[f.get(i)[campo] - min]] = f.get(i);
21 return new ArrayList<>(Arrays.asList(res));
22 }
23
24 public static void main(String[] args) {
25 Scanner sc = new Scanner(System.in);
26 List<int[]> f = new ArrayList<>();
27 while (sc.hasNext()) {
28 String t = sc.next();
29 if (!t.matches("\\d{1,2}/\\d{1,2}/\\d{4}")) {
30 System.out.println("Fecha no válida: «" + t + "»");
31 continue;
32 }
33 String[] p = t.split("/");
34 int d = Integer.parseInt(p[0]), m = Integer.parseInt(p[1]), a = Integer.parseInt(p[2]);
35 if (d < 1 || d > 31 || m < 1 || m > 12 || a < PRIMER_ANIO || a > ULTIMO_ANIO) {
36 System.out.println("Fecha no válida: «" + t + "»");
37 continue;
38 }
39 f.add(new int[] {d, m, a});
40 }
41 if (f.isEmpty()) {
42 System.out.println("No hay fechas");
43 return;
44 }
45 f = porCampo(f, 0, 1, 31);
46 System.out.println("Por día: " + texto(f));
47 f = porCampo(f, 1, 1, 12);
48 System.out.println("Por mes: " + texto(f));
49 f = porCampo(f, 2, PRIMER_ANIO, ULTIMO_ANIO);
50 System.out.println("Por año: " + texto(f));
51 }
52}El truco de radix sort es empezar por el criterio MENOS importante y acabar por el más importante, y que cada pasada sea estable: en caso de empate en el año, se conserva el orden que dejó la pasada del mes.
Son tres pasadas de O(n + k) con k = 31, 12 y 201: lineal en el número de fechas.
Test
Test: Counting sort (ordenación por cuentas)
0/5 respondidas · 0 aciertosElige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.
1.¿Por qué counting sort puede ser más rápido que O(n log n)?
2.¿Qué tamaño tiene el array de cuentas para ordenar valores de 0 a 100?
3.¿Por qué se colocan los elementos recorriendo la entrada de atrás adelante?
4.¿Cuándo NO conviene counting sort?
5.En radix sort, ¿por qué cifra se empieza?