Apuntes DAM
Volver al inicio

Quicksort (ordenación rápida)

AlgoritmosOrdenaciónNivel intermedioTambién: ordenación rápida, quick sort, ordenación de Hoare

Elige un pivote, deja a su izquierda los menores y a su derecha los mayores (el pivote queda en su sitio) y repite con cada lado. O(n log n) de media sin memoria extra; O(n²) con pivotes malos.

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.

Quicksort

Escribe los números y mira cómo cada partición deja el pivote en su sitio definitivo.

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

Paso 1

Array inicial: [10, 80, 30, 90, 40, 50, 70].

1static void quickSort(int[] a, int ini, int fin) {
2    if (ini >= fin) return;
3    int p = particion(a, ini, fin);  // comparaciones = 0, intercambios = 0
4    quickSort(a, ini, p - 1);
5    quickSort(a, p + 1, fin);
6}
7
8static int particion(int[] a, int ini, int fin) {
9    int pivote = a[fin];
10    int i = ini - 1;
11    for (int j = ini; j < fin; j++) {
12        if (a[j] <= pivote) {
13            i++;
14            int t = a[i]; a[i] = a[j]; a[j] = t;
15        }
16    }
17    int t = a[i + 1]; a[i + 1] = a[fin]; a[fin] = t;
18    return i + 1;
19}

Variables

comparaciones
0
intercambios
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

Quicksort también es divide y vencerás, pero hace el trabajo al revés que mergesort: en vez de partir sin más y trabajar al mezclar, trabaja al partir y luego no tiene nada que juntar.

Se elige un elemento, el pivote, y se reorganiza el tramo para que todos los menores o iguales que él queden a su izquierda y los mayores a su derecha. Eso es la partición, y tiene una consecuencia preciosa: el pivote ya está en su posición definitiva. Después se ordena cada lado por separado, con el mismo método, y como todo lo de la izquierda es menor que todo lo de la derecha, al terminar el array está ordenado.

Si el pivote cae cerca del centro, cada partición divide el problema en dos mitades y el coste es n log n, como mergesort pero sin array auxiliar y con muy pocos movimientos: en la práctica es el más rápido de los algoritmos de comparación.

Si el pivote es siempre el mayor o el menor (por ejemplo, el último de un array que ya estaba ordenado), cada partición solo quita un elemento: n niveles de recursividad y O(n²). Por eso las implementaciones reales eligen bien el pivote (al azar, la mediana de tres) y cambian de método si la recursividad se hace demasiado profunda.

Cuándo usarlo

  • Para ordenar arrays de números u otros tipos básicos rápido y sin memoria extra: es lo que hace Arrays.sort(int[]).
  • Cuando no hace falta estabilidad.
  • Su partición sirve sola para encontrar el k-ésimo menor o la mediana en O(n) de media (quickselect), sin ordenar.

Cuándo no

  • Si hace falta un O(n log n) garantizado (sistemas en tiempo real, datos que puede elegir un atacante): mergesort o heapsort.
  • Si hace falta estabilidad: mergesort.
  • Con un pivote fijo (el primero o el último) sobre datos que pueden llegar ordenados: es justo su peor caso.

Paso a paso

  1. Caso base. Un tramo con 0 o 1 elementos (ini >= fin) ya está ordenado.
  2. Elegir el pivote. En la partición de Lomuto, el pivote es el último del tramo, a[fin].
  3. Partir. Un índice i marca el final de la zona de los menores o iguales. Se recorre el tramo con j: cada a[j] <= pivote se intercambia al final de esa zona. Al acabar, se intercambia el pivote con a[i + 1]: ya está en su sitio.
  4. Ordenar cada lado. Se llama a quicksort con [ini, p − 1] y con [p + 1, fin]. No hay que combinar nada al volver.

El código

Quicksort mostrando cada partición

Cada línea es una partición: el pivote queda en su posición definitiva y el resto se reparte a sus lados.

