22. Comparación de complejidades

Comparar complejidades permite elegir algoritmos que sigan siendo viables cuando crecen los datos. La decisión combina el orden de crecimiento, las constantes, el tamaño esperado y los recursos disponibles.

22.1 Introducción

La complejidad resume cómo cambia el costo de un algoritmo cuando aumenta el tamaño de entrada. No reemplaza las mediciones, pero permite anticipar qué solución escalará y cuál se volverá demasiado costosa.

Una buena comparación no se limita a una etiqueta Big-O: debe indicar qué recurso se mide, qué caso se analiza, cuáles son las precondiciones y qué tamaño de datos espera la aplicación.

22.2 Jerarquía de crecimiento

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

La relación se interpreta para valores de n suficientemente grandes.

Las funciones ubicadas más a la izquierda crecen más lentamente. La diferencia entre n y n log n puede ser aceptable; la diferencia entre n² y 2n suele decidir si un problema es viable.

22.3 Tabla de complejidades frecuentes

ComplejidadPatrónEjemplo
Θ(1)Acceso o cálculo fijoAcceder a un arreglo por índice
Θ(log n)Reducir por un factorBúsqueda binaria
Θ(n)Recorrer una vezBuscar máximo
Θ(n log n)Dividir y combinarMerge sort
Θ(n²)Comparar paresDetección ingenua de duplicados
Θ(2n)Explorar subconjuntosFuerza bruta
Θ(n!)Explorar permutacionesProbar todos los órdenes

22.4 Comparar valores concretos

nlog₂ nnn log₂ n2ⁿ
10≈ 310≈ 331001 024
1 000≈ 101 000≈ 9 9661 000 000Inviable
1 000 000≈ 201 000 000≈ 19 931 56910¹²Inviable

Los órdenes exponenciales y factoriales se separan rápidamente del resto. En cambio, duplicar una entrada apenas agrega un paso en un algoritmo logarítmico.

22.5 Constantes frente a crecimiento

Para tamaños pequeños, una constante grande puede hacer que un algoritmo de mejor orden sea más lento. Por ejemplo, 1000n supera a n² mientras n sea menor que 1000; para entradas mayores, el cuadrático crece mucho más rápido.

Algoritmo A: 1000n.
Algoritmo B: n².

n = 100: A = 100 000, B = 10 000.
n = 10 000: A = 10 000 000, B = 100 000 000.

22.6 Comparar funciones con código

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

console.log(compararCostos(16));
// { logaritmico: 4, lineal: 16, linealLogaritmico: 64, cuadratico: 256 }
console.log(compararCostos(1024));
// { logaritmico: 10, lineal: 1024, linealLogaritmico: 10240, cuadratico: 1048576 }

Los valores representan tendencias, no tiempos exactos. Las operaciones reales y el entorno determinan las constantes.

22.7 Misma tarea, distintas complejidades

CondiciónAlgoritmoComplejidad
Arreglo no ordenadoBúsqueda linealΘ(n) en peor caso
Arreglo ordenadoBúsqueda binariaΘ(log n) en peor caso
Tabla hash bien dimensionadaBúsqueda por clavePromedio cercano a Θ(1)
Árbol balanceadoBúsqueda por claveΘ(log n)

La mejora puede requerir ordenar datos o invertir memoria en una estructura auxiliar. Por eso debe compararse el flujo completo y no una operación aislada.

22.8 Búsqueda lineal y binaria

function busquedaLineal(numeros, buscado) {
  for (let i = 0; i < numeros.length; i++) {
    if (numeros[i] === buscado) return i;
  }
  return -1;
}

function busquedaBinaria(numeros, buscado) {
  let inicio = 0;
  let fin = numeros.length - 1;
  while (inicio <= fin) {
    const centro = Math.floor((inicio + fin) / 2);
    if (numeros[centro] === buscado) return centro;
    if (buscado < numeros[centro]) fin = centro - 1;
    else inicio = centro + 1;
  }
  return -1;
}

const datos = [2, 5, 8, 11, 16, 21, 29, 34];
console.log(busquedaLineal(datos, 21)); // 5
console.log(busquedaBinaria(datos, 21)); // 5

Ambas devuelven el mismo resultado, pero la binaria depende de que el arreglo esté ordenado y descarta la mitad de los candidatos en cada paso.

22.9 Ordenar una vez o buscar muchas veces

Si habrá una sola consulta, ordenar un arreglo puede costar más que buscar linealmente. Si habrá muchas, pagar una vez O(n log n) por ordenar puede compensarse con búsquedas O(log n).

