Objetivos observables¶
Identificás el problema que modela el capítulo y el TAD/estructura más adecuada.
Justificás decisiones de diseño con costo temporal/espacial y contrato de operaciones.
Aplicás el contenido en Java sin romper invariantes ni contrato público.
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:
cuánto cuesta insertar,
cuánto cuesta buscar,
cuánto cuesta recorrer,
y qué recursos extra hacen falta para sostener esas operaciones.
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:
cada operación elemental de lectura, escritura, comparación aritmética o salto condicional cuesta una unidad constante;
el costo total de un algoritmo es la suma de sus costos elementales;
el tamaño de entrada se denota con ;
una función de costo se escribe como 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 y funciones de , con para suficientemente grande.
si existen constantes y tales que para todo .
si existen constantes y tales que para todo .
si y .
En otras palabras:
Oda una cota superior asintótica,Ωda una cota inferior asintótica,Θfija el orden exacto de crecimiento.
Proposición 1¶
Si con , entonces .
Demostración.
Para se cumple:
y, como para suficientemente grande, existen constantes tales que:
Por definición, . ∎
Proposición 2¶
Si un bloque de código ejecuta un número fijo de operaciones elementales, su costo es .
Demostración.
El costo total es , independiente de . Tomando y , se verifica , luego . Como también para , se obtiene . ∎
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:
elegir una medida de tamaño de entrada, por ejemplo ,
identificar las operaciones relevantes,
y contar cómo crece la cantidad de trabajo en función de
n.
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 es la longitud del arreglo:
en el mejor caso se encuentra en la primera posición,
en el peor caso se revisan los elementos,
y el crecimiento del costo es lineal respecto de .
Demostración de la búsqueda lineal¶
Sea el número de comparaciones ejecutadas por contiene.
Si el buscado está en la primera posición, el algoritmo hace una comparación y termina: , luego .
Si el buscado no está o está al final, compara contra cada elemento: , luego .
En el caso general, el peor caso de la búsqueda lineal es 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 ).
| Notación | Idea informal | Qué comunica |
|---|---|---|
| cota superior | el algoritmo no crece más rápido que eso | |
| cota ajustada | el crecimiento es de ese orden | |
| cota inferior | el 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 .
Consecuencia útil¶
Si , entonces cualquier implementación cuyo costo pueda escribirse como 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:
(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.
(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.
(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.
(Lineal-Logarítmica): Típica de los mejores algoritmos de ordenamiento basados en comparaciones.
Ejemplo: MergeSort o QuickSort (en caso promedio).
(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:
no significa “siempre hace exactamente operaciones”,
no significa “gratis” (podría ser constante pero lentísimo),
y no siempre es mejor en la práctica si la constante oculta o la implementación (como seguir punteros dispersos en memoria) son demasiado costosas.
Proposición 3¶
Un doble bucle triangular sobre una colección de tamaño tiene costo .
Demostración.
Si el bucle externo corre veces y el interno corre veces en la iteración , el número total de pasos es:
Como:
el costo total es . ∎
Mejor caso, peor caso y caso promedio¶
Decir que una operación es 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ón | Mejor caso | Peor caso |
|---|---|---|
| Buscar en arreglo desordenado | ||
| Acceder por índice en arreglo | ||
| Buscar en BST balanceado | ||
| Buscar en BST degradado |
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:
nodos adicionales,
arreglos auxiliares,
recursión en pila de llamadas,
buffers o estructuras temporales.
Formalización del costo espacial¶
Si es la memoria total usada por un algoritmo sobre entradas de tamaño , conviene distinguir:
espacio de entrada : memoria donde ya viven los datos a procesar;
espacio auxiliar : memoria adicional reservada por el algoritmo.
Entonces:
En análisis de estructuras de datos, casi siempre se reporta , porque 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:
Costo estructural: memoria persistente para almacenar los datos (nodos, punteros, capacidad reservada).
Costo auxiliar por operación: memoria temporal durante una operación puntual.
Ejemplo: una tabla hash puede tener costo estructural en entradas más capacidad reservada, pero una operación lookup típica usa memoria auxiliar.
Algoritmos in-place y no in-place¶
Un algoritmo se considera in-place cuando su espacio auxiliar es (o, en algunas variantes, 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ón | Tiempo típico | Espacio auxiliar típico |
|---|---|---|
| Búsqueda lineal en arreglo | ||
| Búsqueda binaria iterativa | ||
| Búsqueda binaria recursiva | ||
| MergeSort | ||
| QuickSort (in-place, promedio) | por pila |
Costo espacial de recursión¶
Si una recursión tiene profundidad y cada frame agrega costo constante, su espacio auxiliar por pila es:
Por eso:
un recorrido DFS recursivo en árbol balanceado usa stack;
el mismo DFS en árbol degradado usa stack.
Consideraciones prácticas de memoria¶
Además del orden asintótico, conviene explicitar:
factor de sobrecarga por elemento (punteros, metadatos, padding);
capacidad ociosa esperada (por ejemplo en arreglos dinámicos o tablas hash);
localidad de memoria (contigua vs dispersa), que impacta caché y tiempo real;
picos de memoria durante redimensionamientos o fases de copia.
Estas consideraciones no reemplazan a , pero evitan decisiones erróneas cuando dos soluciones tienen el mismo orden asintótico.
Esto importa mucho en comparaciones como:
| Implementación | Ventaja temporal | Costo espacial típico |
|---|---|---|
| Arreglo | acceso por índice rápido | memoria contigua, poco overhead |
| Lista enlazada | inserción local eficiente | punteros extra por nodo |
| Tabla hash | lookup promedio rápido | buckets, capacidad ociosa, colisiones |
| Árbol balanceado | búsquedas garantizadas | metadatos o estructura adicional para balance |
Observación formal¶
Dos algoritmos pueden tener el mismo orden temporal 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 el número de comparaciones de una búsqueda binaria sobre un arreglo ordenado de tamaño .
La recurrencia es:
con .
Demostración de la cota.
Desplegando la recurrencia veces:
Cuando , la recursión termina. Eso ocurre para . Entonces:
y por lo tanto . Como además se hace al menos una comparación por nivel, . Luego:
Inserción al final en un arreglo dinámico¶
Sea un arreglo dinámico que duplica su capacidad cuando se llena.
Una inserción sin redimensionar cuesta .
Una inserción con redimensionamiento cuesta porque copia elementos.
La pregunta correcta es qué pasa en una secuencia larga de inserciones.
Demostración por método agregado.
Supongamos capacidades . Cada vez que se duplica la capacidad, se copian exactamente tantos elementos como capacidad actual tenga el arreglo. Si las inserciones totales son , cada elemento puede copiarse a lo sumo una vez por nivel de duplicación. La suma total de copias está acotada por:
El costo total de inserciones es entonces , y el costo amortizado por inserción es:
Eso justifica que append en un vector dinámico sea amortizado, aunque algunas inserciones individuales sean .
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:
Supongamos que cada operación básica cuesta “1 moneda” de tiempo de procesador.
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”.
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 :
1 unidad paga la escritura del nuevo elemento;
2 unidades se guardan como crédito.
Cuando el arreglo se llena y hay que duplicar:
cada elemento copiado consume 1 crédito,
y esos créditos ya se fueron acumulando en inserciones previas.
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 por operación.
Ejemplo: Arreglo Dinámico (ArrayList)¶
Supongamos un vector que arranca vacío, se llena y entonces duplica su capacidad:
El primer elemento cuesta 1 moneda insertarlo.
Cuando se llena (tamaño ), el siguiente requiere copiar elementos viejos y agregar el nuevo. Costo real: monedas.
Pero durante las inserciones rápidas previas, ahorramos suficientes “monedas” para pagar la copia costosa.
Una inserción puntual puede costar en tiempo real, pero una secuencia larga de inserciones al final tiene un costo amortizado 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:
¿Qué operaciones dominan el uso real?
¿Qué caso importa más: promedio, peor caso, amortizado?
¿Cuánta memoria extra se tolera?
¿La estructura necesita garantías fuertes o buen promedio alcanza?
Ejemplo: lista basada en arreglo vs lista basada en nodos¶
| Operación dominante | Arreglo dinámico | Lista enlazada |
|---|---|---|
| acceso por índice | muy bueno | malo |
| inserción local con referencia al nodo | no natural | muy buena |
| recorridos secuenciales | buena localidad de memoria | peor localidad |
| redimensionamiento | ocasional y costoso | no 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:
Usar Big-O como slogan. “Es ” no dice nada si no está claro qué caso se está midiendo.
Ignorar constantes y contexto. Dos soluciones con la misma cota asintótica pueden comportarse muy distinto.
Comparar operaciones aisladas sin mirar la secuencia real de uso.
Olvidar el costo espacial.
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:
operaciones principales,
costo temporal esperado,
costo espacial,
peor caso relevante,
trade-offs de implementación.
Ese patrón permite comparar:
arreglos con listas,
hash con árboles ordenados,
BST con árboles balanceados,
BFS con Dijkstra o Kruskal.
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:
qué estructura conviene,
qué implementación conviene,
y qué trade-offs se están aceptando.
Ejercicios¶
Próximo paso¶
Para seguir, conviene entrar a Secuencias, donde estas herramientas empiezan a aplicarse sobre la familia lineal más general.