La descarga está en progreso. Por favor, espere

La descarga está en progreso. Por favor, espere

Complejidad de Algoritmos

Presentaciones similares


Presentación del tema: "Complejidad de Algoritmos"— Transcripción de la presentación:

1 Complejidad de Algoritmos
¿Cuándo proporciona un algoritmo una solución SATISFACTORIA a un problema? Primero, debe producir siempre la respuesta correcta. Segundo, deberá ser eficiente. ¿Cómo se puede analizar la eficiencia de los algoritmos? Una medida de eficiencia es el tiempo que requiere un ordenador para resolver un problema utilizando un algoritmo para valores de entrada de un tamaño específico. Una segunda medida es la cantidad de memoria que se necesita de nuevo para valores de entrada de un tamaño dado. Una tercera medida sería estabilidad: un ordenamiento estable mantiene el orden relativo que tenían originalmente los elementos con claves iguales. Un análisis del tiempo requerido para resolver un problema de un tamaño particular está relacionado con la complejidad en tiempo del algoritmo y un análisis de la memoria de ordenador requerida involucra la complejidad en espacio del algoritmo. Obviamente, es importante saber si un algoritmo producirá su respuesta en un milisegundo, en un minuto o en un millón de años y de manera similar debemos tener suficiente memoria disponible para poder resolver el problema.

2 Las consideraciones sobre complejidad en espacio están ligadas a las estructuras de datos usadas en la implementación del algoritmo. La complejidad en el tiempo de un algoritmo se puede expresar en términos del número de operaciones que realiza el algoritmo cuando los datos de entrada tienen un tamaño particular. (comparación de enteros, sumas, multiplicaciones, etc.) La complejidad se describe en términos del número de operaciones requeridas en lugar del tiempo de cálculo real, debido a que distintos ordenadores necesitan tiempos diferentes para realizar las mismas operaciones básicas. Cada algoritmo se comporta de modo diferente de acuerdo a cómo se le entregue la información; por eso es conveniente estudiar su comportamiento en casos extremos, como cuando los datos están prácticamente ordenados o muy desordenados. Complejidad del peor caso.- Por comportamiento de un algoritmo en el peor caso entendemos el mayor número de operaciones que hace falta para resolver el problema dad utilizando el algoritmo para unos datos de entrada de un determinado tamaño. Los análisis del peor caso nos dicen cuántas operaciones tienen que realizar los algoritmos para garantizar que producirán una solución. Complejidad del caso promedio.- En este tipo de análisis de complejidad se busca el número promedio de operaciones realizadas para solucionar un problema considerando todas las posibles entradas de un tamaño determinado. El análisis de la complejidad del caso promedio es generalmente mucho más complicado que el análisis del peor caso.

3 La complejidad del algoritmo se denota según la notación Big-O.
Las expresiones Big-O no tienen constantes o términos de orden bajo. Esto se debe a que cuando N es muy grande, las constantes y los términos mas bajos no existen (un método constante será más rápido que uno lineal y este será más rápido que uno cuadrático). Por ejemplo, O(n) significa que el algoritmo tiene una complejidad lineal. En otras palabras, toma 10 veces más tiempo en operar un set de 100 datos que en hacerlo con un set de 10 items. Si la complejidad fuera O(n2) entonces tomaría 100 veces más tiempo en operar 100 items que en hacerlo con 10. Complejidad Terminología O(1) Complejidad constante O(log n) Complejidad logarítmica O(n) Complejidad lineal O(n log n) Complejidad n log n O(n^b) Complejidad polinómica O(b^n) Complejidad exponencial O(n!) Complejidad factorial

