Programacin II
Del ejemplo anterior, deducimos que la descomposicin del problema deber finalizar en
algn momento, ya que si no fuera as, estaramos siempre aplicando la misma operacin de divisin
de los problemas, sin llegar a ningn lado y con peligro de agotar los recursos del ordenador. Por
tanto, es de vital importancia para aplicar la recursividad determinar aquellos subproblemas cuya
soluciones vengan dadas por soluciones directas o conocidas, pero no recursivas.
Otro ejemplo de la resolucin de un problema de forma recursiva, que sin duda alguna
aclarar bastante las ideas y que se sale del mbito de las matemticas, es la bsqueda de una
palabra en un diccionario: abrimos el diccionario ms o menos por la mitad, decidimos en qu mitad
est la palabra buscada. Si est en la primera, buscamos la palabra en dicha mitad utilizando esta
misma tcnica y si lo est en la segunda, hacemos lo propio con esa segunda parte del diccionario.
De esta forma vamos reduciendo el tamao del diccionario donde buscamos hasta que tenga el
tamao de una pgina, momento en el cual ser fcil buscar la palabra.
En este ejemplo podemos observar cmo el problema se va reduciendo de tamao: el
problema de buscar una palabra en un diccionario se reduce al de buscarla en una de sus mitades, el
cual se convertir en la bsqueda en su correspondiente mitad, es decir, en un cuarto del diccionario,
y as sucesivamente hasta que el proceso finalice cuando el diccionario conste tan slo de una
pgina, situacin en la que ser fcil localizar la palabra, pues slo habr que buscarla
secuencialmente.
De manera general, aquellos problemas que puedan ser resueltos de forma recursiva tendrn
las siguientes caractersticas:
1. Los problemas pueden ser redefinidos en trminos de uno o ms subproblemas, idnticos
en naturaleza al problema original, pero de alguna forma menores en tamao.
2. Uno o ms subproblemas tienen solucin directa o conocida, no recursiva.
3. Aplicando la redefinicin del problema en trminos de problemas ms pequeos, dicho
problema se reduce sucesivamente a los subproblemas cuyas soluciones se conocen
directamente.
4. La solucin a los problemas ms simples se utiliza para construir la solucin al problema
inicial.
Cuando plasmamos la solucin de un problema de manera recursiva en un algoritmo diremos
entonces que ha sido resuelto mediante un algoritmo recursivo. Este algoritmo, o en general cualquier
mdulo, tendr que realizar una o varias llamadas a s mismo, las cuales descompondrn el problema
en otros subproblemas con un tamao ms reducido, hasta que se lleguen a problemas cuyas
soluciones se conozcan, en cuyo caso irn devolviendo el control a los mdulos desde donde fueron
llamados, solventando as los problemas mayores que los originaron.
Es muy importante que siempre se determine en qu momento se detendrn las llamadas
recursivas del mdulo, ya que si no se hace, se corre el riesgo de no llegar nunca a la solucin del
problema inicial por estar llamndose el mdulo a s mismo de forma infinita.
Un aspecto que hay que tener en cuenta cuando se est diseando un algoritmo recursivo es
la cuestin de la eficiencia. La recursividad es una alternativa a la resolucin de problemas de forma
iterativa y, como ya hemos dicho, ofrece soluciones simples y elegantes a algunos problemas,
aunque en algunos casos estas soluciones no son mejores que las correspondientes iterativas,
fundamentalmente por que son tan ineficientes que son totalmente impracticables. Por tanto, y como
veremos en las secciones siguientes, a la hora de disear un algoritmo, habr que decidir que tipo de
solucin, recursiva o iterativa, es la ms idnea.
14
Recursividad
Una vez que hemos diseado el algoritmo recursivo, el siguiente paso dentro del proceso de
desarrollo del software es realizar su implementacin en un lenguaje de programacin, aunque no
todos estn preparados para ello. Lenguajes como BASIC, FORTRAN o COBOL no permiten la
implementacin de algoritmos recursivos, por lo que se descartar este mtodo de resolucin de
problemas en caso de tener que utilizarlos, buscando vas alternativas. Otros como C, C++, Pascal o
Java s incluyen esta caracterstica. En nuestro caso, utilizaremos el leguaje C para estudiar los
conceptos bsicos de la recursividad, creando para ello, y por analoga con los algoritmos, funciones
1
recursivas .
En el resto del tema nos centraremos en el estudio detallado de algunos aspectos necesarios
para implementar funciones recursivas en C, como son las consideraciones bsicas para crear dichos
subprogramas (seccin 2), el funcionamiento de la pila durante la ejecucin de una funcin recursiva
(seccin 3), el paso de parmetros a funciones de ese tipo (seccin 4), las ventajas e inconvenientes
de las soluciones recursivas frente a las iterativas (seccin 5), la conversin de una resolucin
recursiva en iterativa (seccin 6), y finalmente, el esbozo de algunas tcnicas de resolucin de
problemas basadas en recursividad (seccin 7).
Como ya hemos dicho anteriormente, la lnea lgica de desarrollo de un programa que implemente
una solucin recursiva a un problema, y en general cualquier tipo de solucin, sera disear el
algoritmo y posteriormente implementarlo, en nuestro caso en C, pero debido a la simplicidad de los
problemas que vamos a tratar inicialmente, stos se van a implementar directamente sin necesidad
de exponer explcitamente su algoritmo, aunque obviamente se comentar como se solucionarn los
problemas tratados.
15
Programacin II
A la hora de resolver recursivamente un problema, son cuatro las preguntas que nos
debemos plantear:
[P1] Cmo se puede definir el problema en trminos de uno o ms problemas ms
pequeos del mismo tipo que el original?
[P2] Qu instancias del problema harn de caso base?
[P3] Conforme el problema se reduce de tamao se alcanzar el caso base?
[P4] Cmo se usa la solucin del caso base parar construir una solucin correcta al
problema original?
Apliquemos esta tcnica de preguntas y respuestas para disear una funcin recursiva que
obtenga el valor del n-simo trmino de la secuencia de Fibonacci, que fue creada inicialmente para
modelar el crecimiento de una colonia de conejos. La secuencia es la siguiente: 1, 1, 2, 3, 5, 8, 13,
21, 34, 55, 89, 144, ...
Se puede observar que el tercer trmino de la sucesin se obtiene sumando el segundo y el
primero. El cuarto, a partir de la suma del tercero y el segundo. El problema es calcular el valor del nsimo trmino de la solucin, que se obtendr sumando los trminos n-1 y n-2.
16
Recursividad
17
Programacin II
Esta funcin posee dos peculiaridades: tiene ms de un caso base y hay ms de una llamada
recursiva, denominndose a este tipo recursin no lineal, en contraposicin a la recursin lineal
producida cuando slo hay una nica llamada recursiva. Generalmente, la recursin no lineal requiere
la identificacin de ms de un caso base.
Seguidamente vamos a estudiar tres ejemplos ms: el clculo del mximo comn divisor de
dos nmeros, contar el nmero de veces que aparece un valor en un vector y calcular el nmero de
distintas formas de seleccionar k objetos de entre un total de n. Con ello podremos afianzar la tcnica
de las cuatro preguntas.
Mximo comn divisor de dos nmeros m y n.
En los dos problemas anteriores, la solucin obtenida al encontrar el caso base sirve para
construir las soluciones a los problemas mayores que han derivado al caso base, pero hay ocasiones
donde esa solucin del caso base es la propia solucin del problema original. Este problema es un
ejemplo.
Se pretende implementar una funcin recursiva en C que calcule el mximo comn divisor
(MCD) de dos enteros m y n, no negativos, que es el mayor entero que divide a ambos.
El algoritmo de Euclides para encontrar el MCD de m y n es el siguiente: si n divide a m,
entonces MCD(m, n) = n, y en otro caso, MCD(m, n) = MCD(n, m mod n), donde m mod n es el resto
de la divisin de m por n (en C se representa con el operador %).
Apliquemos la tcnica de las cuatro preguntas para ver si esta definicin est correctamente
hecha:
[P1] Es evidente que MCD(m, n) ha sido definida en trminos de un problema del mismo tipo,
pero justifiquemos que MCD(m, n mod m) es de tamao menor que MCD(m, n):
1. El rango de m mod n es 0, ..., n-1, por tanto el resto es siempre menor que n.
2. Si m > n, entonces MCD(n, m mod n) es menor que MCD(m, n).
3. Si n > m, el resto de dividir m entre n es m, por lo que la primera llamada recursiva es
MCD(n, m mod n) es equivalente a MCD(n, m), lo que tiene el efecto de intercambiar los
argumentos y producir el caso anterior.
[P2] Si n divide a m, si y slo si, m mod n = 0, en cuyo caso MCD(m, n) = n, por tanto, se
conseguir el caso base cuando el resto sea cero.
[P3] Se alcanzar el caso base ya que n se hace ms pequeo en cada llamada y adems se
cumple que 0 m mod n n-1.
[P4] En este caso, cuando se llega al caso base es cuando se obtiene la solucin final al
problema, por lo que no se construye la solucin general en base a soluciones de problemas ms
simples.
Teniendo en cuenta las respuestas anteriores, la funcin en C que implementa el clculo del
mximo comn divisor es la siguiente:
int MCD (int m, int n)
{
if (m % n == 0) return n;
else return (MCD(n, m % n));
}
18
Recursividad
19
Programacin II
Para facilitar el entendimiento del problema, supongamos que tenemos un total de n personas
y queremos determinar cuntos grupos de k personas se pueden formar de entre las n personas en
total. La forma de resolucin recursiva que se ha elegido se puede interpretar como la suma de los
grupos que se pueden hacer de un tamao k sin contar a la persona A (ncsr(n-1,k)), ms todos los
que se puedan realizar incluyendo a esa misma persona, ncsr(n-1, k-1) (contamos todos los que se
pueden hacer con una persona menos y luego le aadimos esa persona).
Veamos el siguiente ejemplo: tenemos cuatro personas: A, B, C y D, y queremos determinar
cuntos grupos de dos personas podemos formar, o lo que es lo mismo, calcular ncsr(4,2). Este valor
se obtiene al aplicar la definicin: ncsr(3,1)+ncsr(3,2). Fijada la persona A, calculamos cuntos grupos
se pueden realizar con una persona menos, que en este caso es A (ncsr(3,1) ser igual a 3 ya que
podemos hacer 3 conjuntos de tamao 1:{B}, {C} y {D}) y se lo sumamos al nmero de comits que
se pueden hacer, tambin de tamao 2, pero sin A (ncsr(3,2) ser tambin 3 ya que podemos generar
{B,C}, {B,D} y {D,C}).
[P2] Los casos bases son los siguientes:
S
20
Recursividad
Cuando un programa principal llama a una rutina M, se crea en la pila lo que se denomina el
registro de activacin o entorno E, asociado al mdulo M. El registro de activacin almacena
informacin como constantes y variables locales del mdulo, as como sus parmetros formales.
Conforme se van llamando de manera anidada a las rutinas, se van creando sucesivos
registros de activacin, almacenndose de forma apilada: si M1 llama a M2 y a su vez, invoca a M3,
la pila estar formada por un primer registro de activacin correspondiente a M1, E1, sobre el que se
reservar espacio para el de M2, E2, y posteriormente para el de M3, E3, conviviendo los tres
entornos mientras se est ejecutando M3. Esta situacin queda representada grficamente en el
grfico de la figura 3.
Cuando finaliza la ejecucin del ltimo mdulo invocado, se devuelve el control a M2, y el
espacio ocupado por su entorno se libera, quedando ahora la pila compuesta slo por dos registros
de activacin. Al acabar M2 se elimina de la pila el entorno E2, devolvindose el control a M1 y
quedando slo el registro E1 correspondiente a ese ltimo mdulo, que desaparecer, a su vez,
cuando acabe su ejecucin.
Como se puede observar, este trozo de memoria recibe el nombre de pila por la forma en que
se gestiona, ya que en l se encuentran los entornos apilados en el orden en que han sido llamados,
de tal manera que el registro activo en cada momento es el que est situado en la cabecera de la pila,
ocupando as el lugar ms alto de esta hipottica pila. Esta forma de gestionar ese espacio de
memoria permite que el lenguaje de programacin correspondiente pueda utilizar la recursividad.
En la siguiente figura se introduce una notacin grfica para representar el registro de
activacin de un mdulo. El mdulo se representa mediante una rectngulo con cuatro divisiones: la
primera contendr el nombre del mdulo, la segunda el rea de almacenamiento de los argumentos
formales y sus valores, la tercera la zona donde aparecern las variables locales e igualmente sus
valores, y por ltimo, una cuarta zona, creada slo a efectos pedaggicos, donde aparecern algunas
sentencias que pueden ser interesantes para entender el funcionamiento del mdulo.
21
Programacin II
En la pila existirn tantos entornos de una misma funcin como llamadas recursivas se hayan
efectuado, siendo todos ellos independientes y distintos entre s. As, llamamos profundidad de
recursin de un mdulo recursivo al nmero de entornos que estn presentes en la pila en un
momento dado.
El concepto de profundidad se suele usar como elemento de decisin para optar por una
solucin recursiva de un problema, ya que problemas que requieran profundidades muy grandes
pueden originar que se llene la memoria del ordenador dedicada a la pila (desbordamiento de la pila).
Obviamente, que la profundidad de un mdulo recursivo sea pequea no indica que el algoritmo que
implemente sea eficiente. Como regla general, se evitar utilizar una solucin recursiva en aquellos
casos en los que la profundidad sea muy grande.
Seguidamente, y mediante un ejemplo, estudiaremos cmo evoluciona la pila durante las
diferentes llamadas recursivas. El problema que vamos a resolver en este caso es la impresin de n
palabras recibidas desde la entrada estndar en orden contrario al de entrada en la salida estndar:
desde la ltima palabra introducida hasta la primera. Por ejemplo, si se introdujera la frase "En un
lugar de la Mancha de cuyo nombre no quiero acordarme", la funcin recursiva imprimira: "acordarme
quiero no nombre cuyo de Mancha la de lugar un En".
Cabe destacar que en este problema, y al igual que ocurri en el que contaba las ocurrencias
de un valor en un vector, y en general en todos aquellos que impliquen un procesamiento de listas de
objetos, se procesar inicialmente el primer elemento de la lista y posteriormente y de forma
recursiva, el resto.
Comencemos a disear la funcin recursiva Imp_OrdenInverso, que recibir como argumento
el nmero de palabras que quedan por leer (n). Aplicaremos la tcnica de responder a las ya
conocidas cuatro preguntas:
[P1] Se lee la primera palabra, y recursivamente se llama a la funcin para que lea e imprima
el resto de palabras, para que finalmente se imprima la primera leda. El problema de leer n palabras
e imprimirlas hacia atrs se convierte en realizar el mismo proceso pero sobre n-1 palabras.
[P2] El caso ms simple en la impresin en orden inverso de una cadena ser cuando slo
quede una palabra (n == 1), en cuyo caso se imprimir directamente.
[P3] Como en cada llamada recursiva hay que leer una palabra menos del total, en n-1
llamadas se habr alcanzado el caso base.
22
Recursividad
[P4] Una vez se haya alcanzado el caso base (se haya ledo la ltima palabra y se imprima)
al devolver continuamente el control a las funciones invocantes se irn imprimiendo de atrs a delante
las palabras obtenidas desde la entrada estndar.
La implementacin en C de la rutina Imp_OrdenInverso es la siguiente:
void Imp_OrdenInverso(int n)
{
char Palabra[MAXTAMPAL];
if (n == 1)
{
scanf("%s", Palabra);
printf("%s", Palabra);
}
else
{
scanf("%s", Palabra);
Imp_OrdenInverso(n-1);
printf("%s", Palabra);
}
}
MAXTAMPAL es una constante declarada en algn fichero que albergar el valor del tamao
mximo de una palabra.
Retomemos el objetivo principal de esta seccin, para lo cual estudiaremos cmo evoluciona
grficamente la pila si llamramos a la funcin Imp_OrdenInverso con un valor del parmetro n igual a
5, representado grficamente por las dos siguientes grficos (figuras 5 y 6).
Al llamar a Imp_OrdenInverso(5), se crea el registro de activacin correspondiente, en el cual
se reserva memoria para almacenar el valor del parmetro actual n == 5 y la variable palabra. Se
comienza a ejecutar el cdigo de dicha funcin y tras comprobar que no se da el caso base, el
usuario introduce una primera cadena de caracteres, por ejemplo, "uno". A continuacin se produce la
llamada recursiva con una unidad menos en n (n==4). Encima del registro de la llamada inicial se
crea un nuevo registro de activacin, en este caso para la invocacin Imp_OrdenInverso(4) y se
reservan las zonas de memoria para los parmetros y las variables locales. Este proceso se repite
tres veces ms, encontrndose en el tope de la pila el registro correspondiente a la llamada
Imp_OrdenInverso(1), momento en el cual se cumple el caso base, n == 1, y se lee la ltima palabra y
seguidamente se imprime por la salida estndar. Este es el momento al que se corresponde la 5.
23
Programacin II
.
Figura 5. Evolucin de la pila con las llamadas recursivas.
Una vez finalizada la funcin, se elimina el registro de la pila, quedando en el tope la llamada
anterior, es decir, el caso de n==4. Para finalizar la ejecucin de esa funcin slo queda imprimir la
palabra leda (palabra == "cuatro") y se devolver el control a la funcin que la invoc, tras eliminarse
la zona de memoria reservada para alojar a esta funcin. El mismo proceso se repite para las
funciones que fueron invocadas con n == 3, 2 y 1 respectivamente. Al llegar el tope de la pila al
primer registro de activacin, se imprimir la palabra "una" y finalizar su ejecucin devolviendo el
control a la funcin main. En la salida estndar se habr escrito "cinco cuatro tres dos uno". El
vaciado de la pila segn van devolviendo el control las funciones recursivas corresponde a la figura 6.
24
Recursividad
Para finalizar, hacer hincapi en la correccin de los casos base como forma de evitar una
recursin infinita, y por tanto, que la pila se agote:
1.
Cada rutina recursiva requiere como mnimo un caso base, sin el cual se generarn
una secuencia de llamadas infinitas originando el agotamiento de la pila.
2.
Se debe identificar todos los casos base ya que en caso contrario se puede producir
recursin infinita.
3.
Los casos bases deben ser correctos, es decir, las sentencias ejecutadas cuando se
d un caso base deben originar una solucin correcta para una instancia particular del
problema. En caso contrario, se generar una solucin incorrecta.
4.
Hay que asegurarse que cada subproblema est ms cercano a algn caso base,
por lo que en repetidas llamadas se alcanzar alguno de stos.
Aconsejamos al lector para que llegue a comprender totalmente cmo funciona la pila del
ordenador, que implemente los ejemplos mostrados en este captulo y los ejecute paso a paso
utilizando un depurador. De esta forma, podr observar cmo va evolucionando la pila conforme se
producen las llamadas recursivas.
25
Programacin II
return (*n==0) * 1 = 0
return (*n==0) * 0
return (*n==0) * 0
fact= 0
En este caso, n no se puede pasar por referencia porque en la vuelta atrs de la recursividad
se va a utilizar el valor antiguo que tena antes de invocar a la funcin recursiva. La regla general para
decidir si un parmetro ser pasado por copia o por referencia es la siguiente: si se necesita recordar
el valor que tena un parmetro una vez que se ha producido la vuelta atrs de la recursividad,
entonces ha de pasarse por valor. En caso contrario, se podr pasar por referencia.
Obviamente, si convertimos una funcin en un procedimiento habr que introducir un nuevo
parmetro pasado por referencia para ir guardando el valor solucin en cada momento:
void factorial (long n, long *fact)
{
if (n == 0) *fact=1;
else
{
factorial(n-1, fact);
*fact = *fact * n;
}
}
26
Recursividad
Declarar algunas variables como globales, y por tanto, no pasarlas como parmetros
a la funcin.
b)
c)
Crear una funcin y dentro de ella implementar la funcin recursiva (la primera
recubrir a la segunda). Los parmetros se pasarn nicamente a la primera y sern
globales a la segunda, la cual no tendr parmetros formales o si los tiene, sern muy
pocos.
Aunque son soluciones posibles, para evitar efectos secundarios y escribir un cdigo lo ms
limpio posible, slo vamos a tomar como nica solucin vlida la b) olvidando completamente las dos
restantes en nuestras implementaciones.
Como resumen, habr que tener en cuenta que:
1.
2.
Si la rutina acumula valores en alguna variable o vector, ste debe ser pasado por
referencia. Cualquier cambio en dicho vector se repercutir en el resto de llamadas
recursivas posteriores.
3.
Aunque depende del tipo de lenguaje, si existen en l como mnimo el paso por
referencia y el paso por copia, como ocurre en C, los vectores y las estructuras de
datos grandes no se pasarn por copia ya que se requiere una mayor demanda de
memoria, si no que se pasarn por referencia y para evitar que se puedan modificar
de manera errnea, se utilizar la palabra reservada const si trabajamos en C, o
cualquier mecanismo de proteccin de los parmetros.
Programacin II
A pesar de estas ideas expresadas en el prrafo anterior, hay dos aspectos que influyen en la
ineficiencia de algunas soluciones recursivas, y que tras evaluar convenientemente debemos decidir
si son inconvenientes sustanciales como para decidir buscar una solucin iterativa al problema:
a) El tiempo asociado con la llamada a las rutinas es una cuestin a tener en cuenta,
ya que en cada llamada se aloja en la pila un nuevo entorno, con el consiguiente tiempo
adicional consumido en esta operacin. En una rutina iterativa la operacin se realiza slo
una vez y se puede considerar a este tiempo irrelevante con respecto a la ejecucin total de
la rutina, pero una simple llamada recursiva inicial puede generar un gran nmero de
llamadas posteriores, penalizando sustancialmente el tiempo final de ejecucin con el tiempo
total de creacin de los entornos.
En el momento de disear un algoritmo recursivo hay que decidir si la facilidad de
elaboracin del algoritmo merece la pena con respecto al costo en tiempo de ejecucin que
incrementar la gestin de la pila originado por el gran nmero de posibles llamadas
recursivas.
b) La ineficiencia inherente de algunos algoritmos recursivos. Por ejemplo, fijmonos
en el mdulo que resuelve la sucesin de Fibonacci. Si se trazan las llamadas recursivas de
fibonacci(6) podremos apreciar cmo fibonacci(4) se calcula dos veces como problemas
separados, fibonacci(3) se obtiene tres veces; fibonacci(2), cinco veces y fibonacci(1), tres. Es
de suponer, segn estas evidencias, que conforme aumente n, el nmero de clculos
repetidos que se realizarn ser mucho mayor, presentando una gran cantidad de
procesamiento duplicado que no es aprovechado para evitar llamadas recursivas posteriores.
As, es de especial importancia que la rutina recursiva implemente algn tipo de
tcnica que evite realizar procesamiento duplicado en diferentes partes de la ejecucin de la
rutina. Una solucin podra ser la utilizacin de algn tipo de estructura de datos adicional,
como por ejemplo un vector o una lista, que mantuviera toda la informacin calculada hasta el
momento, para evitar de esta forma, clculos redundantes.
Adicionalmente, y relacionado con la gestin de la pila en la recursividad, cabe destacar que
otro problema puede ser el planteado por la profundidad de la recursin, ya que un gran nmero de
llamadas recursivas almacenadas en la pila y pendientes a ser resueltas puede ocasionar que esta
zona de memoria se agote, pudiendo bloquearse el ordenador o abortndose la ejecucin del
programa. Por tanto, si el tamao del problema a resolver va a ser considerable, debemos
plantearnos el buscar una solucin iterativa.
Habiendo decidido que una solucin recursiva concreta tiene ms inconvenientes que
ventajas, hay dos alternativas a la hora de disear una rutina que resuelva un problema dado: buscar
un algoritmo puramente iterativo o, dada la rutina recursiva, intentar eliminar la recursividad,
manteniendo la idea bsica que sustenta dicha rutina recursiva. En la siguiente seccin se har un
breve estudio de esta segunda alternativa.
28
Recursividad
Programacin II
Se recomienda, para la perfecta compresin de esta seccin que el lector estudie detenidamente el
concepto de Tipo de Dato Abstracto Pila
30
Recursividad
Como hemos dicho anteriormente, se va a utilizar el T.D.A. Pila, del cual vamos a utilizar en
esta seccin las siguientes primitivas (se estudiarn con ms detalle en el captulo 4):
S
TPila crear() => Crea una pila y la deja lista para ser usada.
void destruir(TPila P) => Libera los recursos ocupados por una pila.
void push(TElemento x, TPila UnaPila) => Introduce en el tope de la pila UnaPila el valor de la
variable x.
TElemento tope(TPila UnaPila) => Devuelve el valor que ocupa el tope de la pila UnaPila.
void pop(Tpila UnaPila) => Elimina el elemento que est en el tope de la pila.
int vacia(TPila UnaPila) => Devuelve 1 si la pila UnaPila no tiene elementos y 0 en caso de que
no est vaca.
A continuacin, eliminaremos la recursividad de la ya conocida funcin ncsr:
int ncsr(int n, int k)
{
if (k > n) return 0;
else if ((n == k) || (k == 0)) return 1;
else return ncsr(n-1, k) + ncsr(n-1, k-1);
}
Obteniendo la siguiente funcin no recursiva:
int ncsr_nr(int n, int k)
{
tPila Pila;
int Suma=0;
Pila=crear();
push(n, Pila);
push(k, Pila);
while (!vacia(Pila))
{
k= tope(Pila);
pop(Pila);
n= tope(Pila);
pop(Pila);
if (k >n ) Suma += 0;
else if (k == n || k== 0) Suma +=1;
else
{
push(n-1, Pila);
push(k-1, Pila);
push(n-1, Pila);
push(k, Pila);
}
}
destruir(Pila);
return Suma;
}
31
Programacin II
Despus de crear la pila, las primeras sentencias con las que nos encontramos son las que
introducen en la pila los dos valores de los parmetros formales. Seguidamente hallamos un ciclo
while cuyo cuerpo se ejecutar mientras la pila no est vaca. Qu se hace dentro del ciclo? Se
recuperan los valores de n y k de la pila y se comprueban los casos bases. Si se cumplen se
actualiza la variable Suma con los valores que devolvera la rutina recursiva cuando se alcanzaran los
casos bases. En caso contrario, en la rutina recursiva se procedera a llamarse a s misma con
diferentes parmetros. Ahora, se introducen en la pila los valores de dichos parmetros (en nuestro
ejemplo, son cuatro los valores que se meten, correspondientes con los dos parmetros de las dos
llamadas recursivas) para posteriormente, cuando comience de nuevo el bucle, sacarlos y
procesarlos.
De manera general, dada una estructura recursiva bsica de la forma:
RutinaRecursiva(parmetros)
Si se han alcanzado los casos bases
Entonces
Realizar las acciones que correspondan a dichos casos bases.
Si no
Llamar recursivamente a la rutina actualizando convenientemente los parmetros de
dicha llamada.
La estructura bsica de la eliminacin de la recursividad es la siguiente:
RutinaNoRecursiva(parmetros)
Meter los parmetros y variables locales en la pila.
Repetir mientras no est vaca la pila
Sacar los elementos que estn en el tope de la pila
Si stos cumplen los casos bases
Entonces
Realizar las acciones que correspondan a dichos casos bases.
Si no
Meter en la pila los valores con los que se producira la(s) llamada(s)
recursiva(s).
Hay que tener en cuenta que, si los parmetros y las variables locales son de diferente tipo,
habr que tener tantas pilas como tipos haya para que alberguen los valores de las variables de esos
tipos.
32
Recursividad
Programacin II
34
Recursividad
El problema pareca fcil y fueron muchos los que lo intentaron, aunque nadie lo consegua.
El emperador estaba ya algo desesperado al no encontrar sucesor para el puesto de sabio, cuando
un monje budista se present ante l , dicindole que el problema era tan fcil , que casi se resolva a
s mismo. Esto sorprendi al emperador y ste le dijo que se lo explicara. El monje le dio
primeramente la solucin para un slo disco: mover el disco de A a B, para continuar seguidamente
con la solucin para el caso en que hubiera ms de un disco:
1. Solventar el problema para n-1 discos, ignorando el ltimo de ellos, pero ahora teniendo en
cuenta que el poste destino ser C y B ser el auxiliar (estado 3 de la figura 7).
2. Una vez hecho esto anterior, los n - 1 discos estarn en C y el ms grande permanecer en A.
Por tanto, tendramos el problema cuando n = 1, en cuyo caso se movera ese disco de A a B
(estado 4 de la figura 7).
3. Por ltimo, nos queda mover los n-1 discos de C a B, utilizando A como poste auxiliar (estado 5
de la figura 7).
Desde el punto de la filosofa de la tcnica "divide y vencers", el problema original se divide
en tres subproblemas de tamao menor, correspondiente a los tres puntos anteriores, dos de los
cuales se solventarn utilizando llamadas recursivas, y el que queda ser tan simple de resolver que
no hace falta una nueva invocacin recursiva.
Aplicando de manera directa los tres pasos que el monje indicaba, presentaremos la funcin
N-1
movimientos y que
recursiva TorresHanoi que implementa la solucin al problema en un total de 2
tiene como parmetros el nmero de discos con los que se resolver el problema (NumDiscos) y tres
caracteres que representan a cada poste (Origen, Destino y Auxiliar).
void TorresHanoi(int NumDiscos, char Origen, char Destino, char Auxiliar)
{
if (NumDiscos == 1)
printf("Mover el disco de arriba del disco %c al %c.\n", Origen, Destino);
else
{
TorresHanoi(NumDiscos-1, Origen, Auxiliar, Destino);
TorresHanoi(1, Origen, Destino, Auxiliar);
35
Programacin II
2.7.2 Backtracking.
Para comprender el funcionamiento de esta tcnica vamos a comenzar viendo cmo se
soluciona el "problema de las ocho reinas". Partimos de un tablero de ajedrez, el cual tiene un total de
64 casillas (8 filas x 8 columnas). El problema consiste en situar ocho reinas en el tablero de tal
forma que no se den jaque entre ellas. Una reina puede dar jaque a aquellas reinas que se siten en
la misma fila, columna o diagonal en la que se encuentra dicha reina. En la figura siguiente, podemos
observar una solucin al problema:
R
R
R
R
R
R
R
R
Figura 8. Una solucin para el problema de las 8 reinas.
La solucin a este rompecabezas pasa, por tanto, en situar una reina en cada fila y en cada
columna. Cmo actuaramos para situar las ocho reinas? Comenzaramos por la primera columna y
situaramos la primera reina. Al hacer esto, eliminamos como posibles casillas donde localizar reinas
la fila, la columna y las dos diagonales. Teniendo en cuenta estas restricciones, se sita en la
segunda columna la segunda reina y se "tachan" las nuevas casillas prohibidas. Seguidamente se
procede a poner la tercera reina y as sucesivamente.
Supongamos que se han situado las seis primeras reinas. Puede llegar un momento en el que
no se pueda situar la sptima sin que ninguna otra le d jaque. En ese momento, se deshace el
movimiento que ha ubicado la sexta reina y busca otra posicin vlida para dicha reina. Si no se
pudiera, se vuelve deshacer el movimiento de la sexta reina y se intenta buscar otra localizacin. En
el caso de que no sea posible, se sigue hacia atrs intentando colocar la reina quinta. Si se puede
colocar, entonces se proceder a dejar en el tablero la sexta de nuevo. Si se ha conseguido sin que le
den jaque, se intentar emplazar la sptima y, por ltimo, la octava. En general, si hay algn
problema, se deshace el ltimo movimiento y se prueba a localizar una casilla alternativa. Si no se
consigue, se vuelve hacia atrs y as sucesivamente.
sta tcnica de prueba-error o avance-retroceso, se conoce como bactracking (vuelta atrs).
En ella se van dando pasos hacia delante mientras sea posible y se deshacen cuando se ha llegado a
una situacin que no conduce a la resolucin del problema original.
36
Recursividad
Cmo se puede aplicar la recursividad para solventar este problema? De forma muy
sencilla: dado que se ha situado una reina en una posicin correcta en una columna, hay que
considerar el problema de situar otra reina en la columna siguiente: resolver el mismo problema con
una columna menos. En este momento realizaramos una llamada recursiva. El caso base? Cuando
el tablero se haya reducido a uno de tamao cero, en cuyo caso no se har nada. Por tanto, se partir
de un problema de tamao 8, y se considerarn problemas de tamao menor quitando una columna
en cada llamada recursiva hasta que se llegue a un tablero de tamao cero, momento en el cual
habremos alcanzado el caso base. Si se puede situar una reina en la columna correspondiente, se
hace una llamada recursiva, si no, se vuelve hacia atrs y se prueba otras posiciones.
Para implementar la funcin recursiva en C que encuentre una solucin para el problema de
las ocho reinas necesitaremos una estructura de datos para representar el tablero, para lo cual
utilizaremos una matriz de tamao 8 x 8 de enteros, donde un 1 en una posicin indicar que hay
situada una reina en ella y un 0, que no est ocupada.
int Tablero[8][8];
Inicialmente todos los elementos de la matriz bidimensional sern 0, indicando que no hay
ninguna reina en el tablero y ser global a la funcin recursiva, con objeto de ahorrar espacio en la
pila.
Las funciones auxiliares que nos ayudarn a resolver el problema y que no implementaremos
aqu por ser irrelevantes al tema que nos centra nuestra atencin, son:
S
void AsignarReinaA(int Fila, int Columa) => Sita un 1 en la casilla (Fila, Columna)
indicando que est ocupada por una reina.
void EliminarReinaDe(int Fila, int Columna) => Sita un 0 en la casilla (Fila, Columna)
indicando que esa casilla est libre (antes estaba ocupada por una reina y ahora deja de
estarlo).
int RecibeJaqueEn(int Fila, int Columna) => Devuelve 1 si la casilla (Fila, Columna)
recibe jaque de alguna reina y 0 en caso contrario.
La funcin recursiva que implementaremos ser void SituarReina(int Columna, int *Situada).
El parmetro formal Col indica en qu columna se quiere situar la reina, y Situada, pasado por
referencia, es un parmetro que tomar el valor 1 cuando se haya logrado ubicar correctamente a la
reina correspondiente, y 0 cuando el intento haya sido infructuoso. Columna se pasar por copia para
que, cuando se realice backtracking se pueda trabajar con el valor original de esta variable.
La llamada a esta funcin se hace con Col == 0 (primera columna de la matriz).
void SituarReina(int Columna, int *Situada)
{
int Fila;
if (Columna > 7) *Situada=1;
else
{
*Situada=0;
Fila=1;
while (!(*Situada) && (Fila <= 7))
if (RecibeJaqueEn(Fila, Columna)) ++Fila;
else
{
AsignarReinaA(Fila, Columna);
SituarReina(Columna+1, *Situada);
if ( !(*Situada))
{
EliminaReinaDe(Fila, Columna);
37
Programacin II
++Fila;
}
}
}
}
Bsicamente, se comprueba si se est en el caso base, en cuyo caso se indica que se ha
situado la reina en una casilla libre. En otro caso, se comenzar un bucle que iterar mientras no se
haya situado correctamente la reina dentro de la columna especificada en el parmetro, o nos
hayamos salido fuera del tablero, en cuanto a las filas se refiere. En el bucle se comprobar si en la
casilla (Fila, Columna) se puede situar la reina. Si no es as, se incrementa la fila para probar en otra
casilla de la misma columna. Si no recibiera jaque una reina situada en esa casilla, se asigna a la
posicin del tablero y se llama recursivamente a la funcin SituarReina para situar en la siguiente
columna otra reina. Cuando han finalizado las llamadas recursivas, se comprueba el valor de Situada
para ver si ha habido xito en el resto de intentos. En el caso de que no, se deshace la asignacin
anterior y se incrementa en una la fila para probar con otra casilla.
De forma sencilla se puede modificar esta funcin para que calcule, no slo una solucin
posible, si no todas las existentes, tarea que dejamos a cargo del lector.
2.8 Bibliografa.
[BB86] P. Berlioux, P. Bizard. Algorithms. The construction, proof and analysis of programs.
John Wiley and Sons (1.986).
[CHV88] F. Canorro, P. Helman, R. Veroff. Data abstraction and problem solving with C++.
nd
Walls and Mirrors. 2 Edition. Addison-Wesley (1.988).
[FT96] W. Ford, W. Topp. Data structures with C++. Prentice Hall International (1.996).
38
rd
Captulo 3: Anlisis
de la eficiencia de los
algoritmos.
3.1 Introduccin al anlisis de la eficiencia.
Cuando se nos propone realizar un programa para resolver un problema puede ser
interesante plantearnos el diseo de varios algoritmos, y de entre stos escoger el mejor. Pero
rpidamente surge la pregunta: cul es el mejor? con qu criterios se decide cundo un algoritmo
es mejor que otro?
Esta cuestin puede tener varias respuestas alternativas: aquel que consuma menos
memoria, o el que finalice la ejecucin ms velozmente, aquel que funcione adecuadamente (que
realice correctamente la misin para la cual ha sido diseado) o incluso el que sea ms fcil para un
humano de leer, escribir o entender.
Si nos centramos en las dos primeras, stas pueden ser cuantificadas ms fcilmente, el
consumo de memoria viene determinado por el nmero de variables y el nmero y el tamao de las
estructuras de datos usadas en el algoritmo. Por otro lado, la velocidad quedar cuantificada
determinando el tiempo que tarda en finalizar el programa o, alternativamente, calculando el nmero
de acciones elementales ejecutadas por un procesador.
A pesar de todo lo anteriormente comentado, y teniendo en cuenta los avances tecnolgicos
actuales podramos pensar: por qu deberamos preocuparnos por la calidad de un algoritmo, si lo
nico que tenemos que hacer para que un programa vaya ms rpido es utilizar un ordenador con
ms potencia, o si ste consume mucha memoria, aadirle algunos mdulos de memoria ms? De
hecho, es evidente que si un programa se ejecuta lentamente en una mquina determinada, si
mejoramos las prestaciones, lograremos que finalice ms rpidamente. Esta afirmacin no es del
todo correcta, ya que este aumento de las prestaciones no implica una clara mejora en el tiempo de
ejecucin.
Supongamos que un ordenador es capaz de ejecutar un programa cuya entrada son n datos
-4 n
en 10 2 segundos. Si n=10, el programa terminar en 0'1 segundos. Si fuera 20, aproximadamente
10 minutos; 30, un da y 40, un ao. Si cambiamos el ordenador que estamos usando por uno 100
veces ms rpido, resolveramos el problema con 45 datos en un ao, mejora que no es del todo
significativa. Slo se mejorara de manera importante si se modificara el diseo del programa,
sustituyendo este algoritmo anterior por uno ms eficiente.
Una vez que conocemos qu criterios se pueden usar para decidirnos por un algoritmo u otro,
la pregunta que nos planteamos seguidamente sera: cmo podemos realizar el estudio de la
eficiencia de un algoritmo? Son dos las lneas bsicas con las que podemos abordar dicho estudio:
Empricamente: programar los algoritmos y ejecutarlos varias veces con distintos datos
de entrada.
Programacin II
Otro problema que plantea la alternativa emprica es que no siempre es posible su utilizacin
ya que existen algoritmos que pueden ser comparados con esta tcnica slo cuando el nmero de
datos con los que se ejecutan es relativamente pequeo. Si dicho valor crece, aunque slo sea un
poco, se puede correr el peligro de que el tiempo de ejecucin crezca de manera exagerada. Un
ejemplo es el problema del viajante de comercio, que cosiste en encontrar una ruta que una un
nmero de ciudades pero con coste mnimo, por ejemplo en kilmetros a recorrer. Cuando el total de
poblaciones a unir es bajo, 10 o 20, el resultado lo obtendremos en unos pocos segundos, pero si ese
nmero se elevara a 100, ahora mismo no existira ningn ordenador capaz de encontrar la solucin
en un tiempo razonable.
Estos inconvenientes no se encuentran cuando se lleva a cabo un estudio terico, pues se
realiza independientemente de todos los factores anteriormente citados. El resultado de un anlisis
terico es genrico para cualquier tamao de la entrada y depende exclusivamente de las
instrucciones que componen el algoritmo y del citado tamao. La salida de este anlisis ser una
expresin matemtica que indique cmo se produce el crecimiento del tiempo que tardara en
ejecutarse el algoritmo conforme aumente el tamao de la entrada, cuestin que trataremos
detalladamente ms adelante.
Existe una tercera lnea a la que podramos calificar como hbrida entre emprica y la terica,
que consiste en hacer un estudio terico del algoritmo en cuestin y, posteriormente determinar los
parmetros numricos de las funciones matemticas obtenidas, que dependen de la implementacin
concreta, mediante tcnicas estadsticas, como puede ser la regresin.
En este captulo nos vamos a centrar exclusivamente en el estudio o anlisis terico de la
eficiencia de los algoritmos, y ms concretamente en su vertiente del tiempo, aunque se podrn
realizar las mismas consideraciones con respecto al consumo de memoria e incluso a otros recursos.
En general, a todos aquellos cuyo crecimiento, a igual que ocurre con el tiempo, dependa del tamao
del problema o de los datos de entrada, concepto al que ya hemos hecho referencia en prrafos
anteriores y que debemos clarificar, aunque sea de manera intuitiva para poder seguir adelante.
Supongamos que tenemos entre manos el estudio de la eficiencia de un algoritmo de
ordenacin de un vector de n enteros. En este caso, n es el tamao de los datos de entrada. Si ahora
debiramos estudiar la eficiencia de un algoritmo para el clculo del factorial de un entero, el valor de
ese nmero entero es el que nos determinara el tamao de nuestro problema. Dicho tamao puede
venir expresado no slo en una dimensin, como ocurre en los ejemplos anteriores, sino puede
depender de varias, como es el caso del viajante de comercio, ya que la eficiencia de un algoritmo
que lo resuelva vendr dada en funcin del nmero de poblaciones a visitar, y del nmero de
carreteras que unan dichas poblaciones.
De manera general, el tamao de la entrada de un problema vendr dado en funcin de un
nmero entero que nos mide el nmero de componentes de dicho ejemplo. Es el caso del tamao de
un vector, o por ejemplo, el propio valor que tiene ese ejemplo (el valor del entero al cual se le quiere
hallar su factorial). No existe una regla exacta para determinar el tamao de un problema, sino que
tendr que obtenerse segn las caractersticas de cada uno.
Tras esta introduccin, en la siguiente seccin definiremos el tiempo de ejecucin de un
algoritmo, as como su orden de eficiencia, para pasar a formalizar las notaciones asintticas en la
seccin 4. En la quinta seccin nos dedicaremos a describir cmo se hace el clculo del tiempo de
ejecucin de los algoritmos iterativos y la posterior consecucin de su orden de eficiencia, para pasar
a continuacin, a realizar la misma tarea pero en algoritmos recursivos en la ltima seccin de este
captulo.
40
Programacin II
42
Notaremos lg n como el logaritmo en base dos de n (log2 n), y ln n como el logaritmo en base e de n
(loge n). Cuando nos d igual la base, se indicar simplemente log n.
43
Programacin II
En general, suele admitirse como algoritmo eficiente aquel que alcanza como mucho un coste
quasilineal. El mejor orden es el logartmico, ya que al doblar el tamao del problema apenas afecta
al tiempo de ejecucin, mientras que doblar el tiempo disponible permite tratar problemas enormes en
relacin con el original. Por otro lado, el quasilienal y el lineal presentan como caracterstica que al
duplicar el tamao del problema se duplica aproximadamente el tiempo empleado, y anlogamente, al
duplicar el tiempo se pueden tratar problemas aproximadamente del doble de tamao. En el caso de
las funciones las polinmicas, al multiplicar el tiempo disponible por un factor c en un orden del tipo
k
1/k
n , multiplica el tamao del problema que puede tratar por un factor c , con lo cual necesitamos
mucho tiempo para resolver un problema que ha crecido relativamente poco en tamao. A pesar de
esto, los algoritmos que resuelven un problema en tiempo polinomial, se consideran manejables.
Cualquier coste superior al polinmico es un coste que se califica como intratable, incluso para casos
de tamao moderado, ya que exigen unos recursos prohibitivos. En la siguiente tabla se puede
observar el crecimiento de algunas funciones conforme crece el tamao n del problema.
log n
nlog n
10
33
100
1.000
1.024
100
664
10.000
1.000.000
1'267650600228e+30
1.000
9.966
1.000.000
1.000.000.000
1'071508607186e+301
10.000
132.877
100.000.000
1'0e+12
Enorme
100.000
1.660.964
10.000.000.000
1'0e+15
Ms enorme
10
1.000.000
19.931.569
1.000.000.000.000
1'0e+18
Sin comentarios
44
45
Programacin II
T(n)= 1.000n + 100n - 6 O(n ), por que existen n0= 100, y c0=1.000 que hacen que se
2
2
cumpla que 1.000n + 100n - 6 1.001n .
n
T(n) = 6 2 + n O(2 ) debido a que se pueden encontrar dos constantes n0= 4, y c0=7 que
n
2
n
hacen que 6 2 + n 7 2 .
n
T(n) = 3 O(2 ). Supongamos que existen dos constantes n0 y c0 tales que para todo nn0,
n
n
n
se tiene que 3 c02 . Entonces c0 (3/2) para cualquier valor nn0, pero esta desigualdad anterior
n
no se verifica nunca ya que no existe ninguna constante suficientemente grande que (3/2) para todo
n.
Como ya dijimos anteriormente, hay problemas, como son los que tratan con grafos, en los
que el tiempo de ejecucin depende de ms de un parmetro. La notacin asinttica del orden
+
generalizada a dos parmetros es la siguiente: sea f:N x NR {0} una funcin cualquiera, el
conjunto de las funciones del orden de f(n, m), notado como O(f(n, m)), se define como sigue:
+
f O(f) [Reflexividad]
Mencin especial merecen las conocidas como reglas de la suma y del producto, ya que son
bsicas para el anlisis de la eficiencia de un algoritmo:
Demostracin:
Si g(n) O(f(n)) entonces c1, n1 / g(n) c1f(n),n n1.
Si h(n) O(f'(n)) entonces c2, n2 / h(n) c2f'(n), n n2.
n max(n1, n2) se tiene que f(n)+f'(n) (c1 + c2) max(f(n),g(n)).
46
Esta regla nos asegura que si se dispone de dos trozos de cdigo independientes,
uno con eficiencia O(f(n)) y otro con O(f'(n)), la eficiencia del trozo completo ser O(max(f(n),
f'(n)). Por ejemplo, dados los rdenes de eficiencia de tres trozos consecutivos de un
2
3
2
3
programa. O(n ), O(n ) y O(nlgn), el tiempo de ejecucin de los tres ser O(max(n , n ,
3
nlgn))= O(n ).
Demostracin:
Si g(n) O(f(n)) entonces c1, m1 / g(n) c1f(n), n n1.
Si h(n) O(f'(n)) entonces c2, m2 / h(n) c2f'(n), n n2.
n n1 n2 se tiene que f(n) f'(n) (c1 c2) f(n) g(n).
Esta regla nos asegura que si existen dos trozos de cdigo anidados (no
independientes), uno con eficiencia O(f(n)) y otro O(f'(n)), la eficiencia del trozo completo es
O(f(n)*g(n)). En este caso, si el trozo de cdigo ms interno es O(lgn), y el ms externo posee
O(n), el del cdigo completo pertenecer a O(nlgn).
La notacin o minscula es una cota superior, aunque diferente de O(f(n)): si g(n) o(f(n)),
indica que el crecimiento de g(n) es estrictamente ms lento que el de f(n), al contrario que ocurra
con O( ), en donde en algn momento una constante por f(n) llega a ser una cota superior para g(n).
Su definicin es la siguiente:
+
Las notaciones .
De igual forma que las notaciones O y o se encargan de establecer cotas superiores, las
notaciones nos dan una cota inferior en el orden de eficiencia (el mejor caso). Existen dos: k y ,
cuyas definiciones respectivas son las siguientes:
+
En este caso, si el lmite cuando n tiende a infinito de g(n)/f(n) es infinito, entonces g(n)
47
Programacin II
Las notaciones .
Esta notacin define las funciones con la misma tasa de crecimiento (crecen al mismo ritmo)
que f, es decir, (f) = O(f) k(f). Formalmente:
+
El objetivo final que vamos buscando se puede alcanzar de dos formas diferentes:
Sentencias simples.
Cualquier sentencia simple (lectura, escritura, asignacin, ...) tardar un tiempo O(1), ya que
el tiempo constante que tarda realmente la ejecucin de esa sentencia se puede acotar por una
constante que multiplica a la funcin f(n)=1. Esto podremos hacerlo siempre y cuando no intervengan
en dicha sentencia ni variables estructuradas,ni operandos aritmticos que dependan del tamao del
problema.
Veamos un ejemplo de un programa muy sencillo:
/*1*/ void main()
/*2*/ {
/*3*/ int a, b, c;
/*4*/ printf("Introduzca los dos nmeros a sumar: ");
/*5*/ scanf("%i %i", &a, &b);
/*6*/ c= a+b;
/*7*/ printf("\n La suma de %i y %i es %i.\n", a, b,c);
/*8*/ }
Este programa est compuesto slo por sentencias sencillas (slo intervienen en el clculo
las sentencias que van de la 4 a la 7), por lo que su orden de eficiencia va a ser, utilizando la regla de
la suma, el orden constante, es decir, O(1). Veamos por qu:
Hemos dicho anteriormente que las escrituras, salidas y asignaciones tienen todas ellas O(1),
por tanto, el orden del programa completo ser O(1+1+1+1) = O(max(1,1,1,1))= O(1); La otra forma
de llegar al mismo resultado sera la siguiente: T(n)= te+tl+toa+te= c, siendo te el tiempo constante que
lleva ejecutar una escritura; tl, el de la lectura y toa el que se tarda en hacer una operacin aritmtica
ms una asignacin. Es fcilmente comprobable que T(n)=c O(1).
En este ejemplo anterior, hemos visto que, para calcular el orden de un bloque de sentencias,
se aplica la regla de la suma, tomando el mximo de los ordenes de eficiencia de dicho bloque. Esta
forma de proceder ser independiente de los tipos de sentencias de que conste el bloque.
Sentencias de repeticin.
Fijmonos primeramente en los bucles controlados por contador. Cuntas veces se repite un
bucle de este tipo? En estas sentencias iterativas es muy fcil de determinarlo, ya que los lmites
estn expresados en el mismo bucle, y slo queda realizar una resta de ambos lmites. Una vez
conocido ese valor, bastara con multiplicarlo por el tiempo que tardara en ejecutarse una nica
iteracin del cuerpo del bucle para calcular el tiempo que tarda el bucle completo, sumndole
posteriormente el tiempo de evaluacin de la condicin, ms el de incremento de la variable
contadora.
En el siguiente bucle:
/*1*/ for (i=0; i<n;i++)
/*2*/
49
Programacin II
La sentencia printf tiene un tiempo de ejecucin te. Como el ciclo se repite n veces,
tendramos que T(n) =tc + nte O(n), siendo tc la constante que identifica el tiempo de evaluacin de
la condicin ms el incremento.
El cdigo que se muestra a continuacin, la inicializacin de una matriz cuadrada, nos servir
para ejemplificar cmo se debe aplicar la regla del producto para obtener su orden de eficiencia:
/*1*/ for (i=0; i<n;i++)
for (j=0; j<n;j++)
/*2*/
/*3*/
Matriz[i][j]= 0;
La asignacin de la lnea 3 tiene un orden de ejecucin constante; el bucle de la lnea 2 se
repite n veces, por lo que posee O(n), igual que ocurre con el ciclo de la primera lnea (O(n)).
Aplicando en este caso la regla del producto, llegamos a la conclusin que el tiempo de ejecucin de
2
estos dos ciclos anidados pertenece a O(n ).
Obteniendo el tiempo de ejecucin del bloque completo tendramos: T2-3(n) = nta + tc, para las
2
2
sentencias 2 y 3, y T1-3(n)= n(nta+tc) +tc = tan +tcn +tc O(n ) para las tres juntas.
Si la matriz no hubiera sido cuadrada, es decir, si la dimensin fuera n x m, en este caso, el
orden de eficiencia sera O(n m), siendo un ejemplo del caso en el que se tienen ms de dos
parmetros como tamao de la entrada.
Nos centraremos ahora en los bucles controlados por centinela, donde se opera de manera
anloga a como se ha hecho con el tipo de bucle anterior: se obtiene el tiempo de ejecucin del
interior del bucle, y posteriormente se multiplica por el nmero de iteraciones que se realizan.
En el trozo de cdigo que se muestra a continuacin, el cual busca la posicin en la que se
encuentra un valor dentro de un vector, supondremos el peor caso, es decir, que se tenga que
recorrer todo el vector:
/*1*/ i=0;
/*2*/ while (i<n && Vector[i] != valor)
/*3*/
i++;
As, el bucle while repetir n veces una sentencia de O(n), por lo que su orden de eficiencia
ser n. En este caso tambin despreciamos el tiempo constante de la evaluacin de la expresin del
bucle. Si convirtiramos en un bucle do...while el anterior, la forma de proceder sera la misma.
Sentencias condicionales.
En este caso, el tiempo de ejecucin ser el tiempo de evaluacin de la condicin, ms el
mximo del bloque a ejecutar cuando la condicin se evale como verdadera y del bloque a ejecutar
cuando se evale como falsa. Cuando estamos tratando con rdenes, el de la evaluacin de la
expresin booleana, que es constante, se desprecia. As, si el bloque then tiene una eficiencia O(f(n))
y el bloque else O(g(n)), el orden de eficiencia de la sentencia if ser O(max(f(n), g(n)).
En la siguiente sentencia if:
/*1*/ if (n> m)
/*2*/ n= n *m;
/*3*/ else
/*4*/ for (i=0; i<n;i++)
/*5*/
m= m * n;
50
Como hemos visto anteriormente, el bucle for tiene un tiempo de ejecucin que pertenece a
O(n), que coincide con el bloque de sentencias de la parte else del condicional. Por otro lado, el
bloque de la parte then posee O(1), por lo que el orden de la sentencia condicional ser el mximo de
1 y n, que claramente es n.
En el caso de hacer el clculo obteniendo el tiempo de ejecucin, ste sera T(n) = te +
max(tea, ntea)= te + ntea O(n), donde te corresponde con el tiempo que se tardara en hacer la
evaluacin de la condicin del if, tea, el correspondiente a la asignacin ms la suma. El bucle ejecuta
n veces una asignacin, por lo que su tiempo de ejecucin ser ntea (en esta constante tambin
incluimos el tiempo de gestin del bucle). Finalmente, el T(n) de la sentencia condicional ser el
mximo de ambos tiempos ms el tiempo de evaluacin.
Llamadas a funciones.
Si en un programa se llama a una o varias funciones no recursivas, se deber calcular el
tiempo de ejecucin de cada una de ellas, comenzando por aquellas que no llaman a otras. Una vez
hechos estos clculos iniciales se procede a calcular el tiempo de ejecucin de las funciones que s
contienen invocaciones a otras rutinas, utilizando para ello los tiempos de cada una de las funciones
que ya han sido calculados.
Supongamos que un programa en C llama en dos lugares distintos a una funcin HacerAlgo1
y a otra HacerAlgo2. A su vez, la segunda funcin invoca a una tercera, HacerAlgo3. Para poder
calcular el tiempo de ejecucin del programa principal debemos calcular el tiempo de cada una de las
funciones que son referenciadas. As, HacerAlgo1 no invoca a ninguna, con lo que procederamos a
obtener su eficiencia directamente. Seguidamente nos disponemos a hacer lo mismo para
HacerAlgo2, pero sta s realiza una llamada a otra funcin, por lo que para determinar su orden de
eficiencia, debemos previamente encontrar el de HacerAlgo3. Con esta funcin no hay ningn
problema, y tambin se puede hacer directamente. Una vez realizada esta tarea, procedemos a
incorporar el orden de eficiencia de HacerAlgo3 en los clculos para obtener el de HacerAlgo2, y
finalmente, obtenemos el de la funcin main basado en las llamadas a HacerAlgo1 y HacerAlgo2.
Dependiendo de dnde estn situadas las llamadas a funciones, se deber incluir su tiempo
de ejecucin de una u otra manera:
51
Programacin II
Ejemplos.
En esta seccin vamos a calcular el orden de eficiencia de varios programas. Para ello,
aplicaremos las reglas explicadas en los apartados anteriores.
Comencemos por la funcin Burbuja, la cual ordena un vector de n nmeros enteros de forma
creciente mediante el mtodo de la burbuja. Su cdigo es el siguiente:
void Burbuja (int *Vector, int n)
{
int i, j;
/*1*/ int aux;
/*2*/ for (i = 0; i < n - 1; i++)
/*3*/ for (j = n-1; j< i; j--)
if (Vector[j-1] > Vector[j])
/*4*/
/*5*/
{
/*6*/
Aux= Vector[j-1];
/*7*/
Vector[j-1]= Vector[j];
/*8*/
Vector[j]= Aux;
/*9*/
}
}
Comenzamos el estudio por el bloque de cdigo ms interno, para posteriormente ir
ascendiendo. As, nos vamos al segundo bucle for, y dentro de l nos encontramos una sentencia if
con una bloque then, el cual est formado por tres sentencias simples (lneas 6, 7 y 8), y por tanto,
cada una ellas llevar O(1). Al aplicar la regla de la suma, se obtiene igualmente O(1). Seguimos
avanzando hacia fuera, para obtener seguidamente el orden de eficiencia de la sentencia condicional
de la lnea 4. En este caso, no sabemos si se llegar a ejecutar el cuerpo then del if, pero como
buscamos el peor caso, lo tenemos en cuenta, en cuyo caso el if se ejecutar en O(1). Continuamos
hacia fuera, y encontramos el for de la lnea 3. Cada iteracin tomar un tiempo perteneciente a O(1),
y al repetirse el ciclo n-i veces, tendremos que el cdigo de las lneas 3 a 9 tendr una eficiencia
O((n-i) * 1) = O(n-i). Finalmente nos encontramos con el bucle ms externo: en este caso, la lnea 2
ejecutar n-1 veces, por lo que el tiempo de ejecucin del bucle ser una constante multiplicada por:
n 1
(n i) =
i =1
n(n 1) n 2 n
=
2
2 2
2
que pertenece a O(n ). Por tanto para ejecutar la funcin Burbuja se necesita un tiempo
proporcional al cuadrado del nmero elementos que se desea ordenar.
El siguiente ejemplo que vamos a estudiar es el problema de la suma de la subsecuencia
mxima, el cual consiste en encontrar una secuencia de nmeros consecutivos almacenados en un
3
vector de tamao n cuya suma sea mxima . Por ejemplo, dado el vector -2, 11, -4, 13, -5, -2, el valor
mximo que se puede conseguir al sumar varios elementos consecutivos del vector es 20 y se
alcanza en 11, -4, 13, -5.
Una posible implementacin en C es la siguiente:
En las implementaciones que vamos a estudiar y para facilitar el entendimiento de las expresiones a
la hora de calcular la eficiencia, supondremos que los valores se sitan a partir de la posicin primera
del vector. Adems, vamos a consierar las constantes igual a 1 para simplificar las operaciones.
52
T (n) =
lnea 6. La suma de ms a la derecha, que representa el bucle de la lnea 6, ofrece como resultado ji+1. Si resolvemos la siguiente sumatoria, que se corresponde con el bucle de la lnea 3,
obtendremos:
n
( j i + 1) =
j =i
(n i + 1)(n i + 2)
, y por ltimo, realizamos los clculos sobre la primera
2
i =1
(n i + 1)(n i + 2) 1
3
1
=
i 2 n + i + (n 2 + 3n + 3) 1 =
2
2 i =1
2
2
i =1
i =1
1 n(n + 1)(2n + 1)
3 n(n + 1) n 2 + 3n + 2
n 3 + 3n 2 + 2n
+
O( n 3 )
n +
n=
2
6
2 2
2
6
Programacin II
54
i 1
( n j ), n i + 1 .
j =0
n 2
(n j )
j =0
n2
(n j ) =
i =0
(n + 1)n
O( n 2 )
2
Hemos visto, por tanto, que un programa recursivo generar una recurrencia que describe su
tiempo de ejecucin en funcin de ella misma, y que para obtener su eficiencia se debe resolver. Es
en este aspecto en el que nos vamos a centrar a continuacin: en los diferentes mtodos de
resolucin de recurrencias, ya que no siempre se pueden expandir los tiempos de ejecucin de
manera tan sencilla.
55
Programacin II
[1]
donde los ti son los valores buscados (aplicamos el adjetivo de lineal por que no hay trminos
2
de la forma titi+j ti , por ejemplo), los coeficientes ai son constantes y la recurrencia es homognea
por que la combinacin lineal de los ti es igual a 0.
n
Las soluciones que buscamos son de la forma tn=x , donde x es una constante. Si sustituimos
esta solucin en [1] obtenemos:
n
a0x + a1x
n-1
+ ... + ak x
n-k
=0
[2]
k
k-1
Esta ecuacin tiene dos soluciones x=0, que no nos sirve, y a0x + a1x + ... + ak= 0,
ecuacin que se denomina ecuacin caracterstica asociada a la ecuacin recurrente inicial.
Supongamos que las k races de esta ecuacin caracterstica son r1, r2, ..., rk, entonces cualquier
k
combinacin lineal
tn =
c r
i i
de trminos ri
i =1
homognea.
Clarifiquemos esta explicacin terica anterior con la resolucin de la siguiente recurrencia:
0, si n = 1
=
(
)
1, si n = 1
T n
3T (n 1) + 4T (n 2), si n 2
El siguiente paso ser hacer la sustitucin tn=x , ya que es la forma de la solucin que
buscamos:
n
x - 3x
n-1
n-2
=0.
n-2
- 4x
n-k
x -3x-4=0.
A continuacin, se resuelve la ecuacin obteniendo como races -1 y 4. La solucin general a
la expresin recursiva tendr la forma de la expresin [2], donde los ri son las soluciones de la
recurrencia.
n
Las constantes se obtienen a partir de las condiciones iniciales. Si sustituimos n=0 y n=1 en la
ecuacin anterior, llegaremos al siguiente sistema de dos ecuaciones con dos incgnitas (en este
caso c1 y c2):
c1 + c2 = 0 y -c1 + 4c2 =1
La solucin es c1= -1/5 y c2= 1/5, valores que se substituirn para llegar a la ecuacin del
n
n
n
tiempo de ejecucin sin recurrencias: tn= (1/5)(4 - (-1) ) O(4 ).
Para afianzar el clculo, veamos otro ejemplo sobre la recurrencia que corresponde al tiempo
de ejecucin de la sucesin de Fibonacci:
tn= tn-1 + tn-2, n2, y t0= 0, t1= 1
56
n-k
n-2
se obtiene la ecuacin
1/2
Si resolvemos la ecuacin, sus soluciones son r1= (1+5 )/2 y r2=(1-5 )/2. Sustituyendo las
n
n,
condiciones iniciales n=0 y n=1, en la expresin que nos da la solucin, tn=c1r1 +c2r2 , obtenemos el
sistema de dos ecuaciones con dos incgnitas:
c1 + c2 = 0 y c1r1 + c2r2 = 1.
1/2
1/2
Al solucionarlo, las incgnitas toman los valores c1=1/(5 ) y c2=-1/(5 ). Finalmente, nos
1/2 n
quedara sustituir c1 y c2 en la expresin de la solucin de la recurrencia. Al final T(n) O((1/(5 )) )
En los dos ejemplos anteriores, las races de la ecuacin caracterstica han sido todas
distintas, pero puede darse el caso en el que se repitan, como ocurre en el siguiente ejemplo:
Dada la recurrencia:
tn= 5tn-1 - 8tn-2 + 4tn-3, n3, t0=0, t1=1, t2=2
La ecuacin caracterstica es:
3
x - 5x + 8x - 4= 0
Tras resolver la ecuacin anterior, las soluciones son 1, con multiplicidad 1, y 2, con
2
multiplicidad 2, es decir, (x-1)(x-2) =0. La solucin general ser, por tanto:
n
Esta expresin se crea igual que la expresin [2] para las races simples, aadindole tantos
sumandos como multiplicidades tengan las races mltiples: si m es la multiplicidad de una raz r,
n
n
2 n
m-1 n
entonces se aadirn los sumandos r , nr , n r ,..., n r , multiplicados por sus correspondientes
constantes. A partir de este momento, todo se desarrolla igual: dadas las condiciones iniciales, se
plantean las ecuaciones de un sistema lineal, que en el ejemplo tendr tres ecuaciones con tres
incgnitas:
c1 + c2= 0, c1 + 2c2 + 2c3= 0 y c1 + 4c2 + 8c3= 0,
Las soluciones son c1= -2, c2= 2 y c3=-1/2, para finalmente encontrar la expresin de la
solucin a la recurrencia:
n+1
tn= 2
n-1
- n2
- 2 O(2 ).
(a0x + a1x
k
k-1
+...+ ak)(x-b)
d+1
=0 [4]
k-1
d+1
Donde (a0x + a1x +...+ ak) es la aportacin de la parte homognea y (x-b) de la parte no
homognea. Una vez en este punto, se concluye el proceso dando los mismos pasos que en la
solucin de recurrencias homogneas.
El siguiente ejemplo nos permitir identificar cada una de las funciones que componen la
expresin [3]:
tn= 1 + tn-1 + tn-2 => tn - tn-1 - tn-2 = 1
57
Programacin II
n
Como 1= 1 p(n), siendo p(n)=1 con grado d=0 y b=1. Esto implica que la ecuacin
caracterstica quedara:
2
(x -x-1)(x-1)=0
Un segundo ejemplo:
n
tn - 2tn-1 = 3
En este caso, b=3, p(n) = 1 y, por tanto, d=0, obteniendo la ecuacin caracterstica:
(x-2)(x-3)=0
Si la ecuacin recurrente fuera:
n
tn - 2tn-1= (n+5)3
En este caso, b=3, p(n)= n+5, con d=1. La ecuacin caracterstica sera:
2
(x-2)(x-3) =0
Veamos un ltimo ejemplo, esta vez completo. La recurrencia ser: T(n) = 2T(n-1) + n, n 1 y
T(0) = 0. Cambiando la notacin tendremos la siguiente ecuacin lineal no homognea:
tn - 2tn-1 = n
Identificamos b=1 y p(n)=n, siendo d=1, con lo que se obtiene la ecuacin caracterstica
siguiente:
2
(x-2)(x-1) = 0
Por tanto, la solucin es:
n
n+1
tn =2 2 - n -2 = 2
n+1
- n - 2 O(2
(a0x + a1x
k-1
d1+1
(x - b2)
d1+2
...= 0 [6]
n
n + 2n = b1 p1(n) + b2 p2(n)
Cosa que conseguiremos si identificamos b1= 1, p1(n)= n (grado d=1) y b2= 2 , p2(n)= 1 (d=0).
De esta manera, podemos obtener la ecuacin caracterstica:
2
(x-2)(x-1) (x-2) = 0
Su solucin tiene la forma:
n
1, n = 1
Debido a que el tamao del problema se divide en dos suponemos que n es potencia de dos,
m
por lo que podemos hacer n=2 , quedando:
m
m-1
T(2 )= T(2
m-1
T(2 )= T(2
m-2
) + 1 =T(2
m-3
) + 1 + 1 = T(2
) + 1 + 1 + 1,
y en general:
m
m-i
T(2 )= T(2 ) + m = m + 1
Como m= lg n, desaciendo el cambio, finalmente concluimos que T(n)= lgn + 1 O(lgn).
Un segundo ejemplo donde n es una potencia de 2, corresponde a la resolucin de la
recurrencia:
2
T(n)= 4T(n/2) + n
m-1
T(2 )= 4T(2
)+4 ,
O lo que es lo mismo:
m
tm= 4tm-1 + 4
La ecuacin caracterstica es:
2
(x-4) =0
Y la solucin buscada tendr la forma:
m
Deshaciendo los cambios, el tiempo de ejecucin es T(n)= c1n + c2n lg n O(n lg n).
3.9 Bibliografa.
[AHU87] A.V. Aho, J.A. Ullman. Data structures and algorithms. Addison-Wesley (1.992).
59
Programacin II
60
[MS91] B. M. E. Moret, H.D. Shapiro. Algorithms from P to NP. Volume I. Design and
eficiency. Benjamin/Cummings (1.991).
[PB84] P.W. Purdon, C. A. Brown. The analysis of algorithms. CBS College Publishing
(1.984).
[Smi89] J.D. Smith. Design and analysis of algorithms. PWS-KENT publishing company
(1.989).
nd
rd