Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Estructuras de Datos

Análisis de algoritmos

Modelo de costo para comparar estructuras

Universidad Nacional de Rio Negro - Sede Andina

Objetivos observables

Esta página fija el lenguaje de complejidad que después debería aparecer cada vez que se compare una implementación con otra. La meta no es transformar la parte en una materia separada de algoritmos, sino dar el marco mínimo para discutir costo con criterio.

En estructuras de datos no alcanza con decir “funciona”. También hace falta poder decir:

Modelo formal de costo

Para que la comparación sea reproducible, hay que fijar qué se cuenta y qué se abstrae.

Supuestos del modelo

Tomamos un modelo uniforme:

  1. cada operación elemental de lectura, escritura, comparación aritmética o salto condicional cuesta una unidad constante;

  2. el costo total de un algoritmo es la suma de sus costos elementales;

  3. el tamaño de entrada se denota con nn;

  4. una función de costo se escribe como T(n)T(n) cuando depende solo del tamaño de entrada.

Ese modelo no intenta describir una CPU real. Intenta dar una medida estable para comparar algoritmos entre sí.

Definiciones asintóticas

Sean ff y gg funciones de nn, con g(n)>0g(n) > 0 para nn suficientemente grande.

En otras palabras:

Proposición 1

Si T(n)=an+bT(n) = a n + b con a>0a > 0, entonces T(n)Θ(n)T(n) \in \Theta(n).

Demostración.

Para nmax(1,b/a)n \ge \max(1, |b|/a) se cumple:

anban+ban+ba n - |b| \le a n + b \le a n + |b|

y, como anb(a/2)na n - |b| \ge (a/2)n para nn suficientemente grande, existen constantes c1,c2>0c_1, c_2 > 0 tales que:

c1nT(n)c2nc_1 n \le T(n) \le c_2 n

Por definición, T(n)Θ(n)T(n) \in \Theta(n). ∎

Proposición 2

Si un bloque de código ejecuta un número fijo kk de operaciones elementales, su costo es Θ(1)\Theta(1).

Demostración.

El costo total es T(n)=kT(n)=k, independiente de nn. Tomando c=kc=k y n0=1n_0=1, se verifica 0T(n)c0 \le T(n) \le c, luego T(n)O(1)T(n) \in O(1). Como también T(n)1T(n)\ge 1 para k>0k>0, se obtiene T(n)Θ(1)T(n) \in \Theta(1). ∎

Qué se analiza y qué se simplifica

Cuando se analiza un algoritmo, no se mide “segundos reales” en una máquina específica. Se construye un modelo más simple que permita comparar crecimiento de costo cuando el problema aumenta de tamaño.

El punto de partida suele ser:

Ejemplo simple

public static boolean contiene(int[] datos, int buscado) {
    for (int i = 0; i < datos.length; i++) {
        if (datos[i] == buscado) {
            return true;
        }
    }
    return false;
}

Búsqueda lineal

Si nn es la longitud del arreglo:

Demostración de la búsqueda lineal

Sea T(n)T(n) el número de comparaciones ejecutadas por contiene.

En el caso general, el peor caso de la búsqueda lineal es Θ(n)\Theta(n) porque no existe forma de certificar ausencia sin examinar toda la secuencia en un arreglo no ordenado. Esa afirmación no depende de la implementación concreta: depende de la información disponible.

Notación O, Theta y Omega

Estas tres notaciones permiten hablar de cotas asintóticas (límites de crecimiento cuando nn \to \infty).

NotaciónIdea informalQué comunica
O(f(n))O(f(n))cota superiorel algoritmo no crece más rápido que eso
Θ(f(n))\Theta(f(n))cota ajustadael crecimiento es de ese orden
Ω(f(n))\Omega(f(n))cota inferiorel algoritmo no crece más lento que eso

En esta parte, la notación que más se usa en comparaciones rápidas suele ser O()O(\cdot).

Consecuencia útil

Si f(n)Θ(g(n))f(n) \in \Theta(g(n)), entonces cualquier implementación cuyo costo pueda escribirse como ag(n)+ba\,g(n)+b tiene el mismo orden asintótico. Eso permite ignorar constantes multiplicativas y términos menores sin perder la comparación estructural.

Catálogo de Complejidades Comunes