Java
1import java.util.Arrays;
2
3public class Main {
4    static void quicksort(int[] a, int ini, int fin) {
5        if (ini >= fin) return;                         // 0 o 1 elementos: ya está ordenado
6        int p = particion(a, ini, fin);                 // el pivote queda en su sitio definitivo
7        System.out.println("pivote " + a[p] + " → posición " + p + ": " + Arrays.toString(a));
8        quicksort(a, ini, p - 1);                       // los menores o iguales que el pivote
9        quicksort(a, p + 1, fin);                       // y los mayores
10    }
11
12    /** Partición de Lomuto: el pivote es a[fin]; los <= pivote pasan a la izquierda
13        y el pivote queda justo entre los dos grupos. Devuelve su posición. */
14    static int particion(int[] a, int ini, int fin) {
15        int pivote = a[fin];
16        int i = ini - 1;                                // a[ini..i] son los <= pivote
17        for (int j = ini; j < fin; j++) {
18            if (a[j] <= pivote) {
19                i++;
20                int t = a[i]; a[i] = a[j]; a[j] = t;
21            }
22        }
23        int t = a[i + 1]; a[i + 1] = a[fin]; a[fin] = t;
24        return i + 1;
25    }
26
27    public static void main(String[] args) {
28        int[] a = {10, 80, 30, 90, 40, 50, 70};
29        quicksort(a, 0, a.length - 1);
30        System.out.println("Resultado: " + Arrays.toString(a));
31    }
32}

Salida al ejecutarlo (la misma en los 5 lenguajes)

pivote 70 → posición 4: [10, 30, 40, 50, 70, 90, 80]
pivote 50 → posición 3: [10, 30, 40, 50, 70, 90, 80]
pivote 40 → posición 2: [10, 30, 40, 50, 70, 90, 80]
pivote 30 → posición 1: [10, 30, 40, 50, 70, 90, 80]
pivote 80 → posición 5: [10, 30, 40, 50, 70, 80, 90]
Resultado: [10, 30, 40, 50, 70, 80, 90]

Pivote aleatorio

Un truco de una línea que hace casi imposible el peor caso, vengan como vengan los datos.

Java
1static final Random azar = new Random();
2
3/** Con un pivote al azar, el peor caso (O(n²)) ya no depende de cómo vengan los datos. */
4static int particionAleatoria(int[] a, int ini, int fin) {
5    int r = ini + azar.nextInt(fin - ini + 1);     // una posición al azar del tramo…
6    int t = a[r]; a[r] = a[fin]; a[fin] = t;       // …se lleva al final y se parte como siempre
7    return particion(a, ini, fin);
8}

Traza: quicksort de {10, 80, 30, 90, 40, 50, 70}

TramoPivoteArray tras partirEl pivote queda en
[0..6] [10, 80, 30, 90, 40, 50, 70]70[10, 30, 40, 50, 70, 90, 80]4
[0..3] [10, 30, 40, 50]50[10, 30, 40, 50, 70, 90, 80]3
[0..2] [10, 30, 40]40[10, 30, 40, 50, 70, 90, 80]2
[0..1] [10, 30]30[10, 30, 40, 50, 70, 90, 80]1
[5..6] [90, 80]80[10, 30, 40, 50, 70, 80, 90]5

Los tramos de un solo elemento no se parten: ya están en su sitio.

Complejidad

CasoCuándo pasaCoste
MejorCada pivote cae en el centro del tramoO(n log n)
MedioDatos en orden aleatorioO(n log n), unas 1,39·n log₂ n comparaciones
PeorEl pivote es siempre el mayor o el menor (datos ya ordenados o todos iguales con Lomuto)O(n²)

Memoria: O(log n) de pila de media (O(n) en el peor caso). No es estable. Con 1.000.000 de datos ya ordenados y pivote el último, son 500.000 millones de comparaciones… y probablemente un StackOverflowError.

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²)

El peor caso llega con pivotes malos (array ya ordenado y pivote el último); un pivote aleatorio lo hace improbable. Las curvas grises son las demás clases, para comparar.

En la práctica

  • Arrays.sort de tipos primitivos en Java usa un quicksort de doble pivote (dos pivotes, tres zonas).
  • std::sort de C++ y List.Sort de C# usan introsort: quicksort que cambia a heapsort si la recursividad se hace demasiado profunda y a inserción con trozos pequeños.
  • Quickselect (la partición, siguiendo solo un lado) da la mediana o el percentil 90 de un millón de datos sin ordenarlos.
  • La partición en tres zonas (menores, iguales y mayores, el problema de la bandera holandesa) resuelve el caso de muchos valores repetidos.

