Apuntes DAM
Volver al inicio

Mergesort (ordenación por mezcla)

AlgoritmosOrdenaciónNivel intermedioTambién: merge sort, ordenación por mezcla, ordenación por fusión

Parte el array por la mitad, ordena cada mitad con la misma idea y mezcla las dos mitades ordenadas en una. Siempre O(n log n) y estable, a cambio de un array auxiliar.

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.

Mergesort

Escribe los números y mira cómo se parten por la mitad y se mezclan ya ordenados.

De 2 a 16 enteros separados por espacios
  • en la zona de trabajo

Paso 1

Array inicial: [38, 27, 43, 3, 9, 82, 10].

1static void mergeSort(int[] a, int ini, int fin) {
2    if (fin - ini < 2) return;               // 0 o 1 elementos: ya ordenado
3    int mitad = (ini + fin) / 2;  // comparaciones = 0, escrituras = 0
4    mergeSort(a, ini, mitad);
5    mergeSort(a, mitad, fin);
6    mezclar(a, ini, mitad, fin);
7}
8
9static void mezclar(int[] a, int ini, int mitad, int fin) {
10    int[] aux = Arrays.copyOfRange(a, ini, fin);
11    int i = 0, j = mitad - ini, k = ini;
12    while (i < mitad - ini && j < fin - ini) {
13        if (aux[i] <= aux[j]) a[k++] = aux[i++];
14        else a[k++] = aux[j++];
15    }
16    while (i < mitad - ini) a[k++] = aux[i++];
17    while (j < fin - ini) a[k++] = aux[j++];
18}

Variables

comparaciones
0
escrituras
0

Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.

La idea

Mergesort se basa en una observación sencilla: mezclar dos listas que ya están ordenadas en una sola lista ordenada es fácil y rápido. Basta mirar el primer elemento de cada una, coger el menor y avanzar en esa lista; en una sola pasada están las dos mezcladas.

Así que, para ordenar un array, se parte por la mitad, se ordena cada mitad (con el mismo mergesort, recursivamente) y se mezclan. La recursividad para en los trozos de un elemento, que ya están ordenados. Es el ejemplo clásico de divide y vencerás.

Partir por la mitad una y otra vez da log₂ n niveles, y en cada nivel las mezclas recorren entre todas los n elementos: n log n en total, siempre, estén como estén los datos. Esa garantía es su gran ventaja frente a quicksort.

Su precio es la memoria: la mezcla necesita un array auxiliar del tamaño del tramo. Y su gran virtud práctica es que es estable si, ante un empate, la mezcla coge primero el de la izquierda (<=); por eso las bibliotecas lo usan (en forma de TimSort) para ordenar objetos.

Cuándo usarlo

  • Cuando se necesita un O(n log n) garantizado, sin peor caso cuadrático.
  • Cuando hace falta estabilidad: ordenar por varios criterios encadenando ordenaciones.
  • Para listas enlazadas: se puede mezclar enganchando nodos, sin memoria extra y sin acceso directo.
  • Para ordenar datos que no caben en memoria (ordenación externa): se ordenan trozos que caben y se mezclan los ficheros.

Cuándo no

  • Si la memoria es muy justa: necesita O(n) de memoria extra en arrays.
  • Con arrays pequeños: la recursividad y las copias pesan más que el trabajo; inserción es más rápida.

Paso a paso

  1. Caso base. Un tramo de 0 o 1 elementos ya está ordenado: no se hace nada.
  2. Dividir. Se calcula el medio y se parte el tramo [desde, hasta) en [desde, medio) y [medio, hasta).
  3. Ordenar las mitades. Se llama a mergesort con cada mitad. Al volver, cada mitad está ordenada.
  4. Mezclar. Con dos índices, uno por mitad, se va copiando a un array auxiliar el menor de los dos elementos señalados (<= para que sea estable), y después lo que sobre de la mitad que no se ha acabado. Se copia el auxiliar de vuelta.

El código

Mergesort mostrando cada mezcla

Las mezclas salen en el orden en que las hace la recursividad: primero las de los trozos más pequeños de la izquierda.

