Universidad Nacional de Luján

← Volver a Programación II

Introducción rápida a Complejidad Algorítmica

La complejidad algorítmica permite estimar cómo crece el costo de un algoritmo cuando crece el tamaño de la entrada.

Ese costo suele medirse en:

¿Por qué importa?

Dos algoritmos pueden resolver el mismo problema, pero escalar muy distinto.

Analizar complejidad ayuda a elegir soluciones que sigan siendo viables con datos grandes.

¿Qué significa “tamaño de entrada”?

Cuando hablamos de $n$, primero hay que definir qué está midiendo:

Un buen análisis siempre empieza aclarando esa variable.

Notación asintótica

La notación más usada es O-grande.

Cuando usamos O-grande, nos enfocamos en el crecimiento para valores grandes de $n$ y omitimos constantes y términos menores.

Ejemplo:

\[3n^2 + 5n + 20 \in O(n^2)\]

También aparecen otras dos notaciones:

Ejemplo típico:

\[7n + 3 \in O(n), \quad 7n + 3 \in \Omega(n), \quad 7n + 3 \in \Theta(n)\]

En una primera aproximación práctica:

Mejor, promedio y peor caso

Para un mismo algoritmo, el costo puede variar según la entrada.

En materias introductorias se prioriza el peor caso porque da una garantía robusta.

Regla práctica para analizar código

  1. Identificar la operación dominante.
  2. Contar cuántas veces se ejecuta según $n$.
  3. Quedarse con el término de mayor crecimiento.

Atajos útiles:

Ejemplo 1: búsqueda lineal

#include <stdbool.h>

bool contiene(const int v[], int n, int objetivo) {
    for (int i = 0; i < n; i++) {
        if (v[i] == objetivo) {
            return true;
        }
    }
    return false;
}

Análisis:

Ejemplo 2: doble ciclo

int contar_pares(const int v[], int n) {
    int contador = 0;
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            if ((v[i] + v[j]) % 2 == 0) {
                contador++;
            }
        }
    }
    return contador;
}

Análisis:

Ejemplo 3: recursividad simple

int suma_hasta(const int v[], int n) {
    if (n == 0) {
        return 0;
    }
    return v[n - 1] + suma_hasta(v, n - 1);
}

Relación de recurrencia:

\[T(n) = T(n-1) + O(1)\]

Entonces:

Este punto es importante: un algoritmo puede tener buen tiempo y, sin embargo, usar más memoria por la recursión.

Escalas de crecimiento más comunes

De menor a mayor crecimiento:

\[O(1) < O(\log n) < O(n) < O(n\log n) < O(n^2) < O(2^n) < O(n!)\]

Para entradas grandes, cambiar de $O(n^2)$ a $O(n\log n)$ suele tener un impacto muy grande en tiempos reales.

Relación con recursividad y backtracking

En recursividad y backtracking, la complejidad suele depender del árbol de llamadas.

Mini resumen