notas/complejidad_algoritmica/complejidad_algoritmica.md (13840B)
1 # Complejidad algorítmica 2 3 Algoritmos y Estructuras de Datos (CB100) - FIUBA 4 Martin Klöckner - [mklockner@fi.uba.ar](mailto:mklockner@fi.uba.ar) 5 6 Se dice que un algorítmico es mas eficiente que otro si consume menos recursos, 7 o se ejecuta mas rápido. La eficiencia se puede medir en términos espaciales 8 (cantidad de memoria estática y dinámica que utiliza al ejecutarse) o en 9 términos temporales (el tiempo que tarda en ejecutarse) en general se busca una 10 relación de compromiso que comprende ambos factores. 11 12 Cuando se hace un análisis de la complejidad temporal de un algoritmo, se hace 13 referencia al tamaño de entrada del problema, o tamaño del problema, este tamaño 14 depende de la naturaleza del problema y corresponde a aquel o aquellos elementos 15 que produzcan, al crecer, un aumento en el tiempo de ejecución. Por ejemplo, al 16 calcular el factorial de un número, el tamaño del problema es el número al cual 17 se quiere calcular el factorial, ya que cuanto mayor sea este número mayor será 18 el tiempo de ejecución del algoritmo, otro ejemplo es el caso de una búsqueda 19 binaria en la cual el tamaño del problema será el tamaño del vector a ordenar. 20 21 Se denota entonces el coste real para una operación de $n$ entradas como $T(n)$, 22 esta coste mide el número de operaciones elementales requeridas para ejecutar el 23 algoritmo que se describe, y si bien al analizar un algoritmo existe un mejor 24 caso, un caso promedio y un peor caso, en la practica se suele definir a $T(n)$ 25 en términos del peor caso, ya que determina cual sería el número de operaciones 26 elementales requeridas con la peor entrada posible. Por ejemplo en el siguiente 27 caso, la cantidad de ciclos ejecutados depende directamente de la posición del 28 dato en el vector, en el mejor caso es $1$ y es cuando el dato está en el primer 29 elemento, el caso promedio, es un promedio ponderado entre las probabilidades de 30 todas las posibles entradas, en este caso resulta $n/2$, el peor caso es cuando 31 está al final, y en ese caso la cantidad de operaciones elementales requeridas 32 es $n$, por esto ultimo la complejidad algorítmica $T(n)$ resulta $n$, 33 denotándose $T(n) = n$. 34 35 ```c++ 36 int get_pos(int* vec, int len, int data) { 37 for(int pos = 0; pos < len; ++pos) { 38 if(vec[i] == data) { 39 return pos; 40 } 41 } 42 return -1; 43 } 44 ``` 45 46 ## Operaciones elementales 47 48 Las operaciones elementales son aquellas operaciones básicas de bajo nivel que 49 un algoritmo ejecuta y que tienen un costo constante (es decir, toman el mismo 50 tiempo, independientemente del tamaño de la entrada). Se considera operaciones 51 elementales a las operaciones aritméticas básicas (`+`, `-`, `*`, etc), 52 comparaciones lógicas (`==`, `!=`, `>`, etc), transferencias de control, 53 asignaciones a variables de tipos básicos (`x = 5`, `a = b`, etc), acceso a 54 memoria (`a[i]`, `x = b`, etc). 55 56 Las operaciones elementales sirven para independizar la definición de la 57 complejidad de un algoritmo de la maquina en la cual se ejecuta, ya que la 58 diferencia será una constante relacionada a la rapidez con la cual la maquina en 59 la cual se ejecuta el algoritmo puede realizar dichas operaciones elementales. 60 61 ```c++ 62 int a; // 1 operación elemental 63 a = 5; // 1 operación elemental 64 a = a + 5; // 2 OE (acceso a memoria y suma) 65 ``` 66 67 En el ejemplo anterior la complejidad algorítmica resulta $T(n) = 4$ y es 68 constante independiente de la entrada (no tiene entrada). En el ejemplo 69 siguiente la entrada es $n$. 70 71 72 ```c++ 73 int n; // 1 operación elemental 74 std::cin >> n; // se considera 1 OE 75 76 while(n > 0) { 77 std::cout << n; // se considera 1 OE 78 n--; // 2 OE 79 } 80 ``` 81 82 El número total de operaciones elementales en el mejor de los casos es $2$ y es 83 cuando la entrada es $n <= 0$, en el peor de los caso se puede ver que el ciclo 84 `while` se ejecuta $n$ veces, resultando la complejidad algorítmica $T(n) = 2 + 85 n*3$. 86 87 En el siguiente ejemplo hay una condición y en una de las ramas un ciclo 88 `while`, ante estos casos se toma el peor caso, por lo tanto el coste total 89 resulta $T(n) = 4 + 3*n$, ya que es el coste del ciclo. 90 91 ```c++ 92 int n; // 1 operacion elemental 93 std::cin >> n; // 1 OE 94 95 if(n % 2 == 0) { // 2 OE 96 std::cout << n; // 1 OE 97 } else { 98 while(n > 0) { 99 std::cout << n; // 1 OE 100 n--; // 2 OE 101 } 102 } 103 ``` 104 105 ## Complejidad asintótica 106 107 La complejidad asintótica describe cómo crece el tiempo o espacio requerido por 108 un algoritmo cuando aumenta el tamaño de la entrada, ignorando constantes y 109 detalles menores. Esto se define debido a que en muchos casos calcular el costo 110 en operaciones elementales puede volverse tedioso, por lo que una mejor 111 aproximación es acotar apropiadamente el costo de ejecución del algoritmo. Por 112 ejemplo, se tiene el costo en función de la entrada de dos algoritmos $T_{1}(n)$ 113 y $T_{2}(n)$: 114 115 $$T_{1}(n) = 3n^2 + 5n + 6\hspace{2em} T_{2}(n) = 12n^2 + 2$$ 116 117 Se puede decir entonces, que ambos algoritmos tienen una complejidad similar en 118 términos de cotas, ya que para un número suficientemente grande de la entrada, 119 ambos algoritmos tienden a crecer de forma cuadrática. 120 121 Para acotar debidamente un algoritmo se definen 3 cotas, la cota superior, la 122 cota inferior y la cota mas ajustada o que aproxima mejor entre la cota 123 superior e inferior. 124 125 ### Cota inferior $\Omega$ 126 127 La cota inferior (Omega $\Omega$) hace referencia a una función que acota 128 inferiormente al tiempo real $T(n)$ de un algoritmo dado, es decir, indica que 129 función será siempre superada por $T(n)$ para un $n$ suficientemente grande. 130 131 ### Cota que mejor aproxima $\Theta$ 132 133 La cota que mejor aproxima (Theta $\Theta$) hace referencia a una funcion que 134 acota tanto inferiormente como superiormente al tiempo real $T(n)$ de un 135 algoritmo dado para un $n$ suficientemente grande. 136 137 ### Cota superior $\mathcal{O}$ 138 139 La cota superior hace referencia a una función que acota el crecimiento del 140 número de operaciones elementales o tiempo de ejecución de un algoritmo en el 141 **peor de los casos** para un número de entradas suficientemente grande, esto es 142 la minima función, ya que existen infinitas funciones que pueden ser mayores o 143 que pueden acotar el crecimiento de tiempo del algoritmo. 144 145 Por ejemplo para la siguiente función de tiempo real para un algoritmo dado 146 $T_{1}(n) = 3n^2 + 5n + 6$, se tiene que una cota superior es $O(n) = n^2$, ya 147 que para un número suficientemente grande el termino lineal y constante no hace 148 diferencia. 149 150 ### Orden 151 152 El orden expresa el comportamiento dominante de un algoritmo para un número de 153 entradas $n$ suficientemente grande, en la practica se suele tomar como si fuera 154 la cota superior, aunque por definición no lo es. Por definición se dice que 155 $T(n)$ es de orden $g(n)$ (o pertenece a) $\mathcal{O}(g(n))$ si y solo si existen 156 constantes positivas $c$ y $n_{0}$, tales que se verifica para todo $n > n_{0}$ 157 lo siguiente 158 159 $$0 \leq T(n) \leq c*g(n)\hspace{1em} \forall n \geq n_{0}$$ 160 161 En general en los casos en donde $T(n)$ se expresa como un polinomio, el orden 162 $\mathcal{O}$ del algoritmo, es el termino de mayor grado de $T(n)$ 163 164 Los ordenes mas comunes entre diferentes algoritmos se pueden ordenar en forma 165 creciente en cuanto a complejidad algorítmica, esto permite comparar la 166 eficiencia entre los algoritmos: 167 168 \begin{align*} 169 \mathcal{O}(1) &\subset \mathcal{O}(\log n) \subset \mathcal{O}(n) \subset \mathcal{O}(n \log n) \\ 170 &\subset \mathcal{O}(n^2) \subset \mathcal{O}(n^3) \subset \cdots \subset \mathcal{O}(2^n) \subset \mathcal{O}(n!) 171 \end{align*} 172 173  174 175 #### Propiedades del orden 176 177 A continuación se muestran propiedades de la cota superior $\mathcal{O}(f)$ 178 179 1. $f$ es $\mathcal{O}(f)$ entonces $f$ esta acotada por su orden 180 2. $\mathcal{O}(f)$ es $\mathcal{O}(g) \Rightarrow \mathcal{O}(f) \subset \mathcal{O}(g)$ y $\mathcal{O}(g) \subset \mathcal{O}(f)$ 181 3. $\mathcal{O}(f) = \mathcal{O}(g)$ $\Leftrightarrow$ $f$ es $\mathcal{O}(g)$ y $g$ es 182 $\mathcal{O}(f)$ 183 4. Si $f$ es $\mathcal{O}(g)$ y $g$ es $\mathcal{O}(h)$ $\Rightarrow$ $f$ es $\mathcal{O}(h)$ 184 5. Si $f$ es $\mathcal{O}(g)$ y $f$ es $\mathcal{O}(h)$ $\Rightarrow$ $f$ es $\mathcal{O}(min(g, h))$ 185 6. Si $f_{1}$ es $\mathcal{O}(g)$ y $f_{2}$ es $\mathcal{O}(h)$ 186 $\Rightarrow$ $f_{1} + f_{2}$ es $\mathcal{O}(max(g, h))$ 187 7. Si $f_{1}$ es $\mathcal{O}(g)$ y $f_{2}$ es $\mathcal{O}(h)$ 188 $\Rightarrow$ $f_{1} * f_{2}$ es $\mathcal{O}(g*h)$ 189 190 ## Algoritmos recursivos 191 192 Dado una función de costo real con recurrencia (es decir que depende de la misma 193 función para términos anteriors) por ejemplo $T(n) = T(n-1) + 1$ se puede hallar 194 la complejidad mediante dos métodos gentales, el primero el método de expansion, 195 en el cual se trata de un método iterativo evaluando como depende la función con 196 las iteraciones, y en el segundo método de resolución, aplicando el teorema 197 maestro, el cual es una formula general. 198 199 ### Método de expansión 200 201 El método de expansion se trata de ir hallando los términos recursivos mediante 202 la formula del coste real $T(n)$ y reemplazando en si misma, es un proceso 203 iterativo. Por ejemplo se tiene una expresión de $T(n)$: 204 205 $$\begin{align} 206 T(n) = 2T\left(\frac{n}{2}\right) + \mathcal{O}(1) 207 \end{align}$$ 208 209 De la ecuación anterior se puede obtener $T\left(\frac{n}{2}\right)$ 210 reemplazando $n$ con $n/2$: 211 212 $$\begin{align} 213 T\left(\frac{n}{2}\right) = 2T\left(\frac{n}{4}\right) + 214 \mathcal{O}(1) 215 \end{align}$$ 216 217 Por lo tanto reemplazando $(2)$ en $(1)$ resulta: 218 219 $$\begin{align} 220 T(n) &= 2\cdotp \left[2T\left(\frac{n}{4}\right) + \mathcal{O}(1)\right] + \mathcal{O}(1) \nonumber\\ 221 &= 4T\left(\frac{n}{4}\right) + 2\mathcal{O}(1) + \mathcal{O}(1) \nonumber\\ 222 &= 4T\left(\frac{n}{4}\right) + 3\mathcal{O}(1) 223 \end{align}$$ 224 225 Pero de $(1)$ también se puede obtener $T\left(\frac{n}{4}\right)$: 226 227 $$\begin{align} 228 T\left(\frac{n}{4}\right) = 2T\left(\frac{n}{8}\right) + \mathcal{O}(1) 229 \end{align}$$ 230 231 Y reemplazando $(4)$ en la ecuación $(3)$: 232 233 $$\begin{align} 234 T(n) &= 4\cdotp \left[2T\left(\frac{n}{8}\right) + \mathcal{O}(1)\right] + 2\mathcal{O}(1) + \mathcal{O}(1) \nonumber\\ 235 &= 8T\left(\frac{n}{8}\right) + 4\mathcal{O}(1) + 2\mathcal{O}(1) + \mathcal{O}(1)\nonumber\\ 236 &= 8T\left(\frac{n}{8}\right) + 7\mathcal{O}(1) 237 \end{align}$$ 238 239 Se puede ver que luego de realizar $k$ veces el mismo procedimiento resulta: 240 241 $$\begin{align} 242 T(n) = 2^{k}\cdotp T\left(\frac{n}{2^k}\right) + (2^{k}-1)\cdotp \mathcal{O}(1) 243 \end{align}$$ 244 245 Pero las iteraciones se terminan cuando el numero de entradas es $1$, entonces 246 en $(6)$: 247 248 $$\begin{align} 249 \frac{n}{2^k} = 1 \Rightarrow n = 2^k \Rightarrow \boxed{log_{2}(n) = k} 250 \end{align}$$ 251 252 Con $(7)$ en $(6)$ resulta 253 254 $$T(n) = n\cdotp T(1) + (n - 1)\cdotp \mathcal{O}(1)$$ 255 256 De la expresión anterior, suponiendo $T(1) = \mathcal{O}(1)$ y aproximando, resulta 257 258 $$\begin{align*} 259 T(n) &= n\cdotp \mathcal{O}(1) + (n - 1)\cdotp \mathcal{O}(1) \\ 260 &= \mathcal{O}(n) + \mathcal{O}(n) \Rightarrow \boxed{T(n) = \mathcal{O}(n)} 261 \end{align*}$$ 262 263 Es decir la complejidad resulta $\mathcal{O}(n)$. El mismo problema se podría haber 264 265 ### Teorema maestro 266 267 El teorema maestro se trata de una solución general para hallar la complejidad 268 dependiendo de como evolucionan los términos, en general se tienen dos forums, 269 la forma lineal, o por sustracción y la forma por division, las diferencias o 270 cuando aplicar cada una se muestran a continuación. 271 272 #### Reducción por sustracción 273 274 Dado una función de costo real $T(n)$ de la forma 275 276 $$T(n) = a\cdotp T(n-b)+\mathcal{O}(n^{k})$$ 277 278 Entonces la complejidad algorítmica, o la solución de la ecuación de recurrencia 279 $T(n)$ resulta: 280 281 $$T(n) = 282 \begin{cases} 283 \hspace{0.75em} \mathcal{O}(n^{(n/b)}\cdotp n^{k}) & \text{si } a > 1 \\ 284 \hspace{0.75em} \mathcal{O}(n^{k+1}) & \text{si } a = 1 \\ 285 \hspace{0.75em} \mathcal{O}(n^k) & \text{si } a < 1 286 \end{cases}$$ 287 288 #### Reducción por division 289 290 Dado una función de costo real $T(n)$ de la forma 291 292 $$T(n) = 293 \begin{cases} 294 \hspace{0.75em} c\cdotp n^{k} & \text{si } 1 \leq n < b \\ 295 \hspace{0.75em} a\cdotp T\left(\frac{n}{b}\right) + c\cdotp n^{k} & \text{si } n \geq b 296 \end{cases}$$ 297 298 Entonces la complejidad algorítmica, o la solución de la ecuación de recurrencia 299 $T(n)$ resulta: 300 301 $$T(n) = 302 \begin{cases} 303 \hspace{0.75em} \mathcal{O}(n^k) & \text{si } a < b^{k} \\ 304 \hspace{0.75em} \mathcal{O}(n^{k}\cdotp log(n)) & \text{si } a = b^{k} \\ 305 \hspace{0.75em} \mathcal{O}(n^{log_{b}(a)}) & \text{si } a > b^{k} 306 \end{cases}$$ 307 308 \vspace{-0.5em} 309 310 ## Complejidad amortizada 311 312 Cuando se mide la complejidad de un algoritmo mediante el peor caso 313 ($\mathcal{O}$) puede haber casos en los que no sea representativo, es decir, el 314 peor caso dista mucho de la media de ejecución del algoritmo, por lo que se usa 315 la **complejidad amortizada** la cual es una especie de promedio por operación 316 de un algoritmo en el peor de los casos a lo largo de una serie de operaciones. 317 318 Por definición la complejidad amortizada es una técnica de análisis que se 319 utiliza para determinar el tiempo promedio por operación de un algoritmo en el 320 peor de los casos a lo largo de una secuencia de operaciones. En lugar de 321 analizar el tiempo proporciona una estimación del costo total de una serie de 322 operaciones, dividiendo este costo total por el numero de operaciones. Este tipo 323 de análisis es especialmente útil cuando ciertas operaciones pueden ser muy 324 costosas individualmente, pero esas operaciones costosas ocurren con poca 325 frecuencia. 326 327 Existen varios métodos para realizar un análisis amortizado, pero el mas común 328 es calcular el costo total de una secuencia de operaciones y dividir por el 329 numero total de operaciones: 330 331 $$\boxed{\text{Costo amortizado } = \frac{T(n)}{n}}$$