Para tener intuición sobre qué significa cada cota en la práctica, conviene conocer las familias más frecuentes:

  1. O(1)O(1) (Constante): El costo no depende del tamaño de los datos.

    • Ejemplo: Acceder a arreglo[5]. Así el arreglo tenga 10 elementos o 10 millones, el tiempo es el mismo.

  2. O(logn)O(\log n) (Logarítmica): El costo crece muy lento. Típicamente ocurre cuando el algoritmo descarta la mitad de los datos en cada paso.

    • Ejemplo: Búsqueda binaria en un arreglo ordenado. Si el tamaño se duplica, solo hace falta un paso más.

  3. O(n)O(n) (Lineal): El costo crece proporcionalmente al tamaño de los datos. Hay que mirar, al menos, cada elemento una vez.

    • Ejemplo: Encontrar el máximo en un arreglo desordenado.

  4. O(nlogn)O(n \log n) (Lineal-Logarítmica): Típica de los mejores algoritmos de ordenamiento basados en comparaciones.

    • Ejemplo: MergeSort o QuickSort (en caso promedio).

  5. O(n2)O(n^2) (Cuadrática): El costo crece con el cuadrado del tamaño. Suele aparecer cuando hay bucles anidados procesando la misma colección.

    • Ejemplo: Comparar todos contra todos para encontrar pares duplicados. Si la entrada se multiplica por 10, el tiempo se multiplica por 100.

Conviene no olvidar que:

Proposición 3

Un doble bucle triangular sobre una colección de tamaño nn tiene costo Θ(n2)\Theta(n^2).

Demostración.

Si el bucle externo corre nn veces y el interno corre ii veces en la iteración ii, el número total de pasos es:

i=1n1i=n(n1)2\sum_{i=1}^{n-1} i = \frac{n(n-1)}{2}

Como:

n2n2Θ(n2)\frac{n^2-n}{2} \in \Theta(n^2)

el costo total es Θ(n2)\Theta(n^2). ∎

Mejor caso, peor caso y caso promedio

Decir que una operación es O(n)O(n) no agota la discusión. También importa en qué escenario se está midiendo.

Mejor caso

Es el escenario más favorable. A veces sirve para entender un límite inferior práctico, pero casi nunca alcanza para justificar una estructura.

Peor caso

Es el escenario más desfavorable. Suele ser el dato más útil cuando se necesita garantía.

Caso promedio

Depende de supuestos sobre la distribución de entradas. Puede ser muy informativo, pero también muy engañoso si esos supuestos no se explicitan.

Ejemplo:

OperaciónMejor casoPeor caso
Buscar en arreglo desordenadoO(1)O(1)O(n)O(n)
Acceder por índice en arregloO(1)O(1)O(1)O(1)
Buscar en BST balanceadoO(logn)O(\log n)O(logn)O(\log n)
Buscar en BST degradadoO(1)O(1)O(n)O(n)

En estructuras de datos, una parte grande del diseño consiste en decidir qué caso importa más para el problema real.

Regla práctica

Cuando el usuario necesita garantía, el peor caso manda. Cuando las operaciones se repiten muchas veces sobre datos cambiantes, el costo amortizado suele ser la mejor lectura. El promedio solo es útil si la distribución de entradas está definida y justificada.

Costo temporal y costo espacial

El análisis no se limita al tiempo. También importa cuánto espacio adicional consume una implementación.

Costo temporal

Cuenta cuánto trabajo hace la operación al crecer la entrada.

Costo espacial

Cuenta cuánta memoria extra requiere:

Formalización del costo espacial

Si M(n)M(n) es la memoria total usada por un algoritmo sobre entradas de tamaño nn, conviene distinguir:

Entonces:

M(n)=I(n)+A(n)M(n) = I(n) + A(n)

En análisis de estructuras de datos, casi siempre se reporta A(n)A(n), porque I(n)I(n) depende del problema y no de la estrategia de implementación.

Espacio auxiliar vs memoria de la estructura

En una estructura mutable, hay dos lecturas complementarias:

  1. Costo estructural: memoria persistente para almacenar los datos (nodos, punteros, capacidad reservada).

  2. Costo auxiliar por operación: memoria temporal durante una operación puntual.

Ejemplo: una tabla hash puede tener costo estructural Θ(n)\Theta(n) en entradas más capacidad reservada, pero una operación lookup típica usa O(1)O(1) memoria auxiliar.

Algoritmos in-place y no in-place

Un algoritmo se considera in-place cuando su espacio auxiliar es O(1)O(1) (o, en algunas variantes, O(logn)O(\log n) si se descuenta la pila de recursión controlada). Esta distinción importa porque dos algoritmos con igual tiempo pueden diferir fuerte en memoria.

