CB100

Notas, resueltos y tps de la materia Algoritmos y Estructuras de Datos
Index Commits Files Refs README
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 ![Complejidades algorítmicas](./func_cmp.png)
 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}}$$