Java
1import java.util.Arrays;
2
3public class Main {
4    /** Ordena a[desde..hasta) y escribe cada mezcla que hace. */
5    static void mergesort(int[] a, int desde, int hasta) {
6        if (hasta - desde <= 1) return;                 // 0 o 1 elementos: ya está ordenado
7        int medio = (desde + hasta) / 2;
8        mergesort(a, desde, medio);                     // ordena la mitad izquierda
9        mergesort(a, medio, hasta);                     // y la derecha
10        String izq = Arrays.toString(Arrays.copyOfRange(a, desde, medio));
11        String der = Arrays.toString(Arrays.copyOfRange(a, medio, hasta));
12        mezclar(a, desde, medio, hasta);
13        System.out.println("mezclar " + izq + " y " + der + " → " + Arrays.toString(Arrays.copyOfRange(a, desde, hasta)));
14    }
15
16    /** Mezcla a[desde..medio) y a[medio..hasta), ya ordenados, en a[desde..hasta). */
17    static void mezclar(int[] a, int desde, int medio, int hasta) {
18        int[] tmp = new int[hasta - desde];
19        int i = desde, j = medio, k = 0;
20        while (i < medio && j < hasta) tmp[k++] = a[i] <= a[j] ? a[i++] : a[j++];   // <= : estable
21        while (i < medio) tmp[k++] = a[i++];            // lo que quede de la izquierda
22        while (j < hasta) tmp[k++] = a[j++];            // o de la derecha
23        System.arraycopy(tmp, 0, a, desde, tmp.length);
24    }
25
26    public static void main(String[] args) {
27        int[] a = {38, 27, 43, 3, 9, 82, 10};
28        mergesort(a, 0, a.length);
29        System.out.println("Resultado: " + Arrays.toString(a));
30    }
31}

Salida al ejecutarlo (la misma en los 5 lenguajes)

mezclar [27] y [43] → [27, 43]
mezclar [38] y [27, 43] → [27, 38, 43]
mezclar [3] y [9] → [3, 9]
mezclar [82] y [10] → [10, 82]
mezclar [3, 9] y [10, 82] → [3, 9, 10, 82]
mezclar [27, 38, 43] y [3, 9, 10, 82] → [3, 9, 10, 27, 38, 43, 82]
Resultado: [3, 9, 10, 27, 38, 43, 82]

La ordenación estable de la biblioteca

Como la ordenación de objetos de la biblioteca es estable, ordenar primero por el criterio secundario y luego por el principal da el orden por los dos.

Java
1// Para objetos, Java ordena con TimSort, un mergesort mejorado: es estable.
2List<Alumno> alumnos = cargarAlumnos();
3alumnos.sort(Comparator.comparing(Alumno::nombre));      // primero por nombre…
4alumnos.sort(Comparator.comparing(Alumno::curso));       // …y luego por curso: dentro de cada
5                                                         // curso siguen ordenados por nombre

Traza: mergesort de {38, 27, 43, 3, 9, 82, 10}

MezclaMitad izquierdaMitad derechaResultado
1[27][43][27, 43]
2[38][27, 43][27, 38, 43]
3[3][9][3, 9]
4[82][10][10, 82]
5[3, 9][10, 82][3, 9, 10, 82]
6[27, 38, 43][3, 9, 10, 82][3, 9, 10, 27, 38, 43, 82]

Seis mezclas para siete elementos (siempre n − 1). La última junta las dos mitades ordenadas del array entero.

Complejidad

Datos (n)Mergesort (≈ n log₂ n comparaciones)Burbuja o inserción (≈ n²/2)
1.000≈ 10.000≈ 500.000
100.000≈ 1,7 millones≈ 5.000 millones
10.000.000≈ 230 millones≈ 50 billones

O(n log n) en el mejor, el medio y el peor caso. Memoria extra: O(n) para el array auxiliar (más O(log n) de la pila de llamadas). Estable.

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 log n)
  • Caso medio: O(n log n)
  • Peor caso: O(n log n)

Siempre n log n, a cambio de un array auxiliar de n elementos. Las curvas grises son las demás clases, para comparar.

En la práctica

  • TimSort, el algoritmo de Arrays.sort y Collections.sort para objetos en Java, de sorted() en Python y de Array.prototype.sort en V8, es un mergesort que aprovecha los tramos que ya vienen ordenados.
  • Las bases de datos ordenan tablas que no caben en memoria con un mergesort externo: ordenan bloques, los guardan en disco y los mezclan.
  • La mezcla de dos listas ordenadas sirve por sí sola: unir dos ficheros ordenados, calcular la intersección de dos listas o hacer el JOIN por mezcla de un SGBD.
  • Se paraleliza bien: cada mitad se puede ordenar en un hilo distinto (Arrays.parallelSort).