Errores típicos

  • Recursividad sin caso base correcto (ini == fin en vez de ini >= fin): con un tramo vacío (p − 1 < ini) no se para.
  • Llamar a la recursividad incluyendo el pivote ([ini, p]): si el pivote es el mayor, el tramo no se reduce y la recursividad no termina.
  • Usar siempre el primero o el último como pivote con datos que pueden llegar ordenados: O(n²) y desbordamiento de pila.
  • Esperar que sea estable: la partición mueve elementos iguales de un lado a otro.
  • Olvidar el último intercambio de la partición (a[i + 1] con a[fin]): el pivote se queda al final y no en su sitio.

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. El k-ésimo menor sin ordenar (quickselect)

La primera línea es k y la segunda, un array de enteros. Encuentra el k-ésimo menor (el 1.º es el mínimo) con quickselect: se parte el array como en quicksort y, como el pivote queda en su posición definitiva, se sigue solo por el lado donde está la posición k − 1. La partición ya está hecha (Lomuto, pivote el último) y cuenta cuántas veces se llama. Completa seleccionar.

  • Entrada: 3 y luego 7 10 4 3 20 15.
  • Salida: El 3.º menor es 7 (2 particiones).
  • Errores: No hay números, Número no válido: «x» y k no válido: «texto» (tiene que estar entre 1 y n).
☕JavaEl k-ésimo menor sin ordenar (quickselect)Medio

Ejemplo

Entrada (lo que se escribe por teclado)
3
7 10 4 3 20 15
Salida esperada
El 3.º menor es 7 (3 particiones)
⏳
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    static int particiones = 0;
5
6    /** Partición de Lomuto con el último del tramo como pivote. Devuelve dónde queda el pivote. */
7    static int particion(int[] a, int ini, int fin) {
8        particiones++;
9        int pivote = a[fin];
10        int i = ini - 1;
11        for (int j = ini; j < fin; j++) {
12            if (a[j] <= pivote) {
13                i++;
14                int t = a[i]; a[i] = a[j]; a[j] = t;
15            }
16        }
17        int t = a[i + 1]; a[i + 1] = a[fin]; a[fin] = t;
18        return i + 1;
19    }
20
21    /** El k-ésimo menor (k empieza en 1) con quickselect: se parte y solo se sigue por el lado
22        donde está la posición k − 1, sin ordenar el otro. */
23    static int seleccionar(int[] a, int k) {
24        int ini = 0, fin = a.length - 1, objetivo = k - 1;
25        while (true) {
26            int p = particion(a, ini, fin);
27            if (p == objetivo) return a[p];
28            if (objetivo < p) fin = p - 1;
29            else ini = p + 1;
30        }
31    }
32
33    /** Lee una línea de enteros; si falla, escribe el error y devuelve null. */
34    static int[] leer(Scanner sc) {
35        String linea = sc.hasNextLine() ? sc.nextLine().trim() : "";
36        if (linea.isEmpty()) {
37            System.out.println("No hay números");
38            return null;
39        }
40        String[] partes = linea.split("\\s+");
41        int[] a = new int[partes.length];
42        for (int i = 0; i < partes.length; i++) {
43            try {
44                a[i] = Integer.parseInt(partes[i]);
45            } catch (NumberFormatException e) {
46                System.out.println("Número no válido: «" + partes[i] + "»");
47                return null;
48            }
49        }
50        return a;
51    }
52
53    static String texto(int[] a) {
54        StringJoiner sj = new StringJoiner(" ");
55        for (int x : a) sj.add(String.valueOf(x));
56        return sj.toString();
57    }
58
59    public static void main(String[] args) {
60        Scanner sc = new Scanner(System.in);
61        String primera = sc.hasNextLine() ? sc.nextLine().trim() : "";
62        int[] a = leer(sc);
63        if (a == null) return;
64        if (!primera.matches("\\d{1,4}") || Integer.parseInt(primera) < 1 || Integer.parseInt(primera) > a.length) {
65            System.out.println("k no válido: «" + primera + "» (tiene que estar entre 1 y " + a.length + ")");
66            return;
67        }
68        int k = Integer.parseInt(primera);
69        int x = seleccionar(a, k);
70        System.out.println("El " + k + ".º menor es " + x + " (" + particiones + (particiones == 1 ? " partición)" : " particiones)"));
71    }
72}

Quickselect hace la mitad de trabajo que quicksort en cada nivel porque descarta un lado entero: n + n/2 + n/4 + … ≈ 2n de media, O(n), frente al O(n log n) de ordenar.

Con pivotes malos tiene el mismo peor caso que quicksort (O(n²)); con un pivote aleatorio, ese caso se vuelve rarísimo.

2. El peor caso de quicksort

