Вы находитесь на странице: 1из 27

Análisis y diseño de algoritmos Clase 2

Contenido

Eficiencia algorítmica

• Notación “Big Oh”

Ejercicios del curso

Tips & tricks

Oh” • Ejercicios del curso • Tips & tricks Material elaborado por: Julián Moreno Facultad de

Material elaborado por: Julián Moreno Facultad de Minas, Departamento de Ciencias de la Computación y la Decisión

¿Qué es un algoritmo?

Un algoritmo es un conjunto ordenado de instrucciones bien definidas, inambiguas y finitas que permite resolver

un determinado problema computacional.

Un corolario de esta definición es que un determinado problema computacional puede ser resuelto por diversos

(infinitos) algoritmos.

Veamos una demostración de esto

Nota

Para este y todos los ejercicios que veremos en clase:

paciencia para los que ya saben y mucha concentración para los que se sientan perdidos.

Ejemplo: ACM138 “Street Numbers”*

A computer programmer lives in a street with houses numbered

consecutively (from 1) down one side of the street. Every evening she walks her dog by leaving her house and randomly turning left or right and walking to the end of the street and

back. One night she adds up the street numbers of the houses

she passes (excluding her own). The next time she walks the other way she repeats this and finds, to her astonishment, that the two sums are the same. Although this is determined in part by her house number and in part by the number of houses in

the street, she nevertheless feels that this is a desirable

property for her house to have and decides that all her subsequent houses should exhibit it.

Write a program that, given the quantity of houses in a street,

prints the number of the house that satisfy this condition; print -1

¿Qué es un algoritmo?

Esta característica (múltiples alternativas para resolver un mismo problema) es la motivación de este curso:

diseñar buenos (si no los mejores) algoritmos para

solucionar determinados problemas.

“Quizá el principio más importante para el buen diseñador de algoritmos es negarse a estar satisfecho”

Aho et al. (1974) The design and analysis of computer algorithms

“Si deseas ser un buen diseñador de algoritmos debes preguntarte siempre a manera de mantra: ‘¿se puede hacer mejor?’”

Roughgarden, T. (2012) Algorithms design and analysis part 1

“La primera solución que se nos ocurre o la solución más obvia

generalmente no es la mejor

Yo :D

¿Cómo se mide un algoritmo?

Una manera objetiva de determinar que tan “bueno” es un algoritmo es por medio del número de operaciones

básicas que este debe realizar para resolver un problema

cuya entrada tiene un tamaño n. Es decir, calcular un f(n)

Ejemplo: cuantas comparaciones debe realizar el

selectSort dado un arreglo n de números enteros

diferentes. f(n)=?

f(n)= n*(n-1)/2 - 1

Notación “Big Oh”

Definición formal:

T(n) = O(f(n)), si y solo si existen dos constantes c, n0 > 0 de forma que T(n) c*f(n) para todo n>n0

En términos prácticos:

Considerar el peor escenario

Realizar un análisis asintótico (enfocarse en

valores grandes de n)

No prestar atención a términos constantes o de orden menor

En el ejemplo del selectSort podemos decir entonces que T(n) = 2 , o lo que es lo mismo, que es O( 2 )

Utilidad de la notación “Big Oh”

Aunque esta notación no es rigurosa, y no se debe usar como un predictor exacto del tiempo de ejecución de un

algoritmo, si da cuenta de su tasa de crecimiento cuando

varia el tamaño de la entrada. Por ejemplo, si un algoritmo es O(n 2 ) podemos decir que su tasa de crecimiento es proporcional a una función cuadrática.

Decimos entonces que un algoritmo es mejor (más eficiente) que otro si su “O” es menor. En otras palabras si su tiempo de ejecución (determinado por la cantidad de operaciones básicas) considerando el peor escenario crece más lentamente a medida que se aumenta el tamaño de la entrada.

Comparación de eficiencias

Habiendo comprendido el concepto de la notación Big O, ¿cuál es el orden de complejidad o eficiencia del

bubbleSort?, ¿por qué?

function bubbleSort(X[], n){ for i=n-1:1 { ordenado = true; for j=0:i-1 { if (X[j] > X[j+1]){

switch(X[j], X[j+1])

ordenado = false;

}

}

if ordenado = true: break

}

}

O( 2 ) al igual que selectSort es cuadrática (lo mismo

sucede con el insertSort)

Comparación de eficiencias

Volviendo al ejercicio de “street numbers”, determinemos cuál de los siguientes algoritmos es mejor.

