Quicksort (ordenación rápida)
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.
- 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
- Caso base. Un tramo con 0 o 1 elementos (
ini >= fin) ya está ordenado. - Elegir el pivote. En la partición de Lomuto, el pivote es el último del tramo,
a[fin]. - Partir. Un índice
imarca el final de la zona de los menores o iguales. Se recorre el tramo conj: cadaa[j] <= pivotese intercambia al final de esa zona. Al acabar, se intercambia el pivote cona[i + 1]: ya está en su sitio. - 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.
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}def quicksort(a, ini, fin):
if ini >= fin: # 0 o 1 elementos: ya está ordenado
return
p = particion(a, ini, fin) # el pivote queda en su sitio definitivo
print(f"pivote {a[p]} → posición {p}: {a}")
quicksort(a, ini, p - 1) # los menores o iguales que el pivote
quicksort(a, p + 1, fin) # y los mayores
def particion(a, ini, fin):
"""Partición de Lomuto: el pivote es a[fin]; los <= pivote pasan a la izquierda
y el pivote queda justo entre los dos grupos. Devuelve su posición."""
pivote = a[fin]
i = ini - 1 # a[ini..i] son los <= pivote
for j in range(ini, fin):
if a[j] <= pivote:
i += 1
a[i], a[j] = a[j], a[i]
a[i + 1], a[fin] = a[fin], a[i + 1]
return i + 1
a = [10, 80, 30, 90, 40, 50, 70]
quicksort(a, 0, len(a) - 1)
print("Resultado:", a)const texto = (a) => "[" + a.join(", ") + "]";
function quicksort(a, ini, fin) {
if (ini >= fin) return; // 0 o 1 elementos: ya está ordenado
const p = particion(a, ini, fin); // el pivote queda en su sitio definitivo
console.log(`pivote ${a[p]} → posición ${p}: ${texto(a)}`);
quicksort(a, ini, p - 1); // los menores o iguales que el pivote
quicksort(a, p + 1, fin); // y los mayores
}
/** Partición de Lomuto: el pivote es a[fin]; los <= pivote pasan a la izquierda
y el pivote queda justo entre los dos grupos. Devuelve su posición. */
function particion(a, ini, fin) {
const pivote = a[fin];
let i = ini - 1; // a[ini..i] son los <= pivote
for (let j = ini; j < fin; j++) {
if (a[j] <= pivote) {
i++;
[a[i], a[j]] = [a[j], a[i]];
}
}
[a[i + 1], a[fin]] = [a[fin], a[i + 1]];
return i + 1;
}
const a = [10, 80, 30, 90, 40, 50, 70];
quicksort(a, 0, a.length - 1);
console.log("Resultado: " + texto(a));using System;
class Program {
static string Texto(int[] a) => "[" + string.Join(", ", a) + "]";
static void Quicksort(int[] a, int ini, int fin) {
if (ini >= fin) return; // 0 o 1 elementos: ya está ordenado
int p = Particion(a, ini, fin); // el pivote queda en su sitio definitivo
Console.WriteLine(quot;pivote {a[p]} → posición {p}: {Texto(a)}");
Quicksort(a, ini, p - 1); // los menores o iguales que el pivote
Quicksort(a, p + 1, fin); // y los mayores
}
// Partición de Lomuto: el pivote es a[fin]; los <= pivote pasan a la izquierda
// y el pivote queda justo entre los dos grupos. Devuelve su posición.
static int Particion(int[] a, int ini, int fin) {
int pivote = a[fin];
int i = ini - 1; // a[ini..i] son los <= pivote
for (int j = ini; j < fin; j++) {
if (a[j] <= pivote) {
i++;
(a[i], a[j]) = (a[j], a[i]);
}
}
(a[i + 1], a[fin]) = (a[fin], a[i + 1]);
return i + 1;
}
static void Main() {
int[] a = { 10, 80, 30, 90, 40, 50, 70 };
Quicksort(a, 0, a.Length - 1);
Console.WriteLine("Resultado: " + Texto(a));
}
}<?php
function texto(array $a): string { return "[" . implode(", ", $a) . "]"; }
function quicksort(array &$a, int $ini, int $fin): void {
if ($ini >= $fin) return; // 0 o 1 elementos: ya está ordenado
$p = particion($a, $ini, $fin); // el pivote queda en su sitio definitivo
echo "pivote {$a[$p]} → posición $p: " . texto($a) . "\n";
quicksort($a, $ini, $p - 1); // los menores o iguales que el pivote
quicksort($a, $p + 1, $fin); // y los mayores
}
/** Partición de Lomuto: el pivote es $a[$fin]; los <= pivote pasan a la izquierda
y el pivote queda justo entre los dos grupos. Devuelve su posición. */
function particion(array &$a, int $ini, int $fin): int {
$pivote = $a[$fin];
$i = $ini - 1; // $a[$ini..$i] son los <= pivote
for ($j = $ini; $j < $fin; $j++) {
if ($a[$j] <= $pivote) {
$i++;
[$a[$i], $a[$j]] = [$a[$j], $a[$i]];
}
}
[$a[$i + 1], $a[$fin]] = [$a[$fin], $a[$i + 1]];
return $i + 1;
}
$a = [10, 80, 30, 90, 40, 50, 70];
quicksort($a, 0, count($a) - 1);
echo "Resultado: " . texto($a) . "\n";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.
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}import random
def particion_aleatoria(a, ini, fin):
"""Con un pivote al azar, el peor caso (O(n²)) ya no depende de cómo vengan los datos."""
r = random.randint(ini, fin) # una posición al azar del tramo…
a[r], a[fin] = a[fin], a[r] # …se lleva al final y se parte como siempre
return particion(a, ini, fin)/** Con un pivote al azar, el peor caso (O(n²)) ya no depende de cómo vengan los datos. */
function particionAleatoria(a, ini, fin) {
const r = ini + Math.floor(Math.random() * (fin - ini + 1)); // una posición al azar del tramo…
[a[r], a[fin]] = [a[fin], a[r]]; // …se lleva al final y se parte como siempre
return particion(a, ini, fin);
}// Con un pivote al azar, el peor caso (O(n²)) ya no depende de cómo vengan los datos.
static int ParticionAleatoria(int[] a, int ini, int fin) {
int r = Random.Shared.Next(ini, fin + 1); // una posición al azar del tramo…
(a[r], a[fin]) = (a[fin], a[r]); // …se lleva al final y se parte como siempre
return Particion(a, ini, fin);
}/** Con un pivote al azar, el peor caso (O(n²)) ya no depende de cómo vengan los datos. */
function particionAleatoria(array &$a, int $ini, int $fin): int {
$r = random_int($ini, $fin); // una posición al azar del tramo…
[$a[$r], $a[$fin]] = [$a[$fin], $a[$r]]; // …se lleva al final y se parte como siempre
return particion($a, $ini, $fin);
}Traza: quicksort de {10, 80, 30, 90, 40, 50, 70}
| Tramo | Pivote | Array tras partir | El 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
| Caso | Cuándo pasa | Coste |
|---|---|---|
| Mejor | Cada pivote cae en el centro del tramo | O(n log n) |
| Medio | Datos en orden aleatorio | O(n log n), unas 1,39·n log₂ n comparaciones |
| Peor | El 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.
- 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.sortde tipos primitivos en Java usa un quicksort de doble pivote (dos pivotes, tres zonas).std::sortde C++ yList.Sortde 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 == finen vez deini >= 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]cona[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:
3y luego7 10 4 3 20 15. - Salida:
El 3.º menor es 7 (2 particiones). - Errores:
No hay números,Número no válido: «x»yk no válido: «texto» (tiene que estar entre 1 y n).
Ejemplo
3 7 10 4 3 20 15
El 3.º menor es 7 (3 particiones)
Ver la solución explicada
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: Py, 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úmerosyNúmero no válido: «x».
Ejemplo
10 80 30 90 40 50 70
Ordenado: 10 30 40 50 70 80 90 Comparaciones: 13 · profundidad máxima: 5 Peor caso posible: 21 comparaciones
Ver la solución explicada
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 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.Tras una partición de quicksort, ¿qué elemento está seguro en su posición definitiva?
2.¿Cuál es el peor caso de quicksort con el último elemento como pivote?
3.¿Qué hace quicksort al volver de ordenar los dos lados?
4.¿Por qué
Arrays.sort(int[])usa quicksort yArrays.sort(Object[])no?5.¿Qué técnica usa quicksort para evitar el peor caso en la práctica?