19. Notación Big-O

La notación Big-O expresa una cota superior asintótica: indica que el costo de un algoritmo no crece más rápido que cierta función, salvo por factores constantes y para entradas suficientemente grandes.

19.1 Introducción

Big-O es una herramienta para describir crecimiento, no una unidad de tiempo. Decir que un algoritmo es O(n²) no significa que tarde n² segundos ni que haga exactamente n² operaciones. Significa que su costo puede acotarse superiormente por una constante multiplicada por n² a partir de cierto tamaño de entrada.

Esta abstracción permite comparar algoritmos aunque se ejecuten en computadoras distintas, estén escritos en lenguajes diferentes o tengan constantes internas diferentes.

19.2 Definición formal

f(n) pertenece a O(g(n)) si existen constantes positivas c y n0 tales que:

0 ≤ f(n) ≤ c · g(n), para todo n ≥ n0.

La función g(n) es una cota superior. La constante c absorbe diferencias de implementación y n0 permite ignorar el comportamiento de entradas pequeñas, donde los términos de menor orden pueden ser relevantes.

19.3 Interpretar las constantes

Para demostrar que f(n) = 3n + 10 es O(n), debemos encontrar c y n0 que satisfagan 3n + 10 ≤ cn para n grande.

Si n ≥ 10, entonces 10 ≤ n.
Por lo tanto, 3n + 10 ≤ 3n + n = 4n.

Podemos tomar c = 4 y n0 = 10.
Así, 3n + 10 pertenece a O(n).

No necesitamos encontrar las mejores constantes. Basta con exhibir algunas que hagan verdadera la desigualdad.

19.4 Ejemplo: un polinomio cuadrático

Demostremos que f(n) = 3n² + 7n + 12 es O(n²).

Para n ≥ 1, se cumple n ≤ n² y 1 ≤ n².

3n² + 7n + 12
≤ 3n² + 7n² + 12n²
= 22n².

Tomamos c = 22 y n0 = 1.
Entonces f(n) pertenece a O(n²).

El argumento ilustra una regla general: un polinomio de grado k con coeficientes positivos es O(nk).

19.5 Big-O no describe una igualdad exacta

La expresión «T(n) = O(n)» se usa habitualmente, pero no debe interpretarse como una igualdad numérica. O(n) representa un conjunto de funciones que no crecen más rápido que una función lineal, salvo constantes.

1 pertenece a O(n).
log n pertenece a O(n).
n pertenece a O(n).
n² no pertenece a O(n).

Por eso, decir solo «O(n)» puede ser una cota correcta pero poco precisa para una función que en realidad es constante.

Cuando queremos describir un orden ajustado usaremos Θ (Theta), que estudiaremos más adelante.

19.6 Reglas de simplificación

ExpresiónCota Big-ORazón
7O(1)No depende de n.
5n + 3O(n)Domina el término lineal.
n² + 100nO(n²)Domina n².
n log n + nO(n log n)n log n domina n para n grande.
2n + n10O(2n)La exponencial domina al polinomio.

Eliminar constantes y términos menores es válido solo en análisis asintótico. No debemos hacerlo si necesitamos una estimación exacta de tiempo, memoria o dinero.

19.7 Operaciones secuenciales

Si un algoritmo ejecuta dos bloques consecutivos de costos T1(n) y T2(n), el costo total es la suma. Asintóticamente domina el bloque de mayor crecimiento.

Bloque 1: O(n).
Bloque 2: O(n²).

Total: O(n) + O(n²) = O(n²).

La regla no dice que el primer bloque no se ejecute, sino que su contribución se vuelve pequeña en comparación con n² cuando n crece.

19.8 Un recorrido lineal

function maximo(numeros) {
  if (numeros.length === 0) return undefined;

  let mayor = numeros[0];
  for (let i = 1; i < numeros.length; i++) {
    if (numeros[i] > mayor) mayor = numeros[i];
  }

  return mayor;
}

console.log(maximo([8, 3, 12, 5, 9])); // 12

El bucle ejecuta a lo sumo n - 1 comparaciones. Existe una constante c tal que el costo total es menor o igual que cn para n suficientemente grande; por lo tanto, el algoritmo es O(n).

19.9 Bucles anidados y O(n²)

Dos bucles independientes que recorren n elementos cada uno ejecutan el cuerpo interno n² veces. Si el cuerpo tiene costo constante, el algoritmo es O(n²).

function hayDuplicados(numeros) {
  for (let i = 0; i < numeros.length; i++) {
    for (let j = i + 1; j < numeros.length; j++) {
      if (numeros[i] === numeros[j]) return true;
    }
  }

  return false;
}

console.log(hayDuplicados([4, 8, 15, 16, 8])); // true
console.log(hayDuplicados([4, 8, 15, 16])); // false

En el peor caso, cuando no hay duplicados, se comparan todos los pares posibles. La cantidad exacta es n(n - 1)/2, que pertenece a O(n²).

19.10 Reducción a la mitad y O(log n)

Un algoritmo que elimina aproximadamente la mitad de los candidatos en cada paso es O(log n). La búsqueda binaria es el ejemplo clásico.

Tamaño inicial: n.
Después de 1 paso: n/2.
Después de 2 pasos: n/4.
Después de k pasos: n/2k.