Algoritmo / operaciónTiempo típicoEspacio auxiliar típico
Búsqueda lineal en arregloO(n)O(n)O(1)O(1)
Búsqueda binaria iterativaO(logn)O(\log n)O(1)O(1)
Búsqueda binaria recursivaO(logn)O(\log n)O(logn)O(\log n)
MergeSortO(nlogn)O(n \log n)O(n)O(n)
QuickSort (in-place, promedio)O(nlogn)O(n \log n)O(logn)O(\log n) por pila

Costo espacial de recursión

Si una recursión tiene profundidad d(n)d(n) y cada frame agrega costo constante, su espacio auxiliar por pila es:

Astack(n)Θ(d(n))A_{\text{stack}}(n) \in \Theta(d(n))

Por eso:

Consideraciones prácticas de memoria

Además del orden asintótico, conviene explicitar:

  1. factor de sobrecarga por elemento (punteros, metadatos, padding);

  2. capacidad ociosa esperada (por ejemplo en arreglos dinámicos o tablas hash);

  3. localidad de memoria (contigua vs dispersa), que impacta caché y tiempo real;

  4. picos de memoria durante redimensionamientos o fases de copia.

Estas consideraciones no reemplazan a O()O(\cdot), pero evitan decisiones erróneas cuando dos soluciones tienen el mismo orden asintótico.

Esto importa mucho en comparaciones como:

ImplementaciónVentaja temporalCosto espacial típico
Arregloacceso por índice rápidomemoria contigua, poco overhead
Lista enlazadainserción local eficientepunteros extra por nodo
Tabla hashlookup promedio rápidobuckets, capacidad ociosa, colisiones
Árbol balanceadobúsquedas garantizadasmetadatos o estructura adicional para balance

Observación formal

Dos algoritmos pueden tener el mismo orden temporal Θ()\Theta(\cdot) y distinto costo espacial. Por eso, al comparar estructuras de datos, el orden temporal no alcanza para decidir.

Demostraciones de costos típicos

Búsqueda binaria

Sea T(n)T(n) el número de comparaciones de una búsqueda binaria sobre un arreglo ordenado de tamaño nn.

La recurrencia es:

T(n)=T(n/2)+cT(n) = T(\lfloor n/2 \rfloor) + c

con T(1)=c0T(1)=c_0.

Demostración de la cota.

Desplegando la recurrencia kk veces:

T(n)=T(n/2k)+kcT(n) = T(\lfloor n/2^k \rfloor) + kc

Cuando n2k1\frac{n}{2^k}\le 1, la recursión termina. Eso ocurre para klog2(n)k \ge \log_2(n). Entonces:

T(n)c0+clog2(n)T(n) \le c0 + c \log_2(n)

y por lo tanto T(n)O(logn)T(n)\in O(\log n). Como además se hace al menos una comparación por nivel, T(n)Ω(logn)T(n)\in \Omega(\log n). Luego:

T(n)Θ(logn)T(n) \in \Theta(\log n)

Inserción al final en un arreglo dinámico

Sea un arreglo dinámico que duplica su capacidad cuando se llena.

La pregunta correcta es qué pasa en una secuencia larga de mm inserciones.

Demostración por método agregado.

Supongamos capacidades 1,2,4,8,1,2,4,8,\dots. Cada vez que se duplica la capacidad, se copian exactamente tantos elementos como capacidad actual tenga el arreglo. Si las inserciones totales son mm, cada elemento puede copiarse a lo sumo una vez por nivel de duplicación. La suma total de copias está acotada por:

1+2+4++2log2m<2m1 + 2 + 4 + \cdots + 2^{\lfloor \log_2 m \rfloor} < 2m

El costo total de mm inserciones es entonces Θ(m)\Theta(m), y el costo amortizado por inserción es:

Ttotal(m)Θ(m)    Ttotal(m)mΘ(1)T_{\text{total}}(m) \in \Theta(m) \;\Rightarrow\; \frac{T_{\text{total}}(m)}{m} \in \Theta(1)

Eso justifica que append en un vector dinámico sea O(1)O(1) amortizado, aunque algunas inserciones individuales sean O(n)O(n).

Análisis amortizado

Algunas operaciones son raramente caras, pero la mayoría de las veces son baratas. En esos casos conviene mirar el costo amortizado, que promedia el tiempo de ejecución sobre una secuencia de operaciones.