Ordena una línea de enteros con quicksort (partición de Lomuto, ya hecha, que cuenta las comparaciones) y apunta también la profundidad máxima de la recursividad. Así se ve la diferencia entre un array desordenado y uno ya ordenado, el peor caso con este pivote. Completa quicksort.

  • Entrada: una línea de enteros.
  • Salida: Ordenado: …, Comparaciones: C · profundidad máxima: P y, si se han hecho las n(n − 1)/2 comparaciones del peor caso, ¡El peor caso! Cada partición solo ha separado el pivote; si no, Peor caso posible: X comparaciones.
  • Errores: No hay números y Número no válido: «x».
☕JavaEl peor caso de quicksortMedio

Ejemplo

Entrada (lo que se escribe por teclado)
10 80 30 90 40 50 70
Salida esperada
Ordenado: 10 30 40 50 70 80 90
Comparaciones: 13 · profundidad máxima: 5
Peor caso posible: 21 comparaciones
⏳
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    static long comparaciones = 0;
5    static int profundidadMaxima = 0;
6
7    static int particion(int[] a, int ini, int fin) {
8        int pivote = a[fin];
9        int i = ini - 1;
10        for (int j = ini; j < fin; j++) {
11            comparaciones++;
12            if (a[j] <= pivote) {
13                i++;
14                int t = a[i]; a[i] = a[j]; a[j] = t;
15            }
16        }
17        int t = a[i + 1]; a[i + 1] = a[fin]; a[fin] = t;
18        return i + 1;
19    }
20
21    /** Quicksort que apunta la profundidad máxima de las llamadas (la primera llamada es la 1). */
22    static void quicksort(int[] a, int ini, int fin, int profundidad) {
23        profundidadMaxima = Math.max(profundidadMaxima, profundidad);
24        if (ini >= fin) return;
25        int p = particion(a, ini, fin);
26        quicksort(a, ini, p - 1, profundidad + 1);
27        quicksort(a, p + 1, fin, profundidad + 1);
28    }
29
30    /** Lee una línea de enteros; si falla, escribe el error y devuelve null. */
31    static int[] leer(Scanner sc) {
32        String linea = sc.hasNextLine() ? sc.nextLine().trim() : "";
33        if (linea.isEmpty()) {
34            System.out.println("No hay números");
35            return null;
36        }
37        String[] partes = linea.split("\\s+");
38        int[] a = new int[partes.length];
39        for (int i = 0; i < partes.length; i++) {
40            try {
41                a[i] = Integer.parseInt(partes[i]);
42            } catch (NumberFormatException e) {
43                System.out.println("Número no válido: «" + partes[i] + "»");
44                return null;
45            }
46        }
47        return a;
48    }
49
50    static String texto(int[] a) {
51        StringJoiner sj = new StringJoiner(" ");
52        for (int x : a) sj.add(String.valueOf(x));
53        return sj.toString();
54    }
55
56    public static void main(String[] args) {
57        Scanner sc = new Scanner(System.in);
58        int[] a = leer(sc);
59        if (a == null) return;
60        quicksort(a, 0, a.length - 1, 1);
61        int n = a.length;
62        System.out.println("Ordenado: " + texto(a));
63        System.out.println("Comparaciones: " + comparaciones + " · profundidad máxima: " + profundidadMaxima);
64        long peor = (long) n * (n - 1) / 2;
65        System.out.println(comparaciones == peor && n > 2 ? "¡El peor caso! Cada partición solo ha separado el pivote" : "Peor caso posible: " + peor + " comparaciones");
66    }
67}

Con datos ordenados, el último elemento es siempre el mayor del tramo: cada partición deja un lado vacío y el otro con un elemento menos. Son n − 1 particiones encadenadas (profundidad n) y n(n − 1)/2 comparaciones.

Con datos desordenados la profundidad es del orden de log n y las comparaciones, de n log n: la misma función, dos comportamientos muy distintos según la entrada.

Test

Test: Quicksort (ordenación rápida)

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.Tras una partición de quicksort, ¿qué elemento está seguro en su posición definitiva?

  2. 2.¿Cuál es el peor caso de quicksort con el último elemento como pivote?

  3. 3.¿Qué hace quicksort al volver de ordenar los dos lados?

  4. 4.¿Por qué Arrays.sort(int[]) usa quicksort y Arrays.sort(Object[]) no?

  5. 5.¿Qué técnica usa quicksort para evitar el peor caso en la práctica?

Relacionado