Cuando n/2k ≤ 1, se obtiene k ≥ log2(n).

La eficiencia logarítmica depende de que cada paso descarte una fracción constante de la entrada y no solo unos pocos elementos.

19.11 Big-O de un algoritmo recursivo

Para algoritmos recursivos, primero planteamos una recurrencia y después obtenemos una cota. Merge sort cumple aproximadamente T(n) = 2T(n/2) + cn, cuyo resultado es O(n log n).

Dos subproblemas de tamaño n/2.
Trabajo lineal para combinar.
log n niveles de recursión.
n unidades de trabajo por nivel.

Complejidad: O(n log n).

La cota incluye todas las llamadas y el trabajo de combinación. Analizar solo una llamada recursiva daría una estimación incorrecta.

19.12 Big-O y peor caso

En muchos cursos se informa Big-O como cota de peor caso, pero la notación por sí misma no significa «peor caso». Debemos indicar qué función estamos acotando: tiempo máximo, mínimo, esperado o espacio usado.

Búsqueda lineal:
Mejor caso: O(1).
Peor caso: O(n).

Si se afirma «la búsqueda lineal es O(n)», normalmente se refiere a su tiempo de peor caso, pero conviene decirlo explícitamente.

19.13 Big-O de memoria

La notación Big-O también describe espacio. Un algoritmo que crea una copia de un arreglo de n elementos usa O(n) memoria adicional; una matriz n × n usa O(n²).

RecursoEjemploCota
Memoria constanteRecorrer un arreglo con un acumulador.O(1)
Memoria linealGuardar una copia del arreglo.O(n)
Pila recursivaFactorial recursivo.O(n)
MatrizTabla de distancias entre n nodos.O(n²)

19.14 Cotas correctas pero poco útiles

Si un algoritmo es O(n), también es O(n²), O(n³) y O(2n), porque esas funciones crecen más rápido. Sin embargo, dar una cota demasiado grande pierde información importante.

Búsqueda binaria es O(log n).
También es O(n), pero esa cota oculta su ventaja.

Un buen análisis busca la cota superior más ajustada que podamos justificar.

La notación Theta permitirá expresar formalmente que una función está acotada por arriba y por abajo por el mismo orden.

19.15 Reglas algebraicas útiles

ReglaEjemplo
Constante por funciónO(cg(n)) = O(g(n)).
SumaO(f(n) + g(n)) = O(max(f(n), g(n))).
ProductoO(f(n)) · O(g(n)) = O(f(n)g(n)).
TransitividadSi f ∈ O(g) y g ∈ O(h), entonces f ∈ O(h).

Estas reglas ayudan a combinar costos, pero debemos aplicarlas a expresiones válidas. Por ejemplo, el costo de una rama condicional no siempre es la suma de ambas ramas: en peor caso se toma la más costosa que pueda ejecutarse.

19.16 Analizar una combinación de bloques

Supongamos un algoritmo que primero ordena un arreglo y después lo recorre una vez. Si usamos un ordenamiento O(n log n), el costo total es:

Ordenar: O(n log n).
Recorrer: O(n).

Total: O(n log n + n) = O(n log n).

La fase lineal sigue siendo necesaria, pero no modifica el orden total. Esta forma de razonamiento ayuda a estimar pipelines de procesamiento compuestos por varias etapas.

19.17 Medición empírica y Big-O

Medir tiempos reales complementa Big-O. Las mediciones revelan constantes, efectos de caché, asignación de memoria, optimizaciones del lenguaje y distribuciones de entrada. Big-O explica la tendencia que esperamos al crecer n.

function operacionesLineales(n) {
  let contador = 0;
  for (let i = 0; i < n; i++) contador++;
  return contador;
}

console.log(operacionesLineales(10)); // 10
console.log(operacionesLineales(100)); // 100

Duplicar n duplica las operaciones principales de este ejemplo. Una medición de tiempo puede variar, pero el conteo revela el crecimiento lineal.

19.18 Errores frecuentes

  • Interpretar O(n) como «exactamente n operaciones».
  • Usar Big-O sin definir si se analiza tiempo, memoria, mejor caso o peor caso.
  • Conservar constantes y términos de menor orden al informar la cota asintótica.
  • Decir que un algoritmo O(n) es más lento que uno O(n²) porque 1 es menor que 2, sin considerar las constantes ni el tamaño real.
  • Presentar una cota válida pero demasiado amplia, como O(n²) para búsqueda binaria.
  • Aplicar reglas de suma y producto sin observar el flujo de control del algoritmo.

19.19 Qué debes recordar y conclusión

  • f(n) pertenece a O(g(n)) si f(n) queda acotada por c·g(n) para n suficientemente grande.
  • Big-O describe una cota superior asintótica, no un tiempo exacto.
  • Las constantes y los términos de menor orden se omiten al clasificar crecimiento.
  • Un bucle lineal es O(n), dos bucles independientes suelen ser O(n²) y dividir a la mitad es O(log n).
  • Las recurrencias permiten obtener Big-O de algoritmos recursivos.
  • Una cota puede ser correcta pero poco ajustada; conviene informar la más útil.

Big-O permite garantizar que un costo no crecerá más rápido que cierto orden. En el próximo tema estudiaremos Ω (Omega), la notación que expresa cotas inferiores asintóticas.