Académique Documents
Professionnel Documents
Culture Documents
funciones
Un algoritmo es una lista ordenada de instrucciones claras y precisas para resolver cierto
problema. Para resolver un determinado problema, generalmente podemos hacerlo de
distintas maneras. Nos interesa desarrollar la capacidad de analizar algoritmos para
determinar qué algoritmo resuelve el problema de la manera más “eficiente”.
Ejemplo 8.1:
Escribir un algoritmo que lea n números enteros y que imprima el menor.
Solución:
Algoritmo 1
Para resolver este problema, podemos leer los n datos y almacenarlos en un vector.
Luego, para hallar el menor, usamos una variable que siempre tenga el menor hasta el
momento. Esta variable se puede inicializar con el primer elemento del vector, y luego
comenzamos a comparar desde el segundo en adelante, actualizando la variable cada vez
que hallemos uno menor que el que teníamos. El siguiente algoritmo resuelve el
problema:
for i=1 to n
read(A[i]);
menor-hasta-el-momento = A[1];
for i= 2 to n
if A[i] < menor-hasta-el-momento
menor-hasta-el-momento = x;
menor = menor-hasta-el-momento;
Algoritmo 2
Quizás el lector haya notado que para resolver este problema no es necesario tener todos
los datos almacenados en memoria a la vez. Podemos ir guardando el menor hasta el
momento, y leer cada dato en la misma variable. Una vez haya actualizado el menor hasta
el momento, puedo leer el próximo dato en la misma variable anterior, pues ya no
necesitamos aquel valor. El menor hasta el momento se puede inicializar con el entero
mayor posible, de modo que al comparar con el primer dato que leamos, será seguro que
este dato será menor o igual al valor inicial escogido, y así actualizaremos el menor hasta
el momento con este primer dato. Entonces el algoritmo sería:
menor-hasta-el-momento = +∞;
for i = 1 to n
{
read(x);
if x < menor-hasta-el-momento
menor-hasta-el-momento = x;
}
menor = menor-hasta-el-momento;
¿Qué algoritmo es mejor? ¿Cuál será más eficiente? Hay varios elementos importantes
que es preciso considerar para determinar que un algoritmo es mejor o peor que otro. Por
ejemplo, dos características importantes de un buen algoritmo son su claridad y sencillez.
Estas son características importantes, sobre todo cuando se conoce que la gran mayoría
de los programas que se escriben en algún momento habrá que modificarlos, ya sea para
corregir errores, o para mejorarlo por alguna otra razón. Es imprescindible que la persona
que tendrá que modificar el algoritmo entienda perfectamente qué hace el algoritmo y
cómo lo hace.
En términos del uso de memoria, diremos que un algoritmo es más eficiente en la medida
en que requiera menor uso de memoria de la computadora. En ese sentido, no hay duda
que el algoritmo 2 es mejor que el del 1, pues mientras que el segundo necesita tan sólo 4
espacios para variables (x, i, menor-hasta-el-momento y menor), el primero almacena
todos los datos a la vez, usando n variables y tres variables adicionales (i, menor-hasta-
el-momento y menor). Por lo tanto, el primer algoritmo requiere n + 3 espacios en
memoria para almacenar esa cantidad de números enteros, mientras que el segundo sólo
necesita espacio para cuatro enteros.
Sin embargo, el aspecto que generalmente nos interesa más (suponiendo que disponemos
de suficiente espacio en memoria), es el tiempo de ejecución. Nos interesa medir la
eficiencia o complejidad de un algoritmo como una propiedad del algoritmo propiamente,
independientemente de la máquina en que se ejecute. Evidentemente, el tiempo de
ejecución no será el mismo si ejecutamos el algoritmo en una 286 que en una Penthium.
Por esta razón, no usaremos el tiempo de ejecución directamente para medir la eficiencia
de un algoritmo, sino una medida que de alguna manera (pues no lo hace totalmente) no
tome en cuenta la velocidad o las características particulares de la computadora en que se
ejecuta el algoritmo. Esta medida es el número de instrucciones necesarias para ejecutar
el algoritmo. Este número debe ser, más o menos, el mismo independientemente de la
máquina.1
1
Aún en esta medida también puede haber diferencias de una máquina a otra, pues lo que es una
instrucción en una computadora quizás en otra se descompone en varias instrucciones, pero sin duda con
esta medida las diferencias se hacen menos significativas que si consideramos el tiempo de ejecución.
Definimos la función T(n) como el número de instrucciones necesarias para ejecutar un
algoritmo en el cual la data es de tamaño n. En ocasiones la cantidad de instrucciones que
se ejecutan depende de los valores específicos de la data. En este caso, podemos calcular
el número promedio de instrucciones que ejecuta un algoritmo (tomando en
consideración todos los posibles tipos de datos), o podemos analizar el peor caso
(partiendo de la combinación de datos que haría que el algoritmo ejecute el mayor
número de instrucciones), o el mejor caso.
Ejemplo 8.2:
Determine el número de instrucciones que se ejecutan en el siguiente ciclo:
for i = 1 to n
read(x);
Solución:
En el “for” la i empieza en 1 y compara con n. Si 1 ≤ n, entonces entra al ciclo. Luego la
i se incrementa a 2, y vuelve a comparar. Cuando la i llegue a n + 1, compara con n y
entonces es que no entra al ciclo. Por tanto, entrará al ciclo n veces, pero compara con la
n una vez adicional, es decir, n + 1 veces. n veces entra al ciclo, y una vez no. El “for”,
por lo tanto, tiene tres partes: la inicialización se ejecuta una sola vez, se hacen n+1
comparaciones, y la i se incrementa n veces. Por lo tanto, esta instrucción se descompone
en 1 + n + 1 +n = 2n + 2 instrucciones, y el “read” se ejecuta n veces. Por tanto, en total
se ejecutan
T(n) = (2n + 2) + n = 3n + 2
Ejemplo 8.3:
Halle T(n) para el Algoritmo 1 del ejemplo 8.1:
Solución:
En la tabla a continuación, aparece el número de veces que se ejecuta cada instrucción (o
en el caso del “for” el número de instrucciones en que se descompone).
T(n) = 7n + 2
Ejemplo 8.4:
Halle T(n) para el algoritmo 2 del ejemplo 8.1:
Solución:
La instrucción 2 se descompone en 1 + n + 1 + n = 2n + 2 instrucciones. El algoritmo
entra al ciclo para ejecutar las instrucciones 4, 5 y 6 n veces. Las instrucciones 4 y 5 se
ejecutan estas n veces, y la 6, en el peor caso, también se ejecutará n veces.
Ejemplo 8.5:
Determine el valor de T(n) para el siguiente algoritmo
for i = 1 to n
for j = 1 to n
write(i,j);
Solución:
La instrucción 1 se descompone en 1 + n + n +1 = 2n + 2 instrucciones. La instrucción 2
se descompone en 1 + n + 1 + n = 2n + 2 instrucciones para cada valor de i, desde 1 hasta
n. Por tanto, ahí se ejecutan n(2n + 2) instrucciones. La instrucción 3 se ejecuta para cada
i y cada j desde 1 hasta n. Por la regla del producto, se ejecuta n2 veces. Por tanto, este
algoritmo tiene T(n)
Ejemplo 8.6:
Determine T(n) para el siguiente algoritmo de ordenar (“bubblesort”):
for i = 1 to n-1
for j = n downto2 i + 1
if A[j] < A[j - 1]
{
A[j] = temp;
temp = A[j - 1];
A[j - 1] = temp;
}
Solución:
La instrucción 1 se descompone en 1 + n + n -1 = 2n instrucciones. El análisis de la
instrucción 2 lo hacemos de la siguiente manera:
Nuevamente, esto es una progresión aritmética, cuyo valor es: n(n - 1)/2. Entonces, el
valor de T(n) es:
2
“downto” tiene el efecto de hacer que la variable del ciclo vaya disminuyendo su valor en 1 cada vez que
se sale del ciclo.
T(n) = 2n + n2 + n -2 + 4n(n - 1)/2 = 3n2 + n - 2
Crecimiento de funciones
Podemos dar ejemplos de instrucciones que se ejecutan una cantidad fija de veces, sin
importar el tamaño de la data. Por ejemplo, considere las siguientes instrucciones:
suma = 0;
for i = 1 to 10
{
read(x);
suma = suma + x;
}
promedio = suma/x;
Consideremos ahora un algoritmo para el cual T(n) sea una función lineal en n. Digamos
que T(n) = 5n + 1. La siguiente tabla muestra como aumenta T(n) con el aumento de n.
n T(n)
1 6
2 11
4 21
8 41
16 81
32 161
64 321
Definiremos la función orden O como la función que nos dice cómo aumenta T(n) a
medida que n aumenta. En este caso, como T(n) crece proporcional a n, decimos que
T(n) = O(n).
Ejemplo 8.7:
Determine cómo aumenta T(n) con aumentos de n para un algoritmo con T(n) = 2n2.
Solución:
Construyamos la tabla que indica los aumentos en T(n) a medida que n aumenta:
n T(n)
1 2
2 8
4 32
8 128
16 512
32 2048
64 8192
Vemos que si n se duplica, entonces T(n) se cuatriplica, es decir, se hace 4 veces más
grande. Si n se hace 4 veces mayor (por ejemplo, de 8 a 32), entonces T(n) se hace
2048/128 = 16 veces mayor. Esto implica que T(n) aumenta proporcionalmente con el
cuadrado de n. Un aumento del doble en n, es decir n aumenta a 2n, entonces T(n)
aumenta a (2T(n))2, es decir, a 4T(n). Esto es así porque T(n) es una función cuadrática
de n. En este caso decimos que T(n) es O(n2).
En este ejemplo, los cambios en n se reflejaban en proporción idéntica en T(n), pues la
expresión cuadrática de T(n) no tenía término lineal ni constante. Consideremos el
siguiente ejemplo, en el cual sí aparecen estos términos.
Ejemplo 8.8:
Determine cómo aumenta T(n) con aumentos de n para un algoritmo con
T(n) = 2n2 + 2n + 3
Solución:
Construyamos la tabla que indica los aumentos en T(n) a medida que n aumenta:
n T(n) T(n)/T(n/2)
1 7
2 15 15/7 = 2.14
4 43 43/15 = 2.87
8 147 3.42
16 547 3.72
32 2115 3.87
64 8323 3.94
128 33027 3.97
En este caso, cuando la n se duplica, por ejemplo, T(n) no se hará exactamente 4 veces
mayor. Sin embargo, la razón de T(n)/T(n/2) esta muy cerca de 4. Y lo importante aquí
es que a medida que n se hace cada vez más grande, esta razón se acerca cada vez más a
4. De manera que, a medida que n es mayor y mayor, T(n) crece más y más cerca con el
cuadrado de n. Por tanto, decimos que T(n) es O(n2).
Función O
Definición
Sean f(n), g(n) dos funciones. Decimos que f(n) es O(g(n)) si existe una constante c tal
que: f(n) ≤ c.g(n) para valores suficientemente grandes de n.
Por ejemplo, decimos que T(n) es O(n) si la función g(n) = n domina a T(n) al
multiplicar g(n) por una constante, sin importar cuánto más crezca n.
Solución:
Tenemos que demostrar que la función g(n) = n, multiplicada por una constante c,
domina a T(n) = 5n + 1 a partir de un cierto valor de n. La constante c la podemos
escoger nosotros. Lo importante es que esta constante no cambia aún cuando la n
aumente.
¿Cómo podemos hacer que se cumpla la siguiente desigualdad?
Si escogemos c = 6, entonces esa desigualdad será cierta para todos los valores de n. Es
decir, para n ≥ 1, es cierto siempre que 5n + 1 ≤ 6n. 3Esto demuestra que T(n) es O(n).
Ejemplo 8.10:
Demuestre que T(n) = 5/2n2 -1/2n -1 es O(n2).
Solución:
Tenemos que demostrar que
Verifique que
5/2n2 -1/2n -1 ≤ n2
para n ≥ 1.
Teorema 1:
Sea p(x) un polinomio de grado k. Entonces p(x) es O(xk).
Demostración:
Sea
Esta es la forma general de un polinomio de grado k. Para demostrar que p(x) es O(xk),
tenemos que hallar una constante c tal que
Claramente,
3
Note que siempre podemos escoger una constante mayor, y la desigualdad claramente se
cumplirá también.
akxk + ak-1xk-1 + ... + a1x + a0 ≤ (ak + ak-1 + ... + a1 + a0)xk
para toda x ≥ 1, pues aixi ≤ aixk, siempre que i ≤ k. Por tanto, queda demostrado que p(x)
es O(xk).
Teorema 2:
Sean p(x) y q(x) dos polinomios, donde el grado de q(x) es mayor o igual que el de p(x).
Entonces si la función f(x) es O(p(x)), también es O(q(x)).
Demostración:
Si f(x) es O(p(x)) entonces, por definición, existe una constante c1 tal que
Como el grado de q(x) ≥ grado de p(x), no es difícil ver que para valores suficientemente
grandes de n, q(x) dominará a p(x). De hecho, si tomamos c2 = p(1), entonces tendremos
que
Suponga que T(n) = 5n3 + 2n2 + 1. Según el teorema 1, tenemos que T(n) es O(n3).
Según el teorema 2, T(n) también es O(n4), O(n5), O(n6), ...
Al decir que una función es O(g(n)) lo que podemos saber de la función es que a partir de
ciertos valores de n la función no crece más rápido que g(n). Puede ser, sin embargo, que
crezca mucho más lento que g(n). Generalmente, nos interesa hallar la función de menor
orden O tal que domine a la función en cuestión, pues es la que nos da más información
sobre el comportamiento de la función. Así, por ejemplo, preferimos decir que T(n) = 5n2
es O(n2), aunque también sea O(n3).
Ejemplo 8.11:
Halle la complejidad (función orden O) para el siguiente algoritmo que hace una
búsqueda lineal en un vector de n elementos:
encontré = false;
i = 1;
while not encontré and (i ≤ n)
if x = A[i]
encontré = true;
else
i = i + 1;
Solución:
La cantidad de instrucciones que ejecutará este algoritmo dependerá cuán rápido
encontremos en el vector el elemento x que se busca. Hagamos el análisis del peor caso,
esto es, o que el elemento esté en la última posición en el vector, o que no esté en
ninguna. En ambos casos, la instrucción del “while” se ejecutará n + 1 veces (la i tiene
que ir desde 1 hasta n + 1). La comparación del “if” se hará n veces. Si x no está en el
vector, entonces la instrucción del “else” se hará n veces y la del “if” ninguna. En el peor
caso, la suma de ambas será n.
En la tabla, suponemos que el número que buscamos no está en la lista. Como T(n) = 3n
+ 3, sabemos que el algoritmo tiene complejidad O(n). El análisis estuvo basado en el
peor caso. El caso promedio se puede analizar de la siguiente manera. Si partimos de que
el número está en la lista y de que la probabilidad de que el número esté en una posición
determinada es la misma para todas ellas, entonces tendremos que buscar, en promedio,
en la mitad del vector. Es decir, tendremos que hacer en promedio n/2 comparaciones.
Note que el número de comparaciones será menor que en el peor caso, pero sigue siendo
un polinomio lineal en n, y por lo tanto T(n) seguirá siendo O(n).
suma = 0;
for i = 1 to 10
{
read(x);
suma = suma + x;
}
promedio = suma/x;
Este algoritmo tiene T(n) = 33. Como 33 ≤ c⋅1 para c ≥ 33, esto demuestra que T(n) es
O(1). También, claro está, es O(2), O(100), etc., pero generalmente decimos que es O(1).
Ejemplo 8.12:
Determine la complejidad del siguiente algoritmo:
i = 1;
while i ≤ n
{
i = 2 * i;
write(i);
}
Solución:
La cantidad de veces que se ejecute el ciclo dependerá del valor de n. Sin embargo, la
variable i no va tomando todos los valores hasta llegar a n, sino que asume los valores 1,
2, 4, 8, etc. ¿Cuántos valores distintos asumirá antes de que sea mayor que n, y entonces
el algoritmo se vaya del ciclo? Consideremos la siguiente tabla, que nos indica el número
de valores que asumirá la variable i antes de ser mayor que n para algunos valores de n:
n valores de i
4 1,2,4,8 = 4
8 1,2,4,8,16 = 5
16 6
32 7
64 8
128 9
256 10
512 11
1000 11
Note que cada vez que se duplica la n, el ciclo del “while” se ejecutará solamente una vez
más. La función que tiene esta propiedad es el logaritmo. Verifique que los pares
ordenados dados en la tabla coinciden con la función:
Esto implica que la instrucción del “while” se ejecuta ⎣log2n⎦ veces, y por tanto las
instrucciones dentro del ciclos e ejecutan cada una, ⎣log2n⎦ - 1 veces. Entonces
Por tanto,
T(n) = O(log2n)
Ejemplo 8.13:
Halle la complejidad (función orden O) para el siguiente algoritmo que hace una
búsqueda binaria de un entero x en un vector A de n elementos ordenados
ascendentemente:
encontré = false;
índice-izq = 1;
índice-der = n;
while not encontré and (índice-izq ≤ índice-der)
{
medio = (índice-izq + índice-der) div4 2;
if A[medio] = x
encontre = true;
else
if A[medio] < x
índice-izq = medio + 1;
else
índice-der = medio - 1;
}
if encontré
posición = medio;
Este algoritmo hace una búsqueda de x de la siguiente manera: se localiza el elemento del
medio del vector, y se compara con x. Si son iguales, acabamos y la posición es el índice
dada por la variable medio. Si x es mayor que el elemento del vector en el medio,
entonces, como suponemos que el vector esta ordenado de menor a mayor, seguimos la
búsqueda en la parte derecha del vector (desde el ‘medio’ hasta el valor dado por el
4
“div” es la operación módulo (equivalente al operador ‘%’ en C.
índice derecho). Si x es menor, entonces buscamos en la parte de la izquierda (desde el
índice de la izquierda hasta el ‘medio’).
La clave del algoritmo es que luego de cada comparación, siempre logramos restringir la
búsqueda en un segmento del vector que es la mitad del tamaño anterior (más o menos
uno de la mitad, pues no siempre podemos dividir el vector exactamente por la mitad).
Así, si el vector original era de tamaño 128, al comparar con la posición del medio
(⎣(1+128)/2⎦ = 64), podremos descartar la mitad del vector y seguir la búsqueda en la
otra mitad. Si el vector fuera de 256 elementos, con la primera comparación ya lo
reduciríamos a la mitad. Con la segunda comparación a la cuarta parte. Con la tercera a la
octava parte, etc. S
En el peor caso, seguiremos buscando hasta que nos quede un segmento de un sólo
elemento. En este caso, en cuyo caso habremos comparado un número proporcional a log
n (vea la tabla). Si el elemento no está en el vector, entonces eventualmente el índice
izquierdo será mayor que el derecho, y saldremos del ciclo. También en este caso
habremos hecho un número proporcional a log n.
1 ⎡n/2⎤
2 ⎡n/4⎤
3 ⎡n/8⎤
k ⎡n/2k⎤ = 1
Cuando hemos hecho k comparaciones, nos queda un segmento del vector de tamaño
n/2k. En el peor caso, este valor será 1.
Por tanto, el número de comparaciones será proporcional a log n, lo que implica que el
algoritmo es O(log n).
Ejemplo 8.14:
Halle la complejidad del algoritmo siguiente:
1. k = 2;
2. for i = 1 to n
{
3. for j = 1 to k - 1
4. write(j);
5. k = 2 * k;
}
Solución:
La instrucción 1 se ejecuta 1 vez. La instrucción 2 se descompone en 2n + 2
instrucciones. La instrucción 5 se ejecuta una vez cada vez que entramos al ciclo de la i,
es decir, n veces. Para determinar cuántas veces se ejecutan las instrucciones 3 y 4,
consideremos la siguiente tabla, que nos dice cuántas veces se ejecutan las instrucciones
3 y 4 para cada valor de i y k:
n 2n 2n+1 2n - 1
∑2 i +1
= 4 + 8 + 16 + ... + 2n+1 = 2n+2 – 4 = 4.2n - 4
i =1
La instrucción 4 se ejecutará:
= - = - n = 2. 2n - 2 - n
Cómo crece una función con el crecimiento de la data que procesa es un aspecto
fundamental para analizar la eficiencia de un algoritmo. Podemos pensar que un
algoritmo con complejidad lineal O(n) es más eficiente que uno con complejidad
cuadrática O(n2). Sin embargo, no necesariamente este es el caso si la data no es lo
suficientemente grande. Un algoritmo con T(n) = 200n correrá más rápido que uno con
T(n) = n2 para valores pequeños de n (hasta n = 200, en este caso). Lo importante es que
a la larga, si la data es más y más grande, entonces el algoritmo con menor complejidad
será más eficiente.
Tomemos un momento para ver el impacto que tienen cada una de estas funciones en
términos del tiempo de ejecución de un algoritmo para diversos valores del tamaño de la
data, n. Podemos considerar que tenemos algoritmos que ejecutan una cantidad de
instrucciones equivalentes a las formas generales de las funciones descritas, tal como se
indica en la tabla a continuación, y que cada algoritmo se ejecuta en la misma máquina, la
cual puede ejecutar una instrucción promedio en 1 microsegundo (10-6 seg.). La primera
tabla indica el número de instrucciones que se ejecutarían por los algoritmos:
n log n n n log n n2 2n n!
10 3.32 10 33.2 100 1024 3.62x106
100 6.64 100 664 10,000 1.26x1030 9.33x10157
1,000 9.97 1,000 9,966 1,000,000 1.07x10301 -
10,000 13.29 10,000 132,877 100,000,000 - -
100,000 16.61 100,000 1,660,964 10,000,000,000 - -
1,000,000 19.93 1,000,000 19,931,569 1,000,000,000,000 - -
n log n n n log n n2 2n n!
10 3.32x10-6s 10-5s 3.32x10-5s 10-4s 1.024x10-3s 3.62s
100 6.64x10-6s 10-4s 6.64x10-4s .01s 4x1016 años 2.96x10150 años
1,000 9.97x10-6s 10-3s 9.97x10-3s 1s 3.4x10293 años -
10,000 1.33x10-5s .01s 1.33x10-1s 1 min. 40s - -
100,000 1.66x10-5s .1s 1.66s 2.78 horas - -
1,000,000 1.99x10-5s 1s 19.93s 11.58 días - -
Ejemplo 8.15:
Suponga que un viajero quiere visitar n ciudades. Entre algunas ciudades y otras hay
conexión directa y entre otras no. El problema consiste en determinar si existe una ruta
para el viajero de tal manera que termine en la misma ciudad en que empezó y visite cada
ciudad exactamente una vez.
Se ha demostrado que este problema, tan sencillo de plantear y con una aplicación tan
común, pertenece a la clase de problemas NP Completos. Es decir, no se conoce un
algoritmo polinomial que lo resuelva, pero tampoco se ha demostrado que dicho
algoritmo no exista.
Es posible que para una instancia específica de este problema, es decir, para un
determinado conjunto de ciudades con sus interconexiones directas, exista una solución
polinomial. Pero de lo que se trata el problema es de resolver el caso general. Dado
cualquier conjunto de ciudades y sus conexiones, debemos poder determinar su existe o
no la ruta con las propiedades que nos interesan.
Ejercicios:
a. for i = 1 to n
{
j = 1;
while j ≤ n {
write(j);
j = j + 1; }
}
b. i = n;
while i ≥ 1
{
write(i);
i = i div 2;
}
c. i = n;
for j = 1 to n
while i ≥ 1
{
write(i);
i = i div 2;
}
d. for j = n downto 2
read(x);
e. for i = 2 to n - 1
for j = i to n
write(‘función orden O’);
f. for i = 1 to n
for j = 1 to n
for k = 1 to n
read(A[i,j,k]);
g. k = 1;
for i = 1 to n
{
for j = 1 to k
write(j);
k = k + 1; }
2. Para cada uno de los ejercicios del problema 1, determine la complejidad del
algoritmo. En cada caso halle la constante y a partir de qué valor de n se cumple la
condición de la función orden O.
3. Demuestre que:
a. T(n) = 4n + 1 es O(n)
b. T(n) = 5n2 - 7n + 4 es O(n2)
c. T(n) = 5logn -7 es O(logn)
5. Si tiene algoritmos con los siguientes valores para la función orden O, ordénelos en
orden de menor a mayor complejidad o eficiencia.
nlogn, 1000, 25n2, 7logn, n!, nn, 3n, 12n + 75, n300
6. Halle la complejidad (función orden O) para el siguiente algoritmo que suma dos
matrices A y B de tamaño n x n:
for i = 1 to n
for j = 1 to n
C[i,j] = A[i,j] + B[i,j];