function sn1(n){ if n<3: return -1 for i=2:n-1{ s1 = 0 for j=1:i-1{ s1 += j

}

s2 = 0 for k=i+1:n{ s2 += k

}

if s1=s2: return i

}

return -1

}

O( 2 ) cuadrática

function sn2(n){ if n<3: return -1 for i=2:n-1{ s1 = i*(i-1)/2 s2 = (n*(n+1)-i*(i+1))/2 if s1=s2: return i

}

return -1

}

O(n) lineal

function sn3(n){ if n<3: return -1 i = sqrt((n^2+n)/2) if i = INT(i): return i return -1

}

O(1) constante

if i = INT(i): return i return -1 } O(1) constante Cambiando el condicional por una
if i = INT(i): return i return -1 } O(1) constante Cambiando el condicional por una

Cambiando el condicional por una igualdad y luego despejando

Considerando que

=1

= (+1)

2

Comparación de eficiencias

Ejercicio: Considerando la notación Big O, determinemos

cuál de los dos siguientes algoritmos es mejor.

function fib1(n){

de los dos siguientes algoritmos es mejor. function fib1(n){ if n <= 1: return n else

if n <= 1: return n

else return fib1(n-1) + fib1(n-2)

}

≈ O(2 )

en realidad O(φ ) siendo φ =

function fib2(n){

f[0] = 0, f[1] = 1 for i = 2:n{ f[i] = f[i-1] + f[i-2]

}

return f[n]

}

O(n)
O(n)
1+ 5 2
1+
5
2

Comparación de eficiencias

Supongamos que estamos trabajando en un computador con procesador de un solo

núcleo a 3,2Ghz lo que nos da un aproximado de 1,000,000,000 operaciones por segundo. En la siguiente tabla vamos a relacionar el tamaño de un problema determinado con lo que demorarían en resolverlo una serie de algoritmos con eficiencias diferentes.

 

log(n)

 

n

n.log(n)

 

n^2

n^3

2^n

10

≈ 3 nanosegs

10

nanosegs

≈ 33 nanosegs

100

nanosegs

1 microseg

≈ 1 microseg

100

≈ 7 nanosegs

100

nanosegs

≈ 664 nanosegs

10

microsegs

1 miliseg

≈ 4E+10 milenios

1000

≈ 10 nanosegs

1

microseg

≈ 10 microsegs

1 miliseg

1 seg

 

10.000

≈ 13 nanosegs

10

microsegs

≈ 133 microsegs

100

milisegs

17 minutos

 

100.000

≈ 17 nanosegs

100

microsegs

≈ 2 milisegs

10

segs

≈ 12 dias

 

1'000.000

≈ 20 nanosegs

1 miliseg

≈ 20 milisegs

≈ 17 minutos

≈ 32 años

 

1E+9 (mil millones)

≈ 30 nanosegs

1 seg

≈ 30 segs

≈ 32 años

   

1E+12 (un billón)

≈ 40 nanosegs

≈ 17 minutos

≈ 11 horas

     

1E+15 (mil billones)

≈ 50 nanosegs

≈ 12 dias

≈ 1 año y medio

     

1E+18 (un trillón)

≈ 60 nanosegs

≈ 31 años

≈ 19 siglos

     

1E+21 (mil trillones)

≈ 70 nanosegs

≈ 317 siglos

       

1E+24 (un cuatrillón)

≈ 80 nanosegs

         

1E+27 (mil cuatrillones)

≈ 90 nanosegs

       
1E+27 (mil cuatrillones) ≈ 90 nanosegs        

Notaciones Big Omega y Big Theta

Definiciones formales:

T(n) = Ω(f(n)), si y solo si existen dos constantes c, n0 > 0 de forma que T(n) c*f(n) para todo n>n0

T(n) = θ(f(n)), si y solo si existen tres constantes c1, c2, n0 > 0 de forma que c2*f(n) T(n) c2*f(n) para todo

n>n0

Así como la notación Big O puede considerarse como un límite superior, la notación Big Omega puede considerarse como un límite inferior.

La notación Big Theta implica que T(n) es al mismo tiempo O(f(n)) y Ω(f(n))

Ejercicios de programación tipo ACM-ICPC

Todo ejercicio ACM cuenta con los siguientes 7 elementos

Un título

Un tiempo límite, que determina cuánto es lo máximo que

debe demorarse la ejecución del código solución

Un enunciado que contextualiza el problema y que es presentado generalmente a manera de historia (a veces

descabellada)

Una descripción explícita de cómo es la entrada del problema:

cuántos parámetros son, de qué tipo y en qué rango. Si el ejercicio dice que un determinado dato es de cierta forma no es necesario verificarlo (su rango por ejemplo)

Una descripción explícita de cómo debe ser la salida: qué

incluye y en qué formato. Tanto la entrada como la salida de

datos deben ser por consola

Un ejemplo de entrada

Un ejemplo de salida

Ejercicios de programación tipo ACM-ICPC

Recomendaciones para la solución de problemas

1. Aunque suene tonto, leer cuidadosamente el enunciado. Comenzar entendiendo mal no tiene sentido

2. No comenzar a codificar sin antes tener clara la solución. Preferiblemente diseñar primero el algoritmo en papel y lápiz.

3. Los ejemplos de entrada y salida son precisamente eso:

ejemplos. Que un código cumpla con ellos no significa

necesariamente que esté bueno. De hecho los ejemplos suelen ser los casos más triviales, por ello parte del diseño del algoritmo debe ser probarlo ante casos extremos (según indique el rango de las entradas) o “truculentos” (que sean

extraños pero que no violen las restricciones dadas).

Envío de ejercicios a la plataforma

Una vez cumplidos los cuatro pasos anteriormente mencionados se puede enviar el código teniendo en cuenta que:

1.

La clase o archivo principal se debe llamar Main y no debe

estar en ningún paquete

2.

Se deben incluir las librerías requeridas siempre que sean estándar

3.

No se puede incluir paquetes ni archivos externos

Envío de ejercicios a la plataforma

Ejemplo:

Envío de ejercicios a la plataforma Ejemplo:

Envío de ejercicios a la plataforma

Ejemplo:

import java.util.*;

public class Main {

public static void main(String[] args) { Scanner entrada = new Scanner(System.in); StringBuilder sb = new StringBuilder();

int N;

N = entrada.nextInt(); for(int i=0; i<N; i++){ sb.append("Hello world!\n");

}

System.out.println(sb.toString());

}

}

Envío de ejercicios a la plataforma:

Posibles resultados de un envío

Error de compilación: El código no compila. Esto suele

deberse a no cumplir con las consideraciones 3 a 5 anteriormente mencionadas. A veces ocurre por usar ñ’s en el

código

Error de ejecución: El código compila pero no corre, o no

termina de correr debido a un error. Un caso típico es un

indexOutOfBounds

Tiempo límite excedido: El código corre pero se demora más del tiempo límite, cuando esto ocurre la plataforma

detiene su ejecución en ese momento

Respuesta incorrecta: El código corre y se ejecuta antes del tiempo límite pero la salida no coincide con la que tiene almacenada el juez para la entrada correspondiente. Muchas veces se debe a cuestiones de formato (mayúsculas o

minúsculas, espacios en blanco, saltos de línea, etc.)

Aceptado: El código corre, se ejecuta antes del tiempo límite

y la salida es la adecuada

Entrada de datos

Scanner vs. InputStreamReader

Scanner sirve para leer dato por dato especificándole el tipo de cada uno

Scanner entrada = new Scanner(System.in);

int a = entrada.nextInt(); float b = entrada.nextFloat();

InputStreamReader sirve para leer línea por línea, cada una

como un único string

BufferedReader entrada = new BufferedReader(new InputStreamReader(System.in)); String linea = entrada.readLine();

Entrada de datos

StringTokenizer vs. Split

StringTokenizer sirve para partir un string en tokens”, es decir,

elementos individuales separados entre sí por un delimitador.

Los delimitadores por defecto son: " \t\n\r\f“, es decir, espacio en blanco, caracter de tabulación, caracter de nueva línea, caracter de retorno, y caracter de salto de página

StringTokenizer st = new StringTokenizer(linea); while( st.hasMoreTokens() ){ String dato = st.nextToken(); //procesar dato, por ejemplo convertirlo a algún tipo

}

Tiene además otras dos formas de usarse:

StringTokenizer(String str, String delim) //especifica los delimitadores StringTokenizer(String str, String delim, boolean

returnDelims) //especifica los delimitadores y estos se

