- La notación Big O expresa el rendimiento de una función como su patrón de crecimiento según cambia el tamaño de la entrada
- El artículo explica, con ejemplos, los casos más representativos de Big O: constante, logarítmico, lineal y cuadrático
- La complejidad temporal varía según la estructura de datos y el algoritmo, y se nota en tareas como ordenar o buscar en arreglos
- Para mejorar el rendimiento real del código, lo más importante es elegir la estructura de datos adecuada y eliminar operaciones innecesarias dentro de los bucles
- Big O siempre representa de la forma más simplificada posible la relación entre la entrada y el tiempo de ejecución, y al optimizar el rendimiento es importante medir el código directamente
Descripción general de la notación Big O
- La notación Big O es una forma de describir el patrón de crecimiento del tiempo de ejecución según el tamaño de la entrada (n), en lugar de medir el tiempo directamente
- Clasifica el tiempo de ejecución de una función según la entrada, y por lo general se analizan formas como constante (O(1)), logarítmica (O(log n)), lineal (O(n)) y cuadrática (O(n²))
- Este artículo lo explica con conceptos, ejemplos visuales y ejemplos de código reales para que incluso principiantes puedan entender cada caso
Iteración y algoritmos lineales
- La función
sum(n)es un ejemplo de una estructura iterativa que suma de 1 a n, y a medida que crece el valor de entrada n, el tiempo de ejecución también aumenta en proporción directa - En la práctica,
sum(1e9)tarda alrededor de 1 segundo ysum(2e9)unos 2 segundos, por lo que el tiempo de reloj (wall-clock time) crece con un patrón O(n) - La complejidad temporal es la relación entre la entrada de una función y su tiempo de ejecución, y se expresa con la notación Big O (O(n) — proporcional a n)
- En lugar de iterar, usar la fórmula matemática
sum(n) = (n*(n+1))/2hace que el tiempo de ejecución sea constante e independiente del valor de entrada n - A este tipo de función se le llama complejidad temporal constante O(1), y su rasgo principal es que el tiempo de ejecución no crece cuando cambia la entrada
Sintaxis de la notación Big O
- La O de Big O viene de “Order” (orden de crecimiento) y solo indica la forma del crecimiento en sí
- No representa el valor absoluto del tiempo de ejecución, sino solo el 'patrón' de crecimiento respecto a la entrada de manera concisa
- Por ejemplo, aunque una función sea O(n), no se escribe de forma complicada como 'O(2n)' o 'O(n+1)', sino que se elige solo el término más simple
Reducir el tiempo usando la estructura de la entrada
- Como en el ejemplo de la fórmula de
sum(n), mejorar el algoritmo puede cambiar la complejidad temporal de O(n) a O(1) - Sin embargo, tener complejidad constante no significa automáticamente que siempre sea más rápido; el tiempo total también depende del tipo de operación
- Un algoritmo O(n) puede ser más rápido que uno O(1) para cierta entrada, pero cuando el tamaño de la entrada crece, el enfoque O(1) siempre termina imponiéndose
Ordenamiento y algoritmos cuadráticos: ejemplo de Bubble Sort
- Bubble Sort es un ejemplo básico de ordenamiento que organiza un arreglo repitiendo intercambios entre elementos adyacentes
- Si el arreglo ya está ordenado, basta 1 pasada (O(n)); si está en orden inverso, hace falta recorrerlo n veces repetidamente → en el peor caso, el total de operaciones es n²
- Un algoritmo O(n²) aumenta mucho su tiempo de ejecución en forma cuadrática a medida que crece la entrada
- En el uso real, Big O siempre se basa en el peor caso (worst-case), aunque a veces también se indiquen el caso promedio o el mejor caso
- Aunque la cantidad de pasadas puede reducirse según el estado inicial del arreglo, por considerar el peor caso siempre se clasifica como complejidad temporal cuadrática
Búsqueda y algoritmos logarítmicos: ejemplo de búsqueda binaria
- La búsqueda binaria (Binary Search) estima el valor central de un rango ordenado y en cada paso descarta la mitad del espacio candidato
- Por ejemplo, para adivinar un número específico entre 1 y 100 se necesitan como máximo 7 intentos, y entre 1 y 1,000 millones también se puede lograr en menos de 31 intentos
- Como en cada paso la lista candidata se reduce a la mitad, el tiempo de ejecución es O(log n) (complejidad logarítmica)
- Los algoritmos logarítmicos muestran un crecimiento extremadamente lento cuando n aumenta, por lo que son mucho más eficientes que los lineales o cuadráticos
- Al comparar gráficas, la diferencia de crecimiento entre log n, n y n² se vuelve muy evidente
Aplicación práctica: consejos para mejorar la complejidad temporal
Buscar elementos en una lista
- De forma básica, una función que busca un valor en un arreglo corresponde a O(n)
- Si la búsqueda es frecuente, usar una estructura de datos como Set permite mejorar a O(1)
- Sin embargo, el proceso de conversión con
new Set(array)es en sí mismo O(n), así que solo conviene cuando habrá consultas frecuentes (hay que considerar el costo de conversión) - Ejemplo:
items.has("banana")ofrece complejidad temporal constante
Escribir bucles aprovechando el índice
-
Es común que código como el siguiente, que usa
.indexOfdentro del bucle, sea una causa de problemas de rendimientofunction buildList(items) { const output = []; for (const item of items) { const index = items.indexOf(item); output.push(`Item ${index + 1}: ${item}`); } return output.join("\n"); } -
Como
.indexOfes una operación O(n) dentro del bucle, en conjunto se convierte en un patrón O(n^2) -
Al usar iteración basada en índice o
forEach((item, index) => ...), se mejora a O(n)function buildList(items) { const output = []; for (let i = 0; i < items.length; i++) { output.push(`Item ${i + 1}: ${items[i]}`); } return output.join("\n"); }
Uso de memoización
-
En estructuras con cálculos repetidos, como el factorial cuando se llama varias veces, se puede mejorar el rendimiento aplicando caché de resultados (usando
Map) -
Las consultas en
Mapcorresponden a O(1), por lo que se minimiza el recálculo innecesario -
Sin embargo, el caché ayuda a mejorar el tiempo promedio y, aunque la complejidad temporal del peor caso no cambie, sí puede aportar una mejora eficiente del rendimiento
const cache = new Map(); function factorial(n) { if (cache.has(n)) { return cache.get(n); } if (n === 0) { return 1; } const result = n * factorial(n - 1); cache.set(n, result); return result; }
Evaluación del rendimiento y conclusión
- Al mejorar el rendimiento del código, además de la complejidad temporal teórica, hay que confirmar con pruebas de ejecución directas si realmente hubo mejora
- Big O expresa de la manera más esencial y simplificada el patrón de relación y crecimiento entre la entrada y el tiempo de ejecución
- Elegir un buen algoritmo y optimizar la estructura de datos permite maximizar la eficiencia del código
Resumen
- La notación Big O expresa la relación entre la entrada de una función y su tiempo de ejecución
- Principales niveles de rendimiento: O(1) (constante), O(log n) (logarítmico), O(n) (lineal), O(n^2) (cuadrático)
- Para escribir código eficiente, es importante elegir el algoritmo adecuado y optimizar los bucles
- El rendimiento real debe medirse directamente para verificar si hubo mejora
- Usar gráficas comparativas de crecimiento ayuda a entender de un vistazo las características de la complejidad temporal
1 comentarios
Comentarios de Hacker News
Este artículo y los comentarios en HN continúan la tradición de explicar Big O Notation y debatir sobre su uso práctico y sus detalles técnicos. Como ejemplos útiles están este artículo explicativo y este texto sobre la actitud de los expertos
O(1) en la práctica usa una función hash, que no es algo trivial, pero sí implica un costo constante de operación. Si los datos son muy pocos, un algoritmo pésimo como O(n^2) incluso puede ser más rápido en tiempo real
Siento que la importancia moderna de Big-O ya no es la misma que antes. El hardware actual tiene multihilo, pipelines, NUMA, cachés complejos y demás, así que hay operaciones que terminan en menos de un ciclo y otras que tardan cientos o miles. Si uno intenta explicar un algoritmo solo por la cantidad de veces que se ejecuta el innermost loop, termina distorsionando la realidad. Y cuando se habla de Big-O, también habría que mencionar siempre otras notaciones como Big-Omega. (Por cierto, también disfruté la animación sobre Big-O)
Lo realmente interesante es que en computación cuántica hay operaciones cuyo costo crece como O(n^7) con respecto al número de átomos, pero aun así los científicos no tienen miedo de ejecutar esos cálculos. Porque N es lo bastante pequeño, las computadoras y la memoria siguen mejorando, y el resultado tiene un valor enorme. (No soy especialista en ciencias de la computación, así que disculpen si usé mal la notación O())
Me encantó la visualización. Incluso habiendo estudiado algoritmos antes, seguir viéndolo de forma visual me ayuda muchísimo
Tal vez porque estudié ingeniería eléctrica, siempre sentí que Big O Notation se trataba como un concepto que se salta por encima a grandes rasgos. Siempre lo manejaban como si fuera algo obvio, así que no recuerdo haber visto una explicación realmente amable. Me da curiosidad en qué nivel de matemáticas o computación se presenta por primera vez este concepto
La visualización dinámica ayudó muchísimo a entenderlo. Ojalá hagan más lecciones y materiales como este
Cada vez que aparece un hilo sobre Big-O Notation, siempre espero que alguien explique cómo se conecta el concepto con el anime The Big O. Todavía no entiendo muy bien de qué va ese anime
Personalmente, creo que la manera más efectiva de entender Big O Notation es relacionarlo con situaciones cotidianas
Me parece un material hermoso. Les mandé una señal y espero que haya llegado bien; siento como si me hubiera caído una cucharadita de dopamina sin querer