4 Problemas Tratables, Intratables y NP-completos
Clase P.- Los algoritmos de complejidad polinómica se dice que son tratables en el sentido de que suelen ser abordables en la práctica. Los problemas para los que se conocen algoritmos con esta complejidad se dice que forman la clase P. Aquellos problemas para los que la mejor solución que se conoce es de complejidad superior a la polinómica, se dice que son problemas intratables. Clase NP.- Algunos de estos problemas intratables pueden caracterizarse por el curioso hecho de que puede aplicarse un algoritmo polinómico para comprobar si una posible solución es válida o no. Esta característica lleva a un método de resolución no determinista consistente en aplicar heurísticos para obtener soluciones hipotéticas que se van desestimando (o aceptando) a ritmo polinómico. Los problemas de esta clase se denominan NP (la N de no-deterministas y la P de polinómicos). Clase NP-completos.- Se conoce una amplia variedad de problemas de tipo NP, de los cuales destacan algunos de ellos de extrema complejidad. Gráficamente podemos decir que algunos problemas se hayan en la "frontera externa" de la clase NP. Son problemas NP, y son los peores problemas posibles de clase NP. Estos problemas se caracterizan por ser todos "iguales" en el sentido de que si se descubriera una solución P para alguno de ellos, esta solución sería fácilmente aplicable a todos ellos. Actualmente hay un premio de prestigio equivalente al Nobel reservado para el que descubra semejante solución.

5 Como Determinar las Complejidades
¿Cómo determinar el tiempo de ejecución de un código? Depende del tipo de instrucciones utilizadas. 1. Secuencia de instrucciones instrucción 1; instrucción 2; ... instrucción k; (Este código es una secuencia de exactamente k instrucciones) El tiempo total es la suma de los tiempos de cada instrucción: Tiempo total = tiempo (instrucción 1) + tiempo (instrucción 2) tiempo (instrucción k) Si cada instrucción es "simple" (solo involucra operaciones básicas) entonces el tiempo de cada instrucción es constante y el tiempo total también es constante: O(1). En los siguientes ejemplos se asume que las instrucciones son simples, a menos que se exprese lo contrario.