La Analogía del Ahorro (Método del Banquero o Monedas)

Para entender el costo amortizado, pensá en ahorrar monedas (tokens) para pagar el costo futuro:

  1. Supongamos que cada operación básica cuesta “1 moneda” de tiempo de procesador.

  2. Cada vez que hacés una operación rápida, le cobrás al usuario “3 monedas”. Gastás 1 para hacer la operación y guardás las 2 restantes en una “alcancía”.

  3. Cuando llega la operación costosa, usás las monedas ahorradas para “pagarla” sin pedir tiempo extra, porque en promedio, el presupuesto de 3 monedas por operación cubrió todo el trabajo.

Demostración del método de monedas para un vector dinámico

Asigná 3 unidades de crédito a cada append\texttt{append}:

  1. 1 unidad paga la escritura del nuevo elemento;

  2. 2 unidades se guardan como crédito.

Cuando el arreglo se llena y hay que duplicar:

Como cada elemento participa en a lo sumo una copia por redimensionamiento relevante, el crédito acumulado alcanza para cubrir todas las copias. Por lo tanto, la secuencia completa de inserciones tiene costo amortizado Θ(1)\Theta(1) por operación.

Ejemplo: Arreglo Dinámico (ArrayList)

Supongamos un vector que arranca vacío, se llena y entonces duplica su capacidad:

Una inserción puntual puede costar O(n)O(n) en tiempo real, pero una secuencia larga de inserciones al final tiene un costo amortizado O(1)O(1) por operación, porque las inserciones constantes pagan el redimensionamiento esporádico.

public void agregar(int valor) {
    if (this.cantidad == this.datos.length) {
        // Operación rara y costosa O(N)
        // Está "pagada" por los ahorros previos.
        redimensionar();
    }
    // Operación frecuente y rápida O(1)
    this.datos[this.cantidad] = valor;
    this.cantidad++;
}

Inserción al final con redimensionamiento ocasional

El análisis amortizado no niega el costo caro. Lo ubica correctamente dentro de una secuencia larga de operaciones para demostrar que, a la larga, el sistema no se degrada.

Qué no demuestra el costo amortizado

El amortizado no prueba que ninguna operación sea cara. Solo prueba que el promedio por operación, sobre una secuencia suficientemente larga, es acotado por una constante.

Cómo comparar implementaciones de una misma estructura

En esta parte, el análisis aparece sobre todo para comparar varias implementaciones del mismo TAD. Esa comparación conviene hacerla con preguntas concretas:

  1. ¿Qué operaciones dominan el uso real?

  2. ¿Qué caso importa más: promedio, peor caso, amortizado?

  3. ¿Cuánta memoria extra se tolera?

  4. ¿La estructura necesita garantías fuertes o buen promedio alcanza?

Ejemplo: lista basada en arreglo vs lista basada en nodos

Operación dominanteArreglo dinámicoLista enlazada
acceso por índicemuy buenomalo
inserción local con referencia al nodono naturalmuy buena
recorridos secuencialesbuena localidad de memoriapeor localidad
redimensionamientoocasional y costosono aplica

No hay una ganadora universal. La decisión depende del perfil de uso.

Criterio de decisión

Si una operación aparece en casi todas las rutas de ejecución, su costo domina. Si solo aparece esporádicamente, puede tolerarse una operación cara siempre que el amortizado sea bajo y el espacio extra sea razonable.

Qué errores conviene evitar

En esta parte conviene evitar varios abusos frecuentes:

  1. Usar Big-O como slogan. “Es O(1)O(1)” no dice nada si no está claro qué caso se está midiendo.

  2. Ignorar constantes y contexto. Dos soluciones con la misma cota asintótica pueden comportarse muy distinto.

  3. Comparar operaciones aisladas sin mirar la secuencia real de uso.

  4. Olvidar el costo espacial.

  5. Tomar el mejor caso como si fuera garantía.

Cómo usar este capítulo en el resto de la parte

Desde acá en adelante, cada estructura conviene leer con la misma grilla:

Ese patrón permite comparar:

Resumen

No alcanza con que una estructura “funcione”. También hace falta poder explicar cuánto cuesta usarla y por qué una representación conviene más que otra en un contexto dado.

En esta parte, la notación asintótica no se usa para decorar texto. Se usa para tomar decisiones:

Ejercicios

Próximo paso

Para seguir, conviene entrar a Secuencias, donde estas herramientas empiezan a aplicarse sobre la familia lineal más general.