Entendiendo la notación Big O sin complicaciones: cómo medir tu código

Cuando empezamos a programar nos encontramos con la temida notación Big O. A primera vista parece un concepto reservado a matemáticos, pero en realidad es una herramienta muy útil para cualquier desarrollador que quiera escribir código rápido y escalable. En este artículo te explico, paso a paso y sin tecnicismos innecesarios, cómo interpretar y medir la complejidad de tus funciones en JavaScript.
¿Qué es la notación Big O?
La notación Big O describe, de forma asintótica, cómo crece el tiempo de ejecución (o el uso de memoria) de un algoritmo respecto al tamaño de la entrada n. No nos dice cuánto tarda exactamente, sino cómo se comporta cuando n se hace muy grande. Los casos más habituales son:
O(1): tiempo constante, independiente del tamaño de la entrada.O(log n): tiempo logarítmico, típico de búsquedas binarias.O(n): tiempo lineal, recorre la entrada una sola vez.O(n log n): combinación de lineal y logarítmico, frecuente en algoritmos de ordenación eficientes.O(n²): tiempo cuadrático, ocurre cuando anidamos bucles.
¿Por qué medir la complejidad en la práctica?
Entender la complejidad nos ayuda a predecir cómo se comportará nuestra aplicación bajo carga. Un algoritmo O(n²) que funciona bien con 100 elementos puede volverse intolerable con 10 000. Detectar estos cuellos de botella antes de que el usuario los experimente ahorra tiempo y recursos.
Métodos simples para estimar la complejidad
Antes de lanzar herramientas de profiling, podemos hacer una estimación rápida siguiendo estos pasos:
- Identifica los bucles y cuántas veces se ejecutan.
- Comprueba si hay llamadas recursivas y cómo se reducen los parámetros.
- Cuenta las operaciones anidadas: cada nivel adicional suele multiplicar la complejidad.
- Busca operaciones que dependan de estructuras de datos específicas (por ejemplo,
indexOfen un array esO(n)).
Ejemplo práctico en JavaScript
Supongamos que queremos calcular la suma de todos los números pares de un array. Primero, una solución naïve con dos bucles anidados:
function sumaParesNaive(arr) {
let total = 0;
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length; j++) {
if (i === j && arr[i] % 2 === 0) {
total += arr[i];
}
}
}
return total;
}
Este código recorre arr n veces dentro de otro bucle de n iteraciones, por lo que su complejidad es O(n²). Podemos simplificarlo a una única pasada:
function sumaParesOptimizada(arr) {
let total = 0;
for (let i = 0; i < arr.length; i++) {
if (arr[i] % 2 === 0) {
total += arr[i];
}
}
return total;
}
Aquí solo iteramos una vez, logrando O(n). La diferencia es enorme cuando arr contiene miles de elementos.
Herramientas para medir tiempo real
Una vez que creemos que hemos reducido la complejidad, es útil medir el tiempo real de ejecución. En el navegador o Node.js podemos usar:
console.time('etiqueta')yconsole.timeEnd('etiqueta')para mediciones rápidas.- El API
performance.now()que ofrece mayor precisión en milisegundos decimales. - Paquetes como
benchmarkoautocannonpara pruebas de carga más exhaustivas.
Ejemplo con performance.now():
const arr = Array.from({ length: 100000 }, (_, i) => i);
let t0 = performance.now();
const resultado = sumaParesOptimizada(arr);
let t1 = performance.now();
console.log(Resultado: ${resultado}, tiempo: ${t1 - t0} ms);
Conclusiones
La notación Big O no es una fórmula mágica, pero sí un lenguaje común que nos permite razonar sobre la eficiencia de nuestro código. Con un par de pasos simples —identificar bucles, contar anidaciones y probar con herramientas de timing— podemos pasar de una solución O(n²) a una mucho más ágil O(n). Recuerda que la claridad del código también cuenta: una función bien nombrada y documentada facilita la revisión de su complejidad y evita errores futuros.
Así que la próxima vez que sientas que tu aplicación se vuelve lenta, abre el inspector, mide, y revisa la complejidad. ¡Verás cómo la optimización deja de ser una pesadilla y se convierte en una práctica natural del desarrollo!