Análisis de algoritmos Mtr. Ing. Nancy López Análisis de algoritmos • Un algoritmo se utiliza para resolver un problema. • Normalmente se presentan infinidad de algoritmos. • Para que el algoritmo se considere correcto debe funcionar correctamente en todos los casos. • A medida que aumentan las dificultades la eficiencia del algoritmo cambia. Análisis de algoritmos Variables que intervienen: • Tiempo: necesitamos resolver un problema en un tiempo limitado. • Cantidad de operaciones elementales que debe realizar un algoritmo. • Tamaño de los elementos que se pasan como parámetros. Análisis de algoritmos • Para demostrar que un algoritmo es incorrecto únicamente basta con mostrar que UN caso no funciona. • Para demostrar la validez del algoritmo es necesario definir su Dominio (ejemplo División). • Todo dispositivo de cálculo tiene un límite. (parámetros son demasiados grandes o, nos quedamos sin espacio) • Un problema puede tener varios algoritmos que sean los adecuados. • Si tenemos que resolver 1 o 2 casos se considera que cualquiera de ellos sería eficiente. • Preferentemente nos inclinaríamos por el que sea más fácil de programar. Análisis de algoritmos Qué algoritmos elegir para resolver un problema? • Que sean fáciles de entender, codificar y depurar • Que usen eficientemente los recursos del sistema: que usen poca memoria y que se ejecuten con la mayor rapidez posible • Ambos factores en general se contraponen… • Nos concentraremos ahora en el segundo factor y en particular en el análisis del tiempo de ejecución Análisis de algoritmos • Para seleccionar un algoritmo podemos ver 2 diferentes formas. • La empírica: consiste en ir probando las diferentes técnicas, con base en la experiencia. • La teórica: consiste en ver la cantidad de casos a considerar, teniendo en cuenta el tiempo a computar los casos y la memoria de almacenamiento y dado esto se verá su tiempo de ejecución. La eficiencia estará directamente vinculada al tiempo de ejecución. Análisis de algoritmos • También existe un enfoque híbrido. Siendo éste una mezcla de los 2, en estos casos la experiencia previa es de suma importancia. • Otra cosa a tener en cuenta, será el lenguaje del programador, y el compilador con el cual contaremos. Serán variables que pueden incidir directamente en el tiempo de eficiencia más allá de la máquina en sí. Análisis de algoritmos • Sean "g(n)" diferentes funciones que determinan el uso de recursos. Habrá funciones "g" de todos los colores. Identificaremos "familias" de funciones, usando como criterio de agrupación su comportamiento asintótico. Análisis de algoritmos • Para cada algoritmo las entradas las consideraremos de un tamaño N. • Por ejemplo para un vector se podría decir que es su tamaño, para un Grafo los nodos y las líneas, y si trabajásemos con una base de datos, la cantidad de claves primarias por las que tiene que pasar al realizar una consulta. Análisis de algoritmos • Cada algoritmo cuenta con un T(N) que es el tiempo que lleva el mismo. • Por ejemplo si tuviésemos: S1; for (int i= 0; i < N; i++) S2; Siendo S1= 1er serie de sentencias y S2 = 2da sentencias. El cálculo sería el siguiente: T(N)= t1 + t2*N siendo t1 el tiempo que lleve ejecutar la serie "S1" de sentencias, y t2 el que lleve la serie "S2". Análisis de algoritmos • Prácticamente todos los programas reales incluyen alguna sentencia condicional, haciendo que las sentencias efectivamente ejecutadas dependan de los datos concretos que se le presenten. Esto hace que más que un valor T(N) debamos hablar de un rango de valores • Tmin(N) <= T(N) <= Tmax(N) • Los extremos son habitualmente conocidos como "caso peor" y "caso mejor". Entre ambos se hallará algún "caso promedio" o más frecuente. Análisis de algoritmos • Por una parte necesitamos analizar la potencia de los algoritmos independientemente de la potencia de la máquina que los ejecute e incluso de la habilidad del programador que los codifique. • Por otra, este análisis nos interesa especialmente cuando el algoritmo se aplica a problemas grandes. Casi siempre los problemas pequeños se pueden resolver de cualquier forma, apareciendo las limitaciones al atacar problemas grandes. Análisis de algoritmos • Cualquier técnica de ingeniería, si funciona, acaba aplicándose al problema más grande que sea posible: las tecnologías de éxito, antes o después, acaban llevándose al límite de sus posibilidades. • Las consideraciones anteriores nos llevan a estudiar el comportamiento de un algoritmo cuando se fuerza el tamaño del problema al que se aplica. Matemáticamente hablando, cuando N tiende a infinito. Es decir, su comportamiento asintótico. Análisis de algoritmos • Si podemos garantizar que un programa sólo va a trabajar sobre datos pequeños (valores bajos de N), el orden de complejidad del algoritmo que usemos suele ser irrelevante. Análisis de algoritmos • A un conjunto de funciones que comparten un mismo comportamiento asintótico le denominaremos un orden de complejidad. Habitualmente estos conjuntos se denominan O, existiendo una infinidad de ellos. Análisis de algoritmos • Para cada uno de estos conjuntos se suele identificar un miembro f(n) que se utiliza como representante de la clase, hablándose del conjunto de funciones "g" que son del orden de "f(n)", denotándose como g IN O(f(n)) Análisis de algoritmos • Con frecuencia nos encontraremos con que no es necesario conocer el comportamiento exacto, sino que basta conocer una cota superior, es decir, alguna función que se comporte "aún peor". • O(f(n)) está formado por aquellas funciones g(n) que crecen a un ritmo menor o igual que el de f(n). Análisis de algoritmos • De las funciones "g" que forman este conjunto O(f(n)) se dice que "están dominadas asintóticamente" por "f", en el sentido de que para N suficientemente grande, y salvo una constante multiplicativa "k", f(n) es una cota superior de g(n). Análisis de algoritmos • Se dice que O(f(n)) define un "orden de complejidad". Tomamos el f(n) más sencillo: O(1) orden constante O(log n) orden logarítmico O(n) orden lineal O(n log n) orden lineal logarítmico O(n2) orden cuadrático O(na) orden polinomial (a > 2) O(an) orden exponencial (a > 2) O(n!) orden factorial Análisis de algoritmos • Para compararlos entre sí, supongamos que todos ellos requieren 1 hora de ordenador para resolver un problema de tamaño N=100. • ¿Qué ocurre si disponemos del doble de tiempo? Nótese que esto es lo mismo que disponer del mismo tiempo en un ordenador el doble de potente, y que el ritmo actual de progreso del hardware es exactamente ese: Análisis de algoritmos Ejemplos: Bubble Sort O(n²) - Árbol Binario O(n log n) Análisis de algoritmos Técnicas de diseño Fuerza Bruta • Se debe evitar. • No es un esquema algorítmico sino un calificativo. • Consiste en tomar la primer solución en la que pensemos, sin reflexionarla. • Puede servir como una primera aproximación a la solución final. Técnicas de diseño Fuerza Bruta. Ejemplos • Algunos algoritmos de búsqueda de un elemento en un vector. Uno de ellos realiza una búsqueda secuencial con complejidad lineal sobre el tamaño del vector y puede usarse con cualquier vector. Otro algoritmo realiza un búsqueda dicotómica o binaria, con complejidad logarítmica, y solo se puede usar cuando el vector esté ordenado. Técnicas de diseño • La primera solución es la más tentadora, lo ideal sería comprobar si el algoritmo está ordenado y aplicar en consecuencia. Técnicas de diseño Divide y vencerás • Consiste en descomponer un problema en sub-problemas, resolver independientemente los sub-problemas para luego combinar sus soluciones y obtener la solución del problema original. Esta técnica se puede aplicar con éxito a problemas como la multiplicación de matrices, la ordenación de vectores, la búsqueda en estructuras ordenadas, etc. Divide y vencerás. Ejemplo. Búsqueda de una palabra en un diccionario Como ejemplo sencillo de aplicación de esta estrategia puede considerarse la búsqueda de una palabra en un diccionario de acuerdo con el siguiente criterio. Se abre el diccionario por la página central (quedando dividido en dos mitades) y se comprueba si la palabra aparece allí o si es léxicográficamente anterior o posterior. Si no ha encontrado y es anterior se procede a buscarla en la primera mitad, si es posterior, se buscará en la segunda mitad. El procedimiento se repite sucesivamente hasta encontrar la palabra o decidir que no aparece. Técnicas de diseño Método voraz • Este método trata de producir el mejor resultado a partir de un conjunto de opciones candidatas. • Para ello, se va procediendo paso a paso realizándose la mejor elección (usando una función objetivo que respeta un conjunto de restricciones ) de entre las posibles. • Puede emplearse en problemas de optimización, como en la búsqueda de caminos mínimos sobre grafos, la planificación en el orden de la ejecución de algunos programas en un computador, etc. Método Voraz. Ejemplo. Dar un cambio utilizando el menor número de monedas Considérese ahora el problema de la devolución del cambio al realizar una compra (por ejemplo, en un cajero automático). Suponiendo que se disponga de cantidad suficiente de ciertos tipos diferentes de monedas de curso legal, se trata de dar como cambio la menor cantidad posible usando estos tipos de monedas. La estrategia voraz aplicada comienza devolviendo, cuando se pueda, la moneda de mayor valor (es decir, mientras el valor de dicha moneda sea mayor o igual al cambio que resta por dar), continua aplicándose el mismo criterio para la segunda moneda más valiosa, y así sucesivamente. El proceso finaliza cuando se ha devuelto todo el cambio. Técnicas de diseño • Si un programa se va a ejecutar muy pocas veces, los costes de codificación y depuración son los que más importan, relegando la complejidad a un papel secundario. • Si a un programa se le prevé larga vida, hay que pensar que le tocará mantenerlo a otra persona y, por tanto, conviene tener en cuenta su legibilidad, incluso a costa de la complejidad de los algoritmos empleados. Técnicas de diseño Consideraciones: • Corrección, el algoritmo debe funcionar. Nunca se debe olvidar que la característica más simple e importante de un algoritmo es que funcione. Pude aparecer obvio, pero resulta difícil de asegurar en algoritmos complejos. • Eficiencia, el algoritmo no debe desaprovechar recursos. La eficiencia de un algoritmo se mide por los recursos que este consume. En particular, se habla de la memoria y del tiempo de ejecución. A pesar de que con la reducción de los costes del hardware es posible diseñar computadores más rápidos y con más memoria, no hay que desperdiciar estos recursos y tratar de desarrollar algoritmos más eficientes. Técnicas de diseño • Claridad, el algoritmo debe estar bien documentado. La documentación ayuda a comprender el funcionamiento de los algoritmos. Ciertos detalles o algunas partes especiales de los mismos pueden olvidarse fácilmente o quedar a oscuras si no están adecuadamente comentadas. Resumiendo, lo ideal es que nuestro algoritmo resulte correcto, eficiente, claro, fiable y fácil de mantener. Técnicas de diseño Conclusiones ● Antes de realizar un programa conviene elegir un buen algoritmo, donde por bueno entendemos que utilice pocos recursos, siendo usualmente lo más importante el tiempo que lleve ejecutarse y la cantidad de espacio en memoria que requiera. Es engañoso pensar que todos los algoritmos son "más o menos iguales" y confiar en nuestra habilidad como programadores para convertir un mal algoritmo en un producto eficaz. Es asimismo engañoso confiar en la creciente potencia de las máquinas y el abaratamiento de las mismas como remedio de todos los problemas que puedan aparecer. Algoritmos de ordenamiento Definición Entrada: Una secuencia de n números <a1, a2, …, an>, usualmente en la forma de un arreglo de n elementos. Salida: Una permutación <a’1, a’2, …, a’n> de la secuencia de entrada tal que a’1 ≤ a’2 ≤ … ≤ a’n. Algoritmos de ordenamiento • Los algoritmos de ordenación se clasifican de acuerdo con la cantidad de trabajo necesaria para ordenar una secuencia de n elementos: ¿Cuántas comparaciones de elementos y cuántos movimientos de elementos de un lugar a otro son necesarios? Algoritmos de ordenamiento Ordenamiento por inserción • Este es uno de los métodos más sencillos. Consta de tomar uno por uno los elementos de un arreglo y correrlo hacia su posición con respecto a los anteriormente ordenados. Así empieza con el segundo elemento y lo ordena con respecto al primero. Luego sigue con el tercero y lo coloca en su posición ordenada con respecto a los dos anteriores, así sucesivamente hasta recorrer todas las posiciones del arreglo. insercionDirecta(A[],n) { entero i,j,v para i=0; i<n; i++ { v=A[i] j=i-1 mientras j>=0 y A[j]>v { A[j+1]=A[j] j— } A[j+1] = v } Algoritmos de ordenamiento Análisis del algoritmo: • Número de comparaciones: n(n-1)/2 lo que implica un T(n) = O(n2). • La ordenación por inserción utiliza aproximadamente n2/4 comparaciones y n2/8 intercambios en el caso medio y dos veces más en el peor de los casos. • La ordenación por inserción es lineal para los archivos casi ordenados. Algoritmos de ordenamiento Ordenamiento por selección • El método de ordenamiento por selección consiste en encontrar el menor de todos los elementos del arreglo e intercambiarlo con el que está en la primera posición. Luego el segundo más pequeño, y así sucesivamente hasta ordenar todo el arreglo. Algoritmos de ordenamiento seleccionSort( A[], n) { enteros min, i, j, aux; para (i=0; i<n-1; i++) { min=i; para (j=i+1; j<n; j++) { Si ( A[min] > A[j] ) min=j; } aux=A[min]; A[min]=A[i]; A[i]=aux; } } Algoritmos de ordenamiento Análisis del algoritmo: • La ordenación por selección utiliza aproximadamente n2/2 comparaciones y n intercambios, por lo cual T(n) = O(n2). Algoritmos de ordenamiento Ordenamiento por burbuja (Bubble Sort) • El algoritmo bubblesort, también conocido como ordenamiento burbuja, funciona de la siguiente manera: • Se recorre el arreglo intercambiando los elementos adyacentes que estén desordenados. Se recorre el arreglo tantas veces hasta que ya no haya cambios que realizar. • Prácticamente lo que hace es tomar el elemento mayor y lo va corriendo de posición en posición hasta ponerlo en su lugar. Algoritmos de ordenamiento Burbuja Para (i=0; i<TAM; i++) for(j=0; j<TAM-1; j++) if( clave j < clave j+1 ) //o clave j > clave j+1 { //intercambio } Algoritmos de ordenamiento Análisis del algoritmo: • La ordenación de burbuja tanto en el caso medio como en el peor de los casos utiliza aproximadamente n2/2 comparaciones y n2/2 intercambios, por lo cual • T(n) = O(n2). Algoritmos de ordenamiento • Ordenamiento Rápido (Quicksort) • Vimos que en un algoritmo de ordenamiento por intercambio, son necesarios intercambios de elementos en sublistas hasta que no son posibles más. En el burbujeo, son comparados ítems correlativos en cada paso de la lista. Por lo tanto, para ubicar un ítem en su correcta posición, pueden ser necesarios varios intercambios. Algoritmos de ordenamiento • La idea básica del algoritmo es elegir un elemento llamado pivote, y ejecutar una secuencia de intercambios tal que todos los elementos menores que el pivote queden a la izquierda y todos los mayores a la derecha. • Lo único que requiere este proceso es que todos los elementos a la izquierda sean menores que el pivote y que todos los de la derecha sean mayores luego de cada paso, no importando el orden entre ellos, siendo precisamente esta flexibilidad la que hace eficiente al proceso. • Hacemos dos búsquedas, una desde la izquierda y otra desde la derecha, comparando el pivote con los elementos que vamos recorriendo, buscando los menores o iguales y los mayores respectivamente. Algoritmos de ordenamiento QuickSort (A[], izq, der) Si i <= j Enteros i, j, x, aux { i=izq aux=A[i] j=der A[i]=A[j] x= A[(izq+der)/2] A[j]=aux hacer i++ { j-Mientras (A[i]<x) && (j <= } der) } mientras i <= j i++ Si izq < j Mientras (x < A[j]) && (j> quickSort(A, izq, j) izq) si i<der j-quickSort (A, i, der) Algoritmos de ordenamiento Análisis del algoritmo: • Depende de si la partición es o no balanceada, lo que a su vez depende de cómo se elige los pivotes. • En promedio para todos los elementos de entrada de tamaño n, el método hace O(n log n) comparaciones, el cual es relativamente eficiente. Algoritmos de ordenamiento • Ordenamiento Heap Sort • El ordenamiento por montículos (Heap sort) es un algoritmo de ordenación no recursivo, no estable, con complejidad computacional O(n log n). • Este algoritmo consiste en almacenar todos los elementos del vector a ordenar en un montículo (heap), y luego extraer el nodo que queda como nodo raíz del montículo (cima) en sucesivas iteraciones obteniendo el conjunto ordenado. Basa su funcionamiento en una propiedad de los montículos, por la cual, la cima contiene siempre el menor elemento (o el mayor, según se haya definido el montículo) de todos los almacenados en él. • • • • Procedimiento Heap Sort Heap Sort consiste esencialmente en: Convertir el arreglo en un heap. Construir un arreglo ordenado de atrás hacia adelante (mayor a menor) repitiendo los siguientes pasos: – Sacar el valor máximo en el heap (el de la posición 1) – Poner ese valor en el arreglo ordenado. – Reconstruir el heap con un elemento menos. • Utilizar el mismo arreglo para el heap y el arreglo ordenado. Algoritmos de ordenamiento heapSort (A[],n) para k=n ; k>0 ; k-{ para i=1; i<= k; i++ { item=A[i] j=i/2 mientras j > 0 && A[j] < ítem { A[i]=A[j] i=j j=j/2 } A[i]= item } temp=A[1] A[1]=A[k] A[k]=temp } El padre de i es: i div 2. Los hijos de i son: 2 * i y 2 * i + 1. Comparación Algoritmos de búsqueda Búsqueda secuencial • Busca un elemento de una lista utilizando un valor destino llamado clave. También llamada búsqueda lineal, se exploran los elementos en secuencia, uno después del otro. • El método de búsqueda lineal funcionará bien con arreglos pequeños o no ordenados. La complejidad es pobre: O(n). Algoritmos de búsqueda Búsqueda dicotómica o binaria • Sólo se puede utilizar en arreglos ya ordenados. Usa el método de divide y vencerás. • Procedimiento: el procedimiento es el ya visto en la búsqueda de una palabra en un diccionario. • El orden de complejidad es O(log n) Algoritmos de búsqueda • Búsqueda binaria int busquedaBinaria(vector, TAM, dato) centro,inf=0,sup=TAM-1 mientras(inf<=sup){ centro=(sup+inf)/2 si(vector[centro]==dato) devolver centro sino si(dato < vector [centro] ) sup=centro-1 sino{ inf=centro+1 devolver -1