18. Crecimiento de funciones

El crecimiento de una función indica cómo aumenta su valor cuando crece la entrada. En análisis de algoritmos importa más esa tendencia que el tiempo exacto de una ejecución aislada.

18.1 Introducción

Dos algoritmos pueden tardar casi lo mismo con una entrada pequeña y comportarse de manera completamente diferente con millones de datos. El análisis de crecimiento estudia qué ocurre cuando n aumenta sin límite.

Una función de costo T(n) puede contar comparaciones, operaciones, memoria o llamadas. Comparar sus tasas de crecimiento permite anticipar qué soluciones escalan mejor antes de ejecutarlas en producción.

18.2 Tamaño de entrada y función de costo

Una función de crecimiento debe indicar respecto de qué tamaño se mide. Para un arreglo, n puede ser su longitud; para un grafo, pueden ser V vértices y E aristas; para un número entero, puede ser la cantidad de bits de su representación.

T(n) = cantidad de operaciones sobre un arreglo de n elementos.
T(V, E) = costo de un algoritmo sobre un grafo.
T(b) = costo de procesar un entero de b bits.

La misma función puede parecer eficiente o ineficiente según el parámetro correcto.

18.3 Crecimiento exacto y crecimiento asintótico

El conteo exacto puede dar expresiones como T(n) = 3n² + 7n + 12. En análisis asintótico nos concentramos en el comportamiento para n grande: el término 3n² domina a los términos lineales y constantes.

Para n = 10: 3n² + 7n + 12 = 382.
Para n = 1 000: 3n² + 7n + 12 = 3 007 012.

El cociente (7n + 12) / 3n² se vuelve pequeño al crecer n.
Por eso el crecimiento es cuadrático.

Esto no significa que las constantes sean irrelevantes en la práctica. Significa que, para comparar escalabilidad, el tipo de crecimiento suele ser el factor decisivo.

18.4 Funciones constantes

Una función constante no depende del tamaño de entrada. Un algoritmo puede ejecutar una cantidad fija de operaciones sin importar si recibe 10 o 10 millones de elementos.

T(n) = 8.

Ejemplos: acceder a un elemento de un arreglo por índice, comparar dos números de tamaño fijo, devolver una propiedad almacenada.

Una operación constante dentro de un bucle deja de ser constante en total: si se repite n veces, el costo total pasa a ser lineal.

18.5 Crecimiento logarítmico

Una función logarítmica crece muy lentamente. Aparece cuando cada paso reduce el tamaño del problema por un factor constante, por ejemplo al dividir por dos.

log2(8) = 3
log2(1 024) = 10
log2(1 048 576) = 20.

Duplicar n agrega aproximadamente una sola iteración adicional.

La base del logaritmo cambia solo por un factor constante. En análisis asintótico, log2(n), log10(n) y ln(n) pertenecen a la misma clase de crecimiento.

18.6 Comparar pasos logarítmicos

function pasosParaReducirAMitad(n) {
  let pasos = 0;

  while (n > 1) {
    n = Math.floor(n / 2);
    pasos++;
  }

  return pasos;
}

console.log(pasosParaReducirAMitad(16)); // 4
console.log(pasosParaReducirAMitad(1024)); // 10

El algoritmo no calcula exactamente un logaritmo real, pero cuenta el mismo fenómeno: cuántas divisiones por dos hacen falta para llegar al caso base.

18.7 Crecimiento lineal

Una función lineal es proporcional a n. Si duplicamos la entrada, aproximadamente duplicamos el trabajo. Recorrer todos los elementos de un arreglo una vez es el ejemplo más común.

T(n) = 5n + 2.

n = 100 → cerca de 502 operaciones.
n = 200 → cerca de 1 002 operaciones.

El factor 5 y el término 2 no cambian que el crecimiento sea lineal.

Los algoritmos lineales suelen escalar bien. Aun así, con datos muy grandes o trabajo costoso por elemento, pueden requerir optimización.

18.8 Crecimiento linealítmico

La función n log n aparece cuando se realizan aproximadamente log n niveles de trabajo y cada nivel procesa n elementos. Es típica de algoritmos eficientes de ordenamiento por comparación, como merge sort.

Para n = 1 024: n log2(n) = 1 024 · 10 = 10 240.
Para n = 1 048 576: n log2(n) = 1 048 576 · 20.

Es mayor que n, pero mucho menor que n² para entradas grandes.

En muchos problemas de ordenamiento, n log n es una barrera fundamental cuando el algoritmo solo obtiene información comparando elementos.

18.9 Crecimiento cuadrático y polinómico

Una función cuadrática n² aparece al considerar todos los pares de una colección de n elementos. Más generalmente, las funciones nk con k constante se llaman polinómicas.

FunciónInterpretación frecuente
Todos los pares de elementos.
Tres bucles independientes o triples de elementos.
nkk niveles de combinaciones independientes.

Para n pequeño, un algoritmo cuadrático puede ser aceptable y más simple. El problema aparece cuando n crece: cuadruplicar n multiplica aproximadamente por dieciséis el trabajo de n².

18.10 Comparar funciones lineales y cuadráticas

function costos(n) {
  return {
    lineal: n,
    cuadratico: n * n,
    linealLogaritmico: Math.round(n * Math.log2(n))
  };
}

