Bubble sort en Python — explicado paso a paso (con animación)

Bubble sort es el algoritmo de ordenación más sencillo de entender, el primero que enseñan en clase y, a la vez, uno de los más lentos que existen. Si tuvieras que ordenar una fila de personas por altura comparando solo a cada una con la de al lado e intercambiándolas cuando estén descolocadas, eso es exactamente lo que hace bubble sort. Punto. Esa es toda la idea.
Esta entrada cubre el algoritmo en serio: qué hace paso a paso, su implementación en Python, JavaScript y Java, por qué su complejidad es O(n²) (con números reales), cuándo te puede salvar y cuándo no, y la comparativa rápida con los algoritmos que sí se usan en producción.
Si quieres el panorama completo de los 14 algoritmos de ordenación más conocidos y, sobre todo, qué algoritmo usa de verdad cada lenguaje (spoiler: ninguno usa bubble), tienes el pilar del clúster: Algoritmos de ordenación en Python — los 14 explicados.
Contenido
- 1 Bubble sort en una frase
- 2 Antes de seguir: qué quieren decir O, Θ y Ω
- 3 Cómo funciona — traza paso a paso
- 4 Implementación en Python
- 5 Implementación en JavaScript
- 6 Implementación en Java
- 7 Complejidad — por qué es O(n²)
- 8 Ver en acción — bubble sort sobre 10 listas fijas
- 9 Mitos que circulan sobre bubble sort
- 10 ¿Y en la stdlib de Python?
- 11 Trivia algorítmica
- 12 Cuándo usar bubble (y cuándo no)
- 13 Comparativa rápida
- 14 FAQ rápida
- 15 En resumen
Bubble sort en una frase
Recorrer la lista comparando elementos vecinos y, si están en el orden equivocado, intercambiarlos. Repetir hasta que no haya intercambios. El número más grande “burbujea” hacia el final en cada pasada — de ahí el nombre.
Aquí lo tienes funcionando sobre 12 valores. La animación va con su contador de comparaciones e intercambios en vivo, para que veas exactamente por qué es lento:
Antes de seguir: qué quieren decir O, Θ y Ω
En la tabla de complejidad vas a ver tres símbolos: O, Θ y Ω. Si no los conoces, parecen lo mismo. No lo son.
- O(n²) — “O grande”. Es la cota superior: el peor caso. “Como mucho hará del orden de n² operaciones.”
- Ω(n) — “Omega”. Es la cota inferior: el mejor caso. “Como mínimo hará n operaciones, ni con la lista perfecta puede ir más rápido.”
- Θ(n²) — “Theta”. Cuando cota superior e inferior coinciden, decimos que el algoritmo es Θ de esa función. Es el caso típico: lo que verás casi siempre.
Bubble sort es Ω(n) (mejor caso con lista ya ordenada, sale en una pasada), Θ(n²) (caso medio), O(n²) (peor caso con lista al revés). Mismo algoritmo, tres velocidades muy distintas según la entrada. Explicación más larga en el pilar del clúster.
Cómo funciona — traza paso a paso
Vamos a ordenar una lista pequeña, [3, 1, 4, 1, 5], a mano para que veas cada movimiento.
Pasada 1:
– Comparo 3 y 1. ¿3 > 1? Sí → intercambio → [1, 3, 4, 1, 5].
– Comparo 3 y 4. ¿3 > 4? No → sigo → [1, 3, 4, 1, 5].
– Comparo 4 y 1. ¿4 > 1? Sí → intercambio → [1, 3, 1, 4, 5].
– Comparo 4 y 5. ¿4 > 5? No → sigo → [1, 3, 1, 4, 5].
El 5 ya está en su sitio al final. Esto es la clave: cada pasada deja al menos un elemento más en su posición correcta, por el lado derecho.
Pasada 2:
– 1, 3 → no toca.
– 3, 1 → intercambio → [1, 1, 3, 4, 5].
– 3, 4 → no toca.
(El 4 ya está bloqueado al final, así que ahorramos esa comparación.)
Pasada 3:
– 1, 1 → no toca.
– 1, 3 → no toca.
No hubo intercambios en esta pasada → la lista está ordenada. Termina.
Total: 9 comparaciones, 4 intercambios. Para 5 elementos. Imagínate con un millón.
📥 Llévate el cheatsheet de Optimización Python (gratis)
PDF de 8 páginas con los patrones que más rendimiento te dan en Python: medir antes de optimizar, complejidad real, estructuras adecuadas, evitar trabajo repetido, profiling y cuándo paralelizar. Para tener al lado cuando algo va lento.
Sin spam. Te apuntas a la lista, descargas el cheatsheet y recibes contenido de rendimiento en Python cada semana.
Implementación en Python
La versión más limpia, con el truco de salir antes si la pasada fue “limpia”:
def ordenar(lista: list[int]) -> list[int]:
n = len(lista)
for i in range(n - 1):
intercambio = False
for j in range(n - 1 - i):
if lista[j] > lista[j + 1]:
lista[j], lista[j + 1] = lista[j + 1], lista[j]
intercambio = True
if not intercambio:
break
return lista
if __name__ == "__main__":
print(ordenar([3, 1, 4, 1, 5])) # [1, 1, 3, 4, 5]
Tres detalles:
- El
n - 1 - idel bucle interno: como cada pasada bloquea el mayor al final, no hace falta volver a tocarlo. - El
intercambiobooleano: si una pasada termina sin intercambios, la lista ya está ordenada y salimos antes. Esto es lo que da el mejor caso O(n). - Devuelvo la lista por comodidad, pero el método ordena in-place (modifica la original).
En entrevistas técnicas, mencionar explícitamente esa optimización de “salir antes si no hay intercambios” suma puntos. Demuestra que entiendes lo que hace cada línea, no solo el algoritmo.
Implementación en JavaScript
function ordenar(lista) {
const n = lista.length;
for (let i = 0; i < n - 1; i++) {
let intercambio = false;
for (let j = 0; j < n - 1 - i; j++) {
if (lista[j] > lista[j + 1]) {
[lista[j], lista[j + 1]] = [lista[j + 1], lista[j]];
intercambio = true;
}
}
if (!intercambio) break;
}
return lista;
}
console.log(ordenar([3, 1, 4, 1, 5])); // [1, 1, 3, 4, 5]
La estructura es idéntica. Lo único distinto: el intercambio usa array destructuring en lugar del tuple swapping de Python.
Implementación en Java
public static void ordenar(int[] lista) {
int n = lista.length;
for (int i = 0; i < n - 1; i++) {
boolean intercambio = false;
for (int j = 0; j < n - 1 - i; j++) {
if (lista[j] > lista[j + 1]) {
int tmp = lista[j];
lista[j] = lista[j + 1];
lista[j + 1] = tmp;
intercambio = true;
}
}
if (!intercambio) break;
}
}
Java necesita la variable temporal para el intercambio (no tiene tuple swap nativo). El resto, idéntico.
Complejidad — por qué es O(n²)
| Métrica | Mejor caso | Caso medio | Peor caso |
|---|---|---|---|
| Tiempo | Ω(n) | Θ(n²) | O(n²) |
| Memoria | O(1) | O(1) | O(1) |
| Estable | Sí | Sí | Sí |
¿De dónde sale el O(n²)? En cada pasada haces hasta n comparaciones, y haces hasta n pasadas. Multiplicas: n × n = n². Para n = 1.000, hablamos de hasta un millón de comparaciones. Para n = 1.000.000, un billón. Esto te da la diferencia entre tardar 1 segundo y tardar 10 horas en una operación rutinaria.
El mejor caso O(n) solo aparece si la lista ya está ordenada de entrada: una sola pasada confirma que no hay nada que intercambiar y sales. Por eso bubble se defiende sorprendentemente bien con listas casi ordenadas, aunque sigue sin ser la mejor opción ahí (insertion lo bate).
Por qué es estable: dos elementos iguales nunca se intercambian (la condición es >, estricta). Así, el orden relativo de los duplicados se conserva. Importante cuando ordenas, por ejemplo, una lista de objetos por una clave y quieres que el orden original se respete en los empates.
Ver en acción — bubble sort sobre 10 listas fijas
Toda la comparativa de este clúster usa las mismas 10 listas fijas (con n=15) para que cualquier algoritmo se pueda cruzar honestamente con cualquier otro. Estas son:
| # | Nombre | Qué es |
|---|---|---|
| 1 | ordenada | 1, 2, 3, ..., 15 |
| 2 | inversa | 15, 14, ..., 1 |
| 3 | casi_ordenada | 1..15 con un par de swaps |
| 4 | muy_desordenada | Patrón min-max alternado [1, 15, 2, 14, 3, 13, ...] |
| 5 | duplicados | Enteros del rango [1..5] repetidos |
| 6-10 | aleatoria_s1 .. aleatoria_s5 | 5 permutaciones aleatorias deterministas (semillas 1..5) |
Si quieres ver los valores literales de cada lista, hay un comando:
from sortlab import listas_estandar
for nombre, lista in listas_estandar(n=15).items():
print(f"{nombre:<16} = {lista}")
Y para correr bubble sobre las 10 y volcar la tabla:
from sortlab import bench_estandar, formato_tabla
print(formato_tabla(bench_estandar("bubble", n=15)))
Salida real (tiempo es el mejor de 5 mediciones; Caso y Θ observada los deduce la propia librería comparando con la teoría):
Lista Comp. Interc. Escr. Tiempo (ms) Caso Θ observada
--------------- ----- ------- ----- ----------- ----- -----------
ordenada 14 0 0 0.001 Mejor Θ(n)
inversa 105 105 210 0.018 Peor Θ(n²)
casi_ordenada 95 17 34 0.008 Peor Θ(n²)
muy_desordenada 84 49 98 0.011 Peor Θ(n²)
duplicados 84 39 78 0.010 Peor Θ(n²)
aleatoria_s1 104 62 124 0.014 Peor Θ(n²)
aleatoria_s2 104 54 108 0.013 Peor Θ(n²)
aleatoria_s3 102 62 124 0.013 Peor Θ(n²)
aleatoria_s4 102 69 138 0.014 Peor Θ(n²)
aleatoria_s5 95 50 100 0.011 Peor Θ(n²)
Cómo se lee esto:
ordenadaes el ÚNICO caso de “Mejor” — 14 comparaciones y cero intercambios. El truco del booleano “si no hubo intercambios, sal” se ve clarísimo: una pasada y bubble termina. Para n=15 esperaríamos n−1 = 14, y eso es exactamente lo que sale:Θ(n)cuadra al milímetro.- Todas las demás son “Peor”. Sí, también las aleatorias. La lectura honesta es que bubble vive cerca del peor caso cualquier momento en que la lista no esté ya ordenada. Las comparaciones oscilan entre 84 y 105 (el máximo teórico es
n(n-1)/2 = 105). Esto explica por qué bubble se considera inútil en producción. casi_ordenadasigue siendo Peor, aunque con muchos menos intercambios (17 frente a 105 deinversa). Bubble compara todo siempre que haya UNA inversión, aunque la mayoría de la lista esté bien.muy_desordenadayduplicadosquedan en 84 comparaciones: el patrón min-max y los duplicados hacen que algunas pasadas salgan limpias antes y la optimización del booleano se active alguna vez. Aún así, sigue siendo Peor.
¿Y cómo escala con n grande? Sobre una lista aleatoria (aleatoria_s1, igual estructura, distintos tamaños):
| n | Comparaciones | Tiempo |
|---|---|---|
| 100 | 4.940 | 0,5 ms |
| 1.000 | 499.149 | 52,6 ms |
| 10.000 | 49.986.222 | 5.364 ms (5,4 SEGUNDOS) |
Multiplicas n por 10 → el trabajo se multiplica por casi 100. Eso es Θ(n²) traducido a una experiencia humana: para 10.000 elementos, bubble tarda 5 segundos. Quicksort, sobre la misma entrada, tarda 16 milisegundos (335 veces más rápido).
Reproducirlo sin instalar nada
sortlab vive privado en el Python profesional: medir, optimizar y escalar (con todas las versiones explicadas, profiling y la justificación de cada número). Pero la idea es sencilla: cualquier algoritmo se puede instrumentar a mano. Esta es la versión instrumentada de bubble — copia, pega y ejecuta:
def bubble_medido(lista_original: list[int]) -> dict:
arr = list(lista_original)
comparaciones = 0
intercambios = 0
n = len(arr)
for i in range(n - 1):
intercambio = False
for j in range(n - 1 - i):
comparaciones += 1
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
intercambios += 1
intercambio = True
if not intercambio:
break
return {
"ordenada": arr,
"comparaciones": comparaciones,
"intercambios": intercambios,
}
# Las 5 listas con forma:
listas = {
"ordenada": list(range(1, 16)),
"inversa": list(range(15, 0, -1)),
"casi_ordenada": [1, 11, 3, 4, 5, 6, 7, 8, 9, 10, 2, 12, 13, 14, 15],
"muy_desordenada": [1, 15, 2, 14, 3, 13, 4, 12, 5, 11, 6, 10, 7, 9, 8],
"duplicados": [1, 1, 3, 2, 2, 2, 1, 5, 1, 5, 4, 1, 1, 1, 2],
}
for nombre, lista in listas.items():
r = bubble_medido(lista)
print(f"{nombre:<16} comp={r['comparaciones']:>3} swap={r['intercambios']:>3}")
Salida (idéntica a la columna “Comp.” y “Interc.” de la tabla de arriba para esas 5 listas).
Mitos que circulan sobre bubble sort
- “Bubble sort es el algoritmo más sencillo.” Discutible. Insertion sort es igual de sencillo o más, y tiene mejor rendimiento. La razón por la que bubble se enseña primero es histórica, no técnica.
- “Es bueno para listas casi ordenadas.” No del todo. Es aceptable (sigue comparando todo, aunque haga pocos intercambios), pero insertion lo bate por goleada en ese caso.
- “Si tienes pocos elementos da igual qué algoritmo uses.” Cierto solo si “pocos” significa menos de ~15-20. A partir de ahí, la diferencia se nota.
¿Y en la stdlib de Python?
No. Ningún lenguaje moderno lleva bubble sort en su biblioteca estándar. Python usa Timsort (list.sort, sorted); C++ usa Introsort (std::sort); Java usa Timsort para objetos y Dual-Pivot Quicksort para primitivos. Bubble se queda en los manuales como ejercicio pedagógico.
Trivia algorítmica
El nombre “bubble sort” apareció en publicaciones de los años 60. Donald Knuth lo trató con la frase clásica: “the bubble sort seems to have nothing to recommend it, except a catchy name and the fact that it leads to some interesting theoretical problems”. Es decir: el nombre es lo mejor que tiene.
Cuándo usar bubble (y cuándo no)
Sí:
- Para enseñar el concepto de ordenación, comparación e intercambio. Es pedagógicamente perfecto.
- Listas muy cortas (menos de 10-15 elementos) donde el overhead de algoritmos más sofisticados no compensa.
- Listas casi ordenadas donde solo unos pocos elementos están fuera de sitio, y solo si no tienes insertion sort a mano.
No:
- Producción seria. Casi cualquier otro algoritmo lo bate. Si lo ves en un código corporativo, es bandera roja.
- Listas grandes. A partir de unos miles de elementos se vuelve doloroso.
- Datos en streaming. No está diseñado para procesarse por trozos.
Comparativa rápida
Para que te hagas una idea del lugar que ocupa bubble dentro del clúster:
| Algoritmo | Mejor | Peor | Memoria | Estable | Ventaja |
|---|---|---|---|---|---|
| Bubble | Ω(n) | O(n²) | O(1) | Sí | Conceptualmente trivial |
| Insertion | Ω(n) | O(n²) | O(1) | Sí | Más rápido que bubble en casi todos los casos |
| Selection | Ω(n²) | O(n²) | O(1) | No | Mínimo número de intercambios |
| Quicksort | Ω(n log n) | O(n²) | O(log n) | No | Súper rápido en la práctica |
| Merge sort | Ω(n log n) | O(n log n) | O(n) | Sí | Garantiza O(n log n) siempre |
En la práctica: insertion sort hace lo mismo que bubble pero mejor en casi cualquier escenario. No hay razón práctica para preferir bubble sobre insertion, salvo enseñarlo. Y ya está.
FAQ rápida
¿Por qué se llama “bubble” sort?
Porque los valores más grandes “burbujean” hacia el final de la lista en cada pasada, como burbujas subiendo en el agua.
¿Existe un bubble sort estable?
Sí, el estándar ya lo es. Solo intercambia cuando un elemento es estrictamente mayor que el siguiente, así que los iguales mantienen su orden original.
¿Cuál es el “bubble sort optimizado”?
La versión con la salida temprana (el intercambio booleano de los ejemplos de arriba) y el n - 1 - i para no tocar elementos ya bloqueados. Aún así es O(n²); no hay forma de salir de ahí sin cambiar de algoritmo.
¿Bubble sort es recursivo?
La versión clásica es iterativa. Puedes escribir una recursiva, pero solo añade complejidad sin ventaja.
¿Sirve bubble para ordenar strings?
Sí, cualquier comparable. Cambia > por la comparación que toque. En Python, > ya funciona entre strings (orden lexicográfico).
¿Te ha valido esto?
Si te ha resultado útil, llévate el cheatsheet de Optimización Python en PDF — 8 páginas con cómo medir, cuándo importa la complejidad, qué estructura usar y profiling práctico. Para tener al lado cuando algo va lento. Gratis.
Sin spam. Email + cheatsheet de rendimiento + un correo por semana sobre medir y optimizar Python.
En resumen
Bubble sort es el “Hola, Mundo” de los algoritmos de ordenación. Sirve para entender el concepto, pero no para producir. Cualquier lenguaje moderno usa algo más sofisticado (Timsort en Python, Introsort en C++…); ninguno usa bubble. Conocerlo es importante; usarlo en serio, no.
Si quieres aprender a medir cuánto tarda tu código de verdad —no solo a recitar Big-O— y a elegir la estructura correcta para cada problema, ese es el camino del curso de El Pythonista. Y si esto te enganchó, te dejo abajo el cheatsheet de optimización.
¿Quieres aprender Python en orden, no a saltos?
Esto que has leído es solo una pieza. En El Pythonista lo verás todo encadenado: 11 módulos, 37+ horas de vídeo, 734 actividades y un proyecto real (MovieTracker) que crece contigo desde la primera variable hasta el deploy a producción.
37+ horas · 734 actividades · Proyecto real · Acceso de por vida · 14 días de garantía