6 2. Instrucciones condicionales (if-then-else)
if (condición) { secuencia de instrucciones 1 } else { secuencia de instrucciones 2 Aquí, se ejecutara la secuencia 1 o la secuencia 2. Entonces, el peor caso es el mas lento de los dos: max(tiempo(secuencia 1), tiempo(secuencia 2)). Por ejemplo, si la secuencia 1 es O(N) y la secuencia 2 es O(1) el peor caso para todo el código condicional debe ser O(N). 3. Ciclos for (i = 0; i < N; i++) { secuencia de instrucciones El ciclo se ejecuta N veces, entonces la secuencia de instrucciones también se ejecuta N veces. Como asumimos que las instrucciones son O(1), El tiempo total para el ciclo es N * O(1), lo cual seria O(N).

7 4. Ciclos anidados for (i = 0; i < N; i++) { for (j = 0; j < M; j++) { secuencia de instrucciones } El ciclo exterior se ejecuta N veces. Por cada una de esas ejecuciones, el ciclo interno se ejecuta M veces. Como resultado, las instrucciones en el ciclo interno se ejecutan un total de N * M veces. En este caso, la complejidad es de O(N * M). En el común de los casos, donde la condición de fin del ciclo interior es que j < N, igual al ciclo exterior, la complejidad total para los dos ciclos es de O(N2).

8 Ordenamiento por Selección
Este algoritmo también es sencillo. Consiste en lo siguiente: Buscas el elemento más pequeño de la lista. Lo intercambias con el elemento ubicado en la primera posición de la lista. Buscas el segundo elemento más pequeño de la lista. Lo intercambias con el elemento que ocupa la segunda posición en la lista. Repites este proceso hasta que hayas ordenado toda la lista. De esta manera se puede escribir el siguiente seudocódigo para ordenar una lista de n elementos indexados desde el 1: iterar i desde 1 hasta n-1 minimo = i; iterar j desde i+1 hasta n si lista[j] < lista[minimo] entonces minimo = j intercambiar(lista[i], lista[minimo])

9 Implementación en C: int minimo=0; for(i=0 ; i<n-1 ; i++) {
minimo=i; for(j=i+1 ; j<n ; j++) { if (x[minimo] > x[j]) minimo=j; } temp=x[minimo]; x[minimo]=x[i]; x[i]=temp;

10 Análisis del Algoritmo
Tiempo de Ejecución: El ciclo externo se ejecuta n veces para una lista de n elementos. Cada búsqueda requiere comparar todos los elementos no clasificados. Por lo tanto la complejidad es O(n2). Este algoritmo presenta un comportamiento constante independiente del orden de los datos. Luego la complejidad promedio es también O(n2). Ventajas: Fácil implementación. No requiere memoria adicional. Realiza pocos intercambios. Rendimiento constante: poca diferencia entre el peor y el mejor caso. Desventajas: Lento. Realiza numerosas comparaciones.

11 Ordenamiento por Inserción
Para ordenar una lista con n elementos, la ordenación por inserción comienza con el segundo elemento. Se compara este segundo elemento con el primero y se coloca antes del primero si no es mayor que el primer elemento y tras el primer elemento si es mayor que éste. En este punto, los dos primeros elementos están en el orden correcto. El tercer elemento se compara con el primero y si es mayor que él se compara con el segundo. Se coloca en la posición correcta entre los tres primeros elementos. Para simular esto en un programa necesitamos tener en cuenta algo: no podemos desplazar los elementos así como así o se perderá un elemento. Lo que hacemos es guardar una copia del elemento actual y desplazar todos los elementos mayores hacia la derecha. Luego copiamos el elemento guardado en la posición del último elemento que se desplazó. Pseudo-código for (i=1; i<TAM; i++) temp = lista[i]; j = i - 1; while ( (lista[j] > temp) && (j >= 0) ) lista[j+1] = lista[j]; j--; lista[j+1] = temp;

12 Análisis del Algoritmo
Tiempo de Ejecución: Para una lista de n elementos el ciclo externo se ejecuta n-1 veces. El ciclo interno se ejecuta como máximo una vez en la primera iteración, 2 veces en la segunda, 3 veces en la tercera, etc. Esto produce una complejidad O(n2). Ventajas: Fácil implementación. Requerimientos mínimos de memoria. Desventajas: Lento. Realiza numerosas comparaciones. Este también es un algoritmo lento, pero puede ser de utilidad para listas que están ordenadas o semiordenadas, porque en ese caso realiza muy pocos desplazamientos.

13 Merge sort El merge sort divide la lista a ser ordenada en dos mitades iguales y las pone en arrays separadas. Cada array es ordenado recursivamente, y luego se juntan en el array final. Este algoritmo tiene un comportamiento de O(n log n). - Primera parte: ¿Cómo intercalar dos listas ordenadas en una sola lista ordenada de forma eficiente? - Segunda parte: divide y vencerás. Se separa la lista original en dos trozos mismo tamaño (salvo listas de longitud impar) que se ordenan recursivamente, una vez ordenados se fusionan obteniendo una lista ordenada. Como todo basado en divide y vencerás tiene un caso base y un caso recursivo. * Caso base: cuando la lista tiene 1 ó 0 elementos (0 se da si se trata de una lista vacía). Se devuelve la lista tal cual está. * Caso recursivo: cuando la longitud de la lista es de al menos 2 elementos. Se divide la lista en dos trozos del mismo tamaño que se ordenan recursivamente. Una vez ordenado cada trozo, se fusionan y se devuelve la lista resultante.

14 El esquema es el siguiente:
Ordenar(lista L) inicio si tamaño de L es 1 o 0 entonces devolver L si tamaño de L es >= 2 entonces separar L en dos trozos: L1 y L2. L1 = Ordenar(L1) L2 = Ordenar(L2) L = Fusionar(L1, L2) fin El algoritmo funciona y termina porque llega un momento en el que se obtienen listas de 2 ó 3 elementos que se dividen en dos listas de un elemento (1+1=2) y endos listas de uno y dos elementos (1+2=3, la lista de 2 elementos se volverá adividir), respectivamente. Por tanto se vuelve siempre de la recursión con listas ordenadas (pues tienen a lo sumo un elemento) que hacen que el algoritmo de fusión reciba siempre listas ordenadas.

15 (defun mergesort (lista)
(if (vacia lista) lista (combina (mergesort (divideizq lista)) (mergesort(divideder lista)) ) ) (defun combina ( lista1 lista2 ) ( if (and lista1 lista2) (if ( < (first lista1) (first lista2) ) (cons (first lista1) (combina (rest lista1) lista2)) (cons (first lista2) (combina lista1 (rest lista2)))) (or lista1 lista2) (defun divideder (lista) ( last lista (ceiling (/ (length lista) 2 ) ) ) (defun divideizq (lista) (ldiff lista (divideder lista) ) (defun vacia (lista) (or (eq (length lista) 1) (eq (length lista) 0) )

16 Análisis del Algoritmo
Tiempo de Ejecución: Caso promedio. La complejidad para dividir una lista de n es O(n). Cada sublista genera en promedio dos sublistas más de largo n/2. Por lo tanto la complejidad se define en forma recurrente como: f(1) = 1 f(n) = n + 2 f(n/2) La forma cerrada de esta expresión es: f(n) = n log2n Es decir, la complejidad es O(n log2n). Ventajas: Rápido No memoria adicional Desventajas: Implementación un poco más complicada. Recursividad (utiliza muchos recursos).

17 Shell sort Inventado en 1959, éste algoritmo es el más eficiente de los del tipo O(n2). Pero el Shell es también el más complicado de los algoritmos de este tipo. Como funciona Es una mejora del método de inserción directa, utilizado cuando el array tiene un gran número de elementos. En este método no se compara a cada elemento con el de su izquierda, como en el de inserción, sino con el que está a un cierto número de lugares (llamado salto) a su izquierda. Shell sort mejora al método de inserción comparando los elementos separados por una distancia de varias posiciones, esto permite que un elemento tome “pasos mas grandes” hacia su posición esperada. Se realizan varias pasadas sobre los datos con pasos cada vez menores. El último paso del Shell sort es plenamente un ordenamiento por inserción pero para entonces se garantiza que el arreglo esté prácticamente ordenado Tal vez la propiedad mas crucial del Shell sort es que los elementos se mantienen k-ordenados incluso mientras el salto se disminuye. Por ejemplo, si una lista ya fue 5-ordenada y luego 3-ordenada, la lista ahora no solamente está 3-ordenada sino 5 y 3-ordenada. Si esto no fuera cierto, el algoritmo “desharía” trabajo que había hecho previamente y no obtendría tiempos de ejecución tan bajos.

18 El algoritmo requiere menos de O(n²) comparaciones y cambios en el pero caso. Aunque es fácil desarrollar intuitivamente el sentido de cómo funciona el algoritmo, es bastante difícil analizar su tiempo de ejecución pero los estimados difieren entre O(nlog2n) a O(n1.5) dependiendo de los detalles de implementación Dependiendo en la elección de la secuencia de saltos, Shell sort a probado tener un tiempo de ejecución en el peor caso igual a O(n2), O(n3/2), O(n4/3) o O(nlog2n) o posiblemente mejores tiempos de ejecución aún no probados. La existencia de una implementación que tenga una complejidad O(nlogn) en el peor caso para el Shell sort aún se mantiene como una pregunta abierta a la investigación. El tamaño del set de datos usado tiene un impacto significativo en la eficiencia del algoritmo. Algunas implementaciones de este algoritmo tienen una función que permite calcular el tamaño óptimo del set de datos para un array determinado. La secuencia de salto que fue sugerida inicialmente por Donald Shell fue comenzar con N/2 y posteriormente disminuir a la mitad el salto hasta que llegue a 1. Esta secuencia provee una mejora de desempeño sobre los algoritmos cuadráticos como el método por inserción pero puede ser cambiada levemente para disminuir aún mas el tiempo de ejecución en el peor y el caso promedio.


Descargar ppt "Complejidad de Algoritmos"

Presentaciones similares


Anuncios Google