console.log(costos(10)); // { lineal: 10, cuadratico: 100, linealLogaritmico: 33 }
console.log(costos(1000)); // { lineal: 1000, cuadratico: 1000000, linealLogaritmico: 9966 }

Los valores no representan tiempos exactos, sino la magnitud relativa de las funciones. La distancia entre n² y n log n aumenta rápidamente.

18.11 Crecimiento exponencial

Una función exponencial tiene la variable en el exponente, como 2n o 3n. Cada incremento de n multiplica el valor por una constante; por eso crece mucho más rápido que cualquier polinomio.

210 = 1 024.
220 = 1 048 576.
230 = 1 073 741 824.

Sumar diez al índice multiplica el valor por 1 024.

La fuerza bruta que revisa todos los subconjuntos de n elementos tiene 2n posibilidades. Los algoritmos recursivos sin memoización, como Fibonacci directo, pueden tener este crecimiento.

18.12 Crecimiento factorial

La función factorial n! = n(n - 1)...1 crece incluso más rápido que una exponencial de base fija. Aparece al considerar todas las permutaciones de n elementos.

5! = 120.
10! = 3 628 800.
15! = 1 307 674 368 000.

Probar todas las rutas posibles, órdenes o asignaciones puede producir crecimiento factorial.

Los problemas factoriales exigen podas, heurísticas, programación dinámica u otros métodos para ser tratables con entradas moderadas.

18.13 Jerarquía usual de crecimiento

1 < log n < n < n log n < n² < n³ < 2n < n!

La comparación se entiende para n suficientemente grande.

La jerarquía ayuda a priorizar algoritmos. Un algoritmo Θ(n log n) suele ser una mejora sustancial sobre uno Θ(n²), mientras que pasar de Θ(2n) a Θ(n²) puede transformar un problema impracticable en uno resoluble.

18.14 Un ejemplo numérico de la jerarquía

nlog₂ nnn log₂ n2ⁿ
10≈ 3,310≈ 331001 024
20≈ 4,320≈ 864001 048 576
30≈ 4,930≈ 1479001 073 741 824

Incluso con valores pequeños, la función exponencial se separa rápidamente de las demás. La diferencia se vuelve extrema al aumentar n.

18.15 Funciones que parecen similares

Algunas diferencias que parecen pequeñas cambian la clase de crecimiento. Por ejemplo, n² y (n + 1)² pertenecen al mismo orden, pero n² y 2n no.

FuncionesRelación de crecimientoMotivo
n y 7n + 10Mismo ordenDifieren por constante y término menor.
n² y n² + 100nMismo ordenDomina n².
n² y n³Órdenes distintosn³/n² = n crece sin límite.
n³ y 2ⁿÓrdenes distintosLa exponencial supera a todo polinomio.

18.16 Escalabilidad práctica

El crecimiento asintótico no reemplaza las mediciones, pero orienta qué medir. Un algoritmo O(n²) con una constante pequeña puede superar a uno O(n log n) para entradas diminutas; sin embargo, cuando los datos crecen la tendencia suele imponerse.

Para elegir un algoritmo, considerar:
• tamaño esperado y máximo de entrada;
• frecuencia con que se ejecuta;
• memoria disponible;
• costo de operaciones individuales;
• simplicidad, mantenibilidad y requisitos de corrección.

La complejidad es una herramienta de diseño, no una excusa para ignorar el contexto de una aplicación.

18.17 Crecimiento de memoria

Las mismas funciones se usan para describir memoria. Un algoritmo que almacena un valor por cada entrada usa memoria lineal; una matriz de n por n usa memoria cuadrática; una llamada recursiva puede sumar espacio de pila.

Arreglo de n elementos: Θ(n) espacio.
Matriz n × n: Θ(n²) espacio.
Búsqueda binaria recursiva: Θ(log n) pila.
Factorial recursivo: Θ(n) pila.

Un algoritmo con buen tiempo puede ser inviable si su consumo de memoria crece demasiado. La evaluación completa considera ambos recursos.

18.18 Errores frecuentes

  • Comparar tiempos concretos sin definir el tamaño de entrada.
  • Creer que las constantes nunca importan en una implementación real.
  • Confundir n² con 2n; 2n es lineal y 2n es exponencial.
  • Asumir que dos bucles siempre implican crecimiento cuadrático.
  • Ignorar que una función puede depender de varios parámetros.
  • Usar solo tiempo y olvidar el crecimiento del espacio.

18.19 Qué debes recordar y conclusión

  • El crecimiento de una función describe su tendencia cuando aumenta la entrada.
  • El término dominante determina el orden asintótico de un polinomio.
  • La jerarquía usual es: constante, logarítmica, lineal, n log n, polinómica, exponencial y factorial.
  • Las funciones logarítmicas crecen muy lentamente; las exponenciales y factoriales, muy rápido.
  • El tamaño de entrada debe definirse antes de comparar costos.
  • Tiempo y memoria pueden tener tasas de crecimiento diferentes.

Comprender las tasas de crecimiento permite anticipar la escalabilidad de un algoritmo. En el próximo tema formalizaremos una cota superior mediante la notación Big-O.