Una búsqueda lineal: O(n).
Ordenar y buscar una vez: O(n log n) + O(log n).

m búsquedas lineales: O(mn).
Ordenar y realizar m búsquedas: O(n log n + m log n).

22.10 Tiempo frente a memoria

EstrategiaTiempo de búsquedaMemoria adicional
Recorrer arregloΘ(n)Θ(1)
Arreglo ordenadoΘ(log n)Θ(1) para buscar
Tabla hashPromedio cercano a Θ(1)Θ(n)

La solución más rápida no siempre es la mejor si la memoria es limitada, si no se puede alterar el orden de datos o si las consultas son escasas.

22.11 Comparar ordenamientos

AlgoritmoPeor casoMemoriaUso típico
Insertion sortΘ(n²)Θ(1)Arreglos pequeños o casi ordenados.
Merge sortΘ(n log n)Θ(n)Rendimiento predecible y estable.
Heap sortΘ(n log n)Θ(1)Buen peor caso y poco espacio auxiliar.

Estabilidad, memoria, distribución de los datos y simplicidad pueden justificar una elección distinta aun cuando dos opciones tengan el mismo orden de tiempo.

22.12 Mejor, peor, promedio y amortizado

Una comparación debe usar el mismo caso para todos los algoritmos. Búsqueda lineal tiene mejor caso Θ(1) y peor Θ(n); búsqueda binaria ordenada tiene peor Θ(log n); merge sort es Θ(n log n) en los casos habituales.

El análisis amortizado estudia una secuencia de operaciones. Por ejemplo, un arreglo dinámico puede copiar n elementos al ampliar capacidad, pero sobre muchas inserciones al final el costo amortizado por inserción es Θ(1).

22.13 Varios parámetros

No todos los problemas dependen de una única n. Un recorrido de matriz es Θ(rc) para r filas y c columnas, y un recorrido de grafo típico es Θ(V + E). Reemplazar todo por n puede ocultar la estructura relevante.

Matriz: Θ(rc).
Grafo: Θ(V + E).
Comparar cadenas: depende de las longitudes de ambas.

Conservar los parámetros evita conclusiones engañosas.

22.14 Complejidad y mediciones

Big-O no contempla directamente caché, red, disco, paralelismo, asignación de memoria ni optimizaciones del motor. Por eso conviene combinar análisis y medición con entradas representativas.

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

console.log(contarHasta(10)); // 10
console.log(contarHasta(1000)); // 1000

El conteo confirma crecimiento lineal de la operación principal. Medir el tiempo real aportaría información sobre sus constantes.

22.15 Criterios para elegir

  1. Definir tamaños esperados y máximos de entrada.
  2. Comparar tiempo, memoria y requisitos de corrección.
  3. Incluir costos de preparar o indexar datos.
  4. Evaluar mejor, peor, promedio o amortizado según el caso de uso.
  5. Medir con datos representativos antes de optimizar prematuramente.

La complejidad orienta la decisión; el contexto del producto determina cuál de los compromisos es aceptable.

22.16 Errores frecuentes

  • Elegir un algoritmo solo por Big-O sin considerar el tamaño real.
  • Ignorar el costo de ordenar, construir índices o cargar datos.
  • Comparar peor caso de un algoritmo con mejor caso de otro.
  • Olvidar el espacio, las precondiciones y la frecuencia de uso.
  • Reducir problemas de varios parámetros a una sola n sin justificación.
  • Suponer que una mejora asintótica siempre gana con entradas pequeñas.

22.17 Qué debes recordar

  • La jerarquía de crecimiento permite anticipar escalabilidad.
  • Las constantes importan para tamaños pequeños; el orden domina al crecer n.
  • La misma tarea puede tener algoritmos de complejidades muy distintas.
  • El costo de preparación puede compensarse con muchas consultas.
  • Tiempo, memoria, casos y parámetros deben compararse juntos.

22.18 Resumen de decisiones

Datos pequeños y simplicidad: una solución cuadrática puede ser suficiente.

Datos grandes y muchas consultas: conviene invertir en ordenamiento, índices o estructuras adecuadas.

Recursos limitados: evaluar el intercambio entre tiempo y memoria.

Requisitos estrictos de latencia: priorizar también el peor caso.

22.19 Conclusión

Comparar complejidades permite elegir soluciones que continúen funcionando al crecer los datos. La mejor alternativa equilibra orden de crecimiento, memoria, frecuencia de uso, requisitos de corrección y datos reales.

En el próximo tema estudiaremos las funciones piso y techo, herramientas discretas útiles para expresar redondeos, divisiones enteras y límites de algoritmos.