Errores típicos

  • Mezclar con < en vez de <=: sigue ordenando, pero deja de ser estable (con empate coge primero el de la derecha).
  • Olvidar copiar lo que sobra de una de las mitades al acabar el bucle principal: se pierden elementos.
  • Confundir los límites: con hasta exclusivo, las mitades son [desde, medio) y [medio, hasta); si se mezcla con límites inclusivos y exclusivos a la vez, se repite o se pierde el elemento del medio.
  • Crear dos arrays nuevos en cada llamada (las mitades) en vez de trabajar sobre índices: funciona, pero gasta mucha más memoria y tiempo.
  • Caso base mal puesto (hasta - desde == 0): con un elemento se parte en un tramo vacío y otro de uno, y la recursividad no termina.

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. Mezcla y elementos comunes

Las dos primeras líneas son dos listas de enteros ordenadas de menor a mayor (pueden tener repetidos o estar vacías). Escribe su mezcla (todos los elementos, ordenados) y los valores que están en las dos (sin repetir). Las dos cosas se hacen en una sola pasada con dos índices, como la mezcla de mergesort: nada de ordenar después ni de bucles anidados. Completa mezclar y comunes.

  • Entrada: línea 1 con la lista 1 y línea 2 con la lista 2, por ejemplo 1 3 5 5 8 y 2 3 5 9.
  • Salida: Mezcla: [1, 2, 3, 3, 5, 5, 5, 8, 9] y Comunes: [3, 5]; si no hay ninguno, (ninguno).
  • Errores: Número no válido en la lista N: «x» y La lista N no está ordenada.
☕JavaMezcla y elementos comunesMedio

Ejemplo

Entrada (lo que se escribe por teclado)
1 3 5 5 8
2 3 5 9
Salida esperada
Mezcla: [1, 2, 3, 3, 5, 5, 5, 8, 9]
Comunes: [3, 5]
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
⏳
Test oculto #6
0/6 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    /** Todos los elementos de a y b (ordenadas) en una lista ordenada, en una sola pasada. */
5    static List<Integer> mezclar(List<Integer> a, List<Integer> b) {
6        List<Integer> r = new ArrayList<>();
7        int i = 0, j = 0;
8        while (i < a.size() && j < b.size()) r.add(a.get(i) <= b.get(j) ? a.get(i++) : b.get(j++));
9        while (i < a.size()) r.add(a.get(i++));
10        while (j < b.size()) r.add(b.get(j++));
11        return r;
12    }
13
14    /** Los valores que están en las dos listas (sin repetir), también en una sola pasada. */
15    static List<Integer> comunes(List<Integer> a, List<Integer> b) {
16        List<Integer> r = new ArrayList<>();
17        int i = 0, j = 0;
18        while (i < a.size() && j < b.size()) {
19            int x = a.get(i), y = b.get(j);
20            if (x < y) i++;
21            else if (x > y) j++;
22            else {
23                if (r.isEmpty() || r.get(r.size() - 1) != x) r.add(x);
24                i++;
25                j++;
26            }
27        }
28        return r;
29    }
30
31    /** Lee una lista ordenada; null si hay un error (ya escrito). */
32    static List<Integer> leer(Scanner sc, int n) {
33        String linea = sc.hasNextLine() ? sc.nextLine().trim() : "";
34        List<Integer> l = new ArrayList<>();
35        if (linea.isEmpty()) return l;
36        for (String p : linea.split("\\s+")) {
37            try {
38                l.add(Integer.parseInt(p));
39            } catch (NumberFormatException e) {
40                System.out.println("Número no válido en la lista " + n + ": «" + p + "»");
41                return null;
42            }
43        }
44        for (int i = 1; i < l.size(); i++) {
45            if (l.get(i) < l.get(i - 1)) {
46                System.out.println("La lista " + n + " no está ordenada");
47                return null;
48            }
49        }
50        return l;
51    }
52
53    static String texto(List<Integer> l) {
54        return l.isEmpty() ? "(ninguno)" : l.toString();
55    }
56
57    public static void main(String[] args) {
58        Scanner sc = new Scanner(System.in);
59        List<Integer> a = leer(sc, 1);
60        if (a == null) return;
61        List<Integer> b = leer(sc, 2);
62        if (b == null) return;
63        System.out.println("Mezcla: " + texto(mezclar(a, b)));
64        System.out.println("Comunes: " + texto(comunes(a, b)));
65    }
66}

Las dos funciones aprovechan que las listas están ordenadas: en cada paso se sabe con seguridad que el menor de los dos señalados ya no se va a necesitar más (o va a la mezcla, o no tiene pareja).

Es la misma técnica que el JOIN por mezcla de las bases de datos y que la intersección de listas de un buscador: lineal, en vez del n·m de comparar todos con todos.

2. Mergesort de abajo arriba