retornan junto a los tokens

Entrada de datos

StringTokenizer vs. Split

Split hace lo mismo que StringTokenizer solo que además de permitir definir delimitadores específicos, también permite usar expresiones regulares lo cual puede ser útil si

se desea remover datos inútiles de la entrada

String arr1[] = linea.split("-"); String arr2[] = linea.split("\\s+|,\\s*|.\\s+");

Entrada de datos: Análisis

Generalmente InputStreamReader es más rápido que Scanner, mientras que StringTokenizer es más rápido que Split.

, mientras que StringTokenizer es más rápido que Split. Tomado de: http://goo.gl/qO2RFM Qué usar dependerá

Tomado de: http://goo.gl/qO2RFM

Qué usar dependerá entonces de la entrada:

Pocos datos: Scanner

Muchos datos: InputStreamReader

Datos relativamente uniformes: StringTokenizer

Datos que requieran limpieza o una “extracción especial: Split

Salida de datos: impresión

No se debe abusar de System.out.println() pues hace lento el código. Cuando se necesite imprimir por pantalla muchos datos es preferible “acumularlos” y luego imprimirlos de una sola vez.

Para concatenar sin embargo no es recomendable usar String pues

esta tiene una eficiencia O(n), es preferible usar StringBuffer o StringBuilder, ambos con concatenación O(1) pero siendo más rápida la segunda por no ser sincronizada.

