Notación asintótica O, Ω y θ y relaciones de recurrencia
Notación asintótica explicada: O grande, omega y theta, jerarquía de órdenes, comparar costes con límites y resolver recurrencias de sustracción y división, con ejemplos resueltos.
Abrir el simulador
Compara dos costes con el límite del cociente, busca c y n₀ y resuelve recurrencias con su árbol de llamadas.
Un algoritmo tarda 3n2+4n+4 pasos y otro 4n+4. ¿Cuál es mejor? Para tallas pequeñas la respuesta depende de los detalles, pero a partir de cierto tamaño siempre gana el segundo, porque crece más despacio. La notación asintótica sirve para decir eso con precisión: olvida las constantes y las tallas pequeñas y se queda con lo que decide en problemas grandes, cómo crece el coste. Es el idioma con el que se comparan algoritmos.
Los dos costes, escritos en función de n: 3n^3, n log(n), 2^n, sqrt(n)…
Ejemplos
Parejas típicas: 3n3 frente a 600n2, la base del logaritmo, 2n frente a n3…
Escala
Eje vertical lineal o logarítmico, para ver juntas funciones muy distintas
n · hasta
Hasta qué talla se dibuja (de 10 a un millón)
El panel de la derecha da el límite del cocientef/g y la conclusión, los valores de c y n0 que cumplen la definición de O y de Ω (con c⋅g(n) dibujada a trazos) y la talla en la que las dos curvas se cruzan.Pestaña «Recurrencias»
Se muestran la recurrencia, su expansión tras i pasos, el coste de cada nivel del árbol de llamadas, la solución según la tabla y una comprobación numérica: T(n) calculado con la recurrencia y dividido entre el orden de la solución, que debe tender a una constante.
El logaritmo se escribe log y es el neperiano. La base no cambia el orden; si quieres log2n exacto, escribe log(n)/log(2).
La talla es lo que mide el tamaño del problema: el número de elementos de un vector, las dimensiones de una matriz, el propio número. Un paso es un fragmento de código que tarda un tiempo acotado por una constante, sin importar la talla: una suma, una comparación, una asignación. El coste se mide en pasos, no en segundos.Según el principio de invarianza, dos implementaciones del mismo algoritmo solo se diferencian en una constante multiplicativa. Un ordenador diez veces más rápido divide el tiempo entre diez, pero no cambia la forma en que crece. Por eso las constantes se ignoran y se estudia el orden.
Sean f,g:N→R+, con f(n) el coste del algoritmo.Cota superior, O:g domina a f a partir de un punto.f(n)∈O(g(n))⟺∃c∈R+,∃n0∈N:f(n)≤cg(n)∀n≥n0Cota inferior, Ω: hay un múltiplo de g que f nunca baja.f(n)∈Ω(g(n))⟺∃c∈R+,∃n0∈N:f(n)≥cg(n)∀n≥n0Orden exacto, θ: las dos a la vez.f(n)∈θ(g(n))⟺∃c1,c2,n0:c1g(n)≤f(n)≤c2g(n)∀n≥n0Para demostrar que f∈O(g) basta con dar una pareja c, n0 que funcione. Por ejemplo, 3n+2∈O(n) porque con c=4 se cumple 3n+2≤4n para todo n≥2. Es justo lo que calcula el simulador.
Como O(n)⊂O(n2), la función 3n+2 está en O(n), pero también en O(n2) y en O(2n). Siempre se da la cota más ajustada. Con Ω las inclusiones van al revés.
Si con la talla fija el coste cambia según los datos (las instancias), se estudian el mejor y el peor caso. Entonces el coste se da con Ω para el mejor caso y con O para el peor. La búsqueda secuencial es Ω(1) (el elemento está el primero) y O(n) (no está). Si no hay instancias, se da directamente con θ.
El coste de un algoritmo recursivo se escribe en función de sí mismo:T(n)={c1aT(s(n))+p(n)en el caso baseen otro casodonde a es el número de llamadas, s(n) la talla de cada una (normalmente n−b o n/b) y p(n) el trabajo que no es recursivo. Se resuelve por sustitución: se cambia T(⋅) por su definición una y otra vez hasta ver el término general, y se llega al caso base.
Sustracción (la talla se reduce restando b):T(n)=aT(n−b)+cnk⟹T(n)∈⎩⎨⎧θ(nk)θ(nk+1)θ(andivb)a<1a=1a>1División (la talla se divide entre b):T(n)=aT(n/b)+cnk⟹T(n)∈⎩⎨⎧θ(nk)θ(nklogn)θ(nlogba)a<bka=bka>bk
En los exámenes de Algoritmia la tabla no se puede usar, salvo que se diga lo contrario: la complejidad se obtiene resolviendo la recurrencia. La tabla sirve para comprobar el resultado.
Por qué salen esos tres casos: el árbol de llamadas
En el modelo de división, el nivel j del árbol tiene aj llamadas de talla n/bj, y cada una hace c(n/bj)k pasos. El nivel entero cuesta:aj⋅c(bjn)k=cnk(bka)jEs una progresión geométrica de razón a/bk, con logbn niveles:
Si a<bk, cada nivel cuesta menos que el anterior y manda la raíz: θ(nk).
Si a=bk, todos los niveles cuestan lo mismo, cnk, y hay logbn: θ(nklogn).
Si a>bk, cada nivel cuesta más y mandan las hojas, que son alogbn=nlogba.
El simulador dibuja una barra por nivel con su coste: se ve de un vistazo cuál de los tres casos es.
Demuestra que 10n2+4n+2∈θ(n2).Cota inferior:10n2+4n+2≥10n2 para todo n≥0, así que vale c1=10.Cota superior: para n≥1, 4n≤4n2 y 2≤2n2, luego 10n2+4n+2≤16n2. Vale c2=16 con n0=1.Con el límite es inmediato: limn210n2+4n+2=10, una constante distinta de cero. El simulador encuentra c=11 con n0=5: también vale, porque no hay una única pareja.
Ejemplo 2 · ¿3n³ o 600n²?
Asintóticamente es mejor 600n2, porque lim600n23n3=lim200n=∞.Pero 3n3≥600n2⟺n≥200. Para tallas menores que 200, el algoritmo cúbico da menos pasos. Si casi todos los problemas reales son pequeños, conviene el cúbico; para cualquier talla, se pueden combinar los dos y llamar a uno u otro según n.
Ejemplo 3 · Factorial por sustitución
Función Factorial(n: entero) retorna (entero) si (n = 0) entonces retorna 1 sino retorna n * Factorial(n-1)
T(n)={c1T(n−1)+c2n=0n>0Expandiendo: T(n)=T(n−1)+c2=T(n−2)+2c2=⋯=T(n−i)+ic2. Se llega al caso base cuando i=n:T(n)=c1+nc2∈θ(n)Comprobación: sustracción con a=1, b=1, k=0: θ(nk+1)=θ(n). ✓
Ejemplo 4 · Mergesort
Divide el vector en dos mitades, ordena cada una y las mezcla en tiempo lineal:T(n)=2T(n/2)+cnExpandiendo: T(n)=2iT(n/2i)+icn. El caso base se alcanza con n/2i=1, es decir, i=log2n:T(n)=nT(1)+cnlog2n∈θ(nlogn)Comprobación: división con a=2, b=2, k=1; como a=bk=2, sale θ(nlogn). ✓ En el árbol, los log2n niveles cuestan cn cada uno.
Ejemplo 5 · Una recurrencia que hay que acotar
Función Ejemplo(n) retorna (entero) si n = 1 entonces retorna 1 sino retorna n * Ejemplo(n-1) * Ejemplo(n div 2)
T(n)=T(n−1)+T(ndiv2)+c no se resuelve directamente. Se acota por los dos lados:
Por debajo, cambiando T(n−1) por el más pequeño T(ndiv2): T1(n)=2T1(n/2)+c∈θ(n).
Por arriba, cambiando T(ndiv2) por el más grande T(n−1): T2(n)=2T2(n−1)+c∈θ(2n).
Conclusión: T(n)∈Ω(n) y T(n)∈O(2n). Comprueba las dos en el simulador con «Dos llamadas n/2» y «Dos llamadas n − 1».
La notación asintótica describe tallas suficientemente grandes. En la práctica también importan:
Las constantes ocultas: un algoritmo teóricamente mejor puede esconder una constante enorme. La multiplicación de matrices de Strassen, θ(n2,81), solo compensa con matrices bastante grandes.
La talla real de los problemas: si siempre son pequeños, puede ganar el algoritmo asintóticamente peor.
La memoria: ganar tiempo suele costar espacio.
El coste de desarrollo: si un programa se usa pocas veces, puede compensar uno menos eficiente pero más sencillo.
La notación O la introdujo el matemático Paul Bachmann en 1894, y Edmund Landau la popularizó en teoría de números; por eso se llaman símbolos de Landau. En 1976, Donald Knuth propuso usar Ω y θ tal como se usan hoy en informática. El teorema que resuelve las recurrencias de división se conoce como teorema maestro desde que lo recogió el libro de Cormen, Leiserson, Rivest y Stein (1990), aunque se basa en un trabajo de Bentley, Haken y Saxe de 1980.
O(g) es un conjunto de funciones, así que lo correcto es f∈O(g). La forma f=O(g) es una costumbre muy extendida, pero no es una igualdad: no se puede leer al revés.
¿Qué pasa si el límite del cociente no existe?
El criterio del límite no decide, y hay que volver a la definición. Por ejemplo, f(n)=n para n par y f(n)=n2 para n impar está en O(n2) y en Ω(n), pero no en θ de ninguna de las dos.
¿Por qué Fibonacci recursivo es exponencial?
Porque cada llamada hace dos más, T(n)=T(n−1)+T(n−2)+c, y el árbol tiene un número de nodos que crece como φn, con φ≈1,618. Está entre θ(2n/2) y θ(2n), como se ve acotando por los dos lados.
¿Sirve para la memoria igual que para el tiempo?
Sí. Todo lo que se dice del coste temporal vale para el espacial: basta con contar celdas de memoria en lugar de pasos.
¿Qué orden tiene un bucle que divide i entre 2 en cada vuelta?
Logarítmico: si i empieza en n y se divide entre 2 hasta llegar a 1, da log2n vueltas. Es lo que pasa en la búsqueda binaria.