El mergesort también se puede hacer sin recursividad: primero se mezclan los elementos de dos en dos (bloques de 1), luego los bloques de 2 de dos en dos, luego los de 4… hasta que un bloque ocupa todo el array. Lee una línea de enteros y ordénala así, mostrando el array tras cada nivel. La mezcla ya está hecha: completa nivel, que mezcla cada pareja de bloques consecutivos de un tamaño dado.

  • Entrada: una línea de enteros separados por espacios.
  • Por cada nivel: Bloques de 1 mezclados de dos en dos: 27 38 3 43 …; al final, Ordenado en N niveles: ….
  • Errores: No hay números y Número no válido: «x».
☕JavaMergesort de abajo arribaDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
38 27 43 3 9 82 10
Salida esperada
Bloques de 1 mezclados de dos en dos: 27 38 3 43 9 82 10
Bloques de 2 mezclados de dos en dos: 3 27 38 43 9 10 82
Bloques de 4 mezclados de dos en dos: 3 9 10 27 38 43 82
Ordenado en 3 niveles: 3 9 10 27 38 43 82
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
⏳
Test oculto #6
0/6 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    /** Mezcla a[desde..medio) y a[medio..hasta), ya ordenados. */
5    static void mezclar(int[] a, int desde, int medio, int hasta) {
6        int[] tmp = new int[hasta - desde];
7        int i = desde, j = medio, k = 0;
8        while (i < medio && j < hasta) tmp[k++] = a[i] <= a[j] ? a[i++] : a[j++];
9        while (i < medio) tmp[k++] = a[i++];
10        while (j < hasta) tmp[k++] = a[j++];
11        System.arraycopy(tmp, 0, a, desde, tmp.length);
12    }
13
14    /** Un nivel del mergesort de abajo arriba: mezcla cada pareja de bloques consecutivos de tamaño
15        ancho (el último bloque puede ser más corto o no tener pareja). */
16    static void nivel(int[] a, int ancho) {
17        for (int desde = 0; desde < a.length; desde += 2 * ancho) {
18            int medio = Math.min(desde + ancho, a.length);
19            int hasta = Math.min(desde + 2 * ancho, a.length);
20            if (medio < hasta) mezclar(a, desde, medio, hasta);
21        }
22    }
23
24    /** Lee una línea de enteros; si falla, escribe el error y devuelve null. */
25    static int[] leer(Scanner sc) {
26        String linea = sc.hasNextLine() ? sc.nextLine().trim() : "";
27        if (linea.isEmpty()) {
28            System.out.println("No hay números");
29            return null;
30        }
31        String[] partes = linea.split("\\s+");
32        int[] a = new int[partes.length];
33        for (int i = 0; i < partes.length; i++) {
34            try {
35                a[i] = Integer.parseInt(partes[i]);
36            } catch (NumberFormatException e) {
37                System.out.println("Número no válido: «" + partes[i] + "»");
38                return null;
39            }
40        }
41        return a;
42    }
43
44    static String texto(int[] a) {
45        StringJoiner sj = new StringJoiner(" ");
46        for (int x : a) sj.add(String.valueOf(x));
47        return sj.toString();
48    }
49
50    public static void main(String[] args) {
51        Scanner sc = new Scanner(System.in);
52        int[] a = leer(sc);
53        if (a == null) return;
54        int niveles = 0;
55        for (int ancho = 1; ancho < a.length; ancho *= 2) {
56            nivel(a, ancho);
57            niveles++;
58            System.out.println("Bloques de " + ancho + " mezclados de dos en dos: " + texto(a));
59        }
60        System.out.println("Ordenado en " + niveles + (niveles == 1 ? " nivel: " : " niveles: ") + texto(a));
61    }
62}

Es el mismo trabajo que el mergesort recursivo, en otro orden: en vez de bajar hasta los trozos de 1 y volver mezclando, se empieza por ellos. Hay ⌈log₂ n⌉ niveles y cada uno recorre el array una vez.

Sin recursividad no hay riesgo de desbordar la pila, y es la base de la ordenación externa: cada nivel es una pasada sobre los ficheros.

Test

Test: Mergesort (ordenación por mezcla)

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.¿Cuál es el coste de mergesort en el peor caso?

  2. 2.¿Qué le hace falta a mergesort que no necesitan la burbuja ni el quicksort?

  3. 3.Para que mergesort sea estable, ante un empate la mezcla debe coger…

  4. 4.¿Cuántas mezclas hace mergesort sobre 8 elementos?

  5. 5.¿Por qué Java usa un algoritmo basado en mergesort para ordenar objetos?

Relacionado