StringBuilder sb = new StringBuilder();

sb.append("Hola "); sb.append("mundo"); System.out.println(sb.toString());

Salida de datos: formatación

Cuando se requiere imprimir un valor numérico con una cantidad de cifras decimales y/o con un ancho de campo determinado se puede usar System.out.printf().

System.out.printf("%.2f", 123.458); //mostraría 123.46

//%f es para real, %d para entero, %e notación científica

System.out.printf("%+d", 26); //mostraría +26

System.out.printf("%10.1f", 123.458); //mostraría

123.46

Datos de alta magnitud o precisión

BigInteger: Entero de precisión arbitraria (Para operaciones con enteros muy grandes, si deben manejar valores mayores a , cuidado, se debe usar solo como último recurso pues es menos eficiente). BigInteger provides analogues to all of Java's primitive integer operators, and all

relevant methods from java.lang.Math. Additionally, BigInteger provides

operations for modular arithmetic, GCD calculation, primality testing, prime generation, bit manipulation, and a few other miscellaneous operations.

BigDecimal (Para operaciones con números reales, cuando la precisión

de double no basta). Immutable, arbitrary-precision signed decimal numbers. A BigDecimal consists of an arbitrary precision integer unscaled value and a 32-bit integer scale. If zero or positive, the scale is the number of digits to the right of the decimal point. If negative, the

unscaled value of the number is multiplied by ten to the power of the

negation of the scale. The value of the number represented by the BigDecimal is therefore (unscaledValue × 10-scale).

The

scale

BigDecimal

class

provides

operations

for

arithmetic,

manipulation, rounding, comparison, hashing, and format conversion.

Estructuras de datos típicas

Estructura

Java

Inserción

Indexación

Búsqueda

Borrado

Arreglo

ArrayList

O(n)

O(1)

O(log(n)) si está ordenado, O(n) si no

O(1)

dinámico

ListaEnlazada

LinkedList

O(1) si es al inicio o al

O(1) para el inicio o el

O(n)

O(1) para el inicio o el

final, O(n) si

final, O(n) si

final, O(n) si

no

no

no

Pila

Queue

O(1) para el push

O(1) para el peek

O(n)

O(1) para el pop

Cola

Deque

O(1) para el

O(1) para el

O(n)

O(1) para el

push

peek

pop

Árbol binario de búsqueda balanceado

TreeSet

O(log(n))

No aplica

O(log(n))

O(log(n))

Montículo

PriorityQueue

O(log(n))

No aplica

O(1) si es para la cima,

O(log(n)) si es la cima,

binario

O(n) si no

O(n) si no

Tabla hash

HashMap

O(1)*

No aplica

O(1)*

O(1)*

Tareas

1. Para calentar motores en lo que a programar se refiere, codificar los tres algoritmos de ordenamiento

mencionados (selectSort, insertSort, bubbleSort) en el

lenguaje que deseen. Luego probarlos con diferentes tamaños de arreglos (100, 1.000, 10.000, 100.000, 1000.000). Hacer un gráfico tamaño vs tiempo de

ejecución. Comprobar si tienen la forma n^2.

2. Hacer lo mismo con las funciones fib1 y fib2 pero para los valores de n entre 5 y 20.

3. Realizar todos los ejercicios de calentamiento en la plataforma.