Apuntes DAM
Volver al inicio

Counting sort (ordenación por cuentas)

AlgoritmosOrdenaciónNivel intermedioTambién: ordenación por conteo, ordenación por cuentas, radix sort

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.

De 2 a 12 enteros entre 0 y 15
  • 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

  1. Contar. Con cuenta de tamaño max + 1 a ceros, se recorre la entrada sumando uno en cuenta[a[i]].
  2. Acumular. Para cada valor v desde 1, cuenta[v] += cuenta[v − 1]: ahora cuenta[v] es cuántos elementos son menores o iguales que v, es decir, la posición donde termina v en el resultado.
  3. 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.
  4. 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.

Java
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}

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.

Java
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}

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}

Fasecuenta[0..8]Resultado
Contar0 1 2 2 1 0 0 0 1—
Acumular0 1 3 5 6 6 6 6 7—
Colocar a[6] = 10 0 3 5 6 6 6 6 71 · · · · · ·
Colocar a[5] = 30 0 3 4 6 6 6 6 71 · · · 3 · ·
Colocar a[4] = 30 0 3 3 6 6 6 6 71 · · 3 3 · ·
Colocar a[3] = 80 0 3 3 6 6 6 6 61 · · 3 3 · 8
Colocar a[2] = 20 0 2 3 6 6 6 6 61 · 2 3 3 · 8
Colocar a[1] = 20 0 1 3 6 6 6 6 61 2 2 3 3 · 8
Colocar a[0] = 40 0 1 3 5 6 6 6 61 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 notas11 (0 a 10)≈ 1.000.000≈ 20.000.000
1.000 edades121 (0 a 120)≈ 1.100≈ 10.000
1.000 números1.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.

0102030405015101520tamaño de la entrada (n)operacionesO(n!)O(2ⁿ)O(n²)O(n log n)O(1)O(log n)O(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 max en lugar de max + 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] y Moda: 7 (3 veces).
  • Errores: Nota no válida: «x» (se salta) y No hay notas.
☕JavaHistograma de notasFácil

Ejemplo

Entrada (lo que se escribe por teclado)
7 5 10 7 3 7 5
Salida esperada
3: * (1)
 5: ** (2)
 7: *** (3)
10: * (1)
Ordenadas: [3, 5, 5, 7, 7, 7, 10]
Moda: 7 (3 veces)
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
0/5 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
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: … y Por año: …, las fechas en formato dd/mm/aaaa separadas por espacios.
  • Errores: Fecha no válida: «x» (se salta) y No hay fechas.
☕JavaFechas ordenadas con radix sortDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
03/05/2024 15/01/2023 03/01/2024 28/02/2023 01/05/2024
Salida esperada
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
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
0/5 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
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 aciertos

Elige 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. 1.¿Por qué counting sort puede ser más rápido que O(n log n)?

  2. 2.¿Qué tamaño tiene el array de cuentas para ordenar valores de 0 a 100?

  3. 3.¿Por qué se colocan los elementos recorriendo la entrada de atrás adelante?

  4. 4.¿Cuándo NO conviene counting sort?

  5. 5.En radix sort, ¿por qué cifra se empieza?

Relacionado