Anda di halaman 1dari 48

Captulo 2: Recursividad

2.1 Introduccin a la recursividad.


La recursividad es una tcnica ampliamente utilizada en matemticas, y que bsicamente
consiste en realizar una definicin de un concepto en trminos del propio concepto que se est
definiendo. Veamos algunos ejemplos:

Los nmeros naturales se pueden definir de la siguiente forma: 0 es un nmero natural y el


sucesor de un nmero natural es tambin un nmero natural.

El factorial de un nmero natural n, es 1 si dicho nmero es 0, o n multiplicado por el factorial


del nmero n-1, en caso contrario.

La n-sima potencia de un nmero x, es 1 si n es igual a 0, o el producto de x por la potencia


(n-1)-sima de x, cuando n es mayor que 0.

En todos estos ejemplos se utiliza el concepto definido en la propia definicin, es decir, un


nmero natural se define en trminos de los naturales anteriores a l, o el factorial de un nmero se
establece en funcin del factorial de otro nmero.
La recursividad es una tcnica de resolucin de problemas muy potente ya que muchos
problemas que a primera vista parecen poseer una solucin difcil, son resueltos de manera sencilla y
elegante, incluso inmediata de forma recursiva. Este mtodo divide el problema original en varios ms
pequeos que son del mismo tipo que el inicial, procediendo seguidamente a encontrar la solucin de
estos subproblemas, soluciones en las que se basar la del problema inicialmente planteado. Para
ello, realiza de nuevo las correspondientes divisiones en problemas ms pequeos an, hasta que
stos tengan un tamao tan pequeo para que su solucin se conozca de forma directa. Una vez
conocidas estas soluciones, podremos ir resolviendo sucesivamente los problemas ms grandes
hasta llegar a la solucin buscada del problema primeramente planteado. Es de vital importancia que
exitan dichas soluciones a los problemas ms sencillos, por que nos permitirn, a partir de ellas, ir
resolviendo los problemas ms complejos.
Para ilustrar el funcionamiento de la resolucin de problemas mediante la recursividad
indicado en el prrafo anterior, supongamos que se pudiera resolver un problema P conociendo la
solucin de otro problema Q que es del mismo tipo que P, pero ms pequeo. Igualmente,
supongamos que pudiramos resolver Q mediante la bsqueda de la solucin de otro nuevo
problema, R, que sigue siendo del mismo tipo que Q y P, pero de un tamao menor que ambos. Si el
problema R fuera tan simple que su solucin es obvia o directa, entonces, dado que sabemos la
solucin de R, procederamos a resolver Q y, una vez resuelto, finalmente se obtendra la solucin
definitiva al primer problema, P.
El tpico problema que clarifica lo descrito en el prrafo anterior es el clculo del factorial de
un nmero, por ejemplo 5. Calcular 5! se basa en multiplicar 5 por 4!, por tanto, el obtener el valor de
5! depender de cunto valga 4! (se ha reducido el tamao del problema en una unidad). Nuestro
nuevo problema es ahora calcular 4! que, aplicando de nuevo la definicin, es 4! = 4 * 3!. As, para
calcular 4! debemos hallar 3!. Si seguimos descomponiendo el problema en otro ms pequeo de
manera sucesiva tendremos que 3! es 3 * 2!, 2! = 2 * 1! y 1! = 1 * 0!. En la definicin recursiva del
factorial se dice que el factorial de 0 es 1, por tanto, hemos encontrado la solucin al caso ms
sencillo. Con esta solucin podemos ir hacia atrs construyendo las de los problemas ms complejos:
0! = 1
1! = 1 * 0! = 1
2! = 2 * 1! = 2
3! = 3 * 2! = 6
4! = 4 * 3! = 24
5! = 5 * 4! = 120

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).

2.2 Diseo de mdu los recursivos.


Comencemos clasificando a las funciones recursivas. Si una funcin F contiene una llamada
explcita a s misma, entonces se dice que es recursiva de forma directa, pero si F posee una
referencia a otra funcin Q, que a su vez contiene una llamada a F, entonces F es recursiva de
forma indirecta.
La primera implementacin de una funcin recursiva que vamos a tratar es el factorial de un
nmero, la cual se obtiene de manera directa a partir de la propia definicin del factorial (en este caso
es una funcin recursiva directa):
long factorial (long n)
{
if (n == 0) return 1;
else return n * factorial(n-1);
}
En esta funcin podemos claramente determinar dos partes principales:
1. la llamada recursiva, que expresa el problema original en trminos de otro ms pequeo,
y
2. el valor para el cual se conoce una solucin no recursiva. Esto es lo que se conoce como
caso base: una instancia del problema cuya solucin no requiere de llamadas recursivas.
La existencia del caso base cubre fundamentalmente dos objetivos
1. Actuar como condicin de finalizacin de la funcin recursiva. Sin el caso base la rutina
recursiva se llamara indefinidamente y no finalizara nunca.

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

2. Es el cimiento sobre el cual se construir la solucin completa al problema.


Veamos seguidamente, en la figura 1, una traza de las llamadas recursivas a la funcin
factorial originadas para calcular el correspondiente valor de 5 desde una funcin main:

Figura 1: evolucin de las llamadas recursivas del factorial de 5.

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

Las respuestas a la preguntas anteriores seran:


[P1] fibonacci(n) = fibonacci(n-1) + fibonacci(n-2).
[P2] En este caso hay que seleccionar como casos bases fibonacci(1) = 1 y fibonacci(2)=1.
[P3] En cada llamada a la rutina fibonacci se reduce el tamao del problema en uno o en dos,
por lo que siempre se alcanzar uno de los dos casos bases.
[P4] fibonacci(3) = fibonacci(2) + fibonacci(1) = 1 + 1. Se construye la solucin del problema
n==2 a partir de los dos casos bases.
Teniendo en cuenta estas respuestas estamos preparados para implementar la funcin
fibonacci en C:
int fibonacci(int n)
{
if ((n == 1) || (n == 2)) return 1;
else return (fibonacci(n-2) + fibonacci(n-1));
}
La evolucin de las llamadas recursivas para calcular el valor del cuarto trmino de la serie
sera la siguiente:

Figura 2: evolucin de las llamadas recursivas para calcular fibonacci(3).

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

Contar del nmero de veces que aparece un valor dado en un vector.


Dado un vector de n enteros, el problema a resolver recursivamente consiste en contar el
nmero de veces que un valor dado aparece en dicho vector.
Comencemos por las respuestas a las preguntas P1, P2, P3 y P4:
[P1] Si el primer elemento del vector es igual que el valor buscado, entonces la solucin ser
1 ms el nmero de veces que aparece dicho valor en el resto del vector. Si no estuviera en la
primera posicin, la solucin al problema original ser slo el nmero de veces que el valor se
encuentra en las posiciones restantes, por lo que siempre se formula un nuevo problema con un
tamao menor del vector en una unidad.
[P2] El caso base ser cuando el vector a inspeccionar no tenga elementos, por lo que el
nmero de ocurrencias del valor buscado en dicho array ser cero.
[P3] Cada llamada recursiva se reduce en uno el tamao del vector, por lo que nos
aseguramos que en N llamadas habremos alcanzado el caso base.
[P4] Cuando se encuentra una coincidencia se suma uno al valor devuelto por otra llamada
recursiva. El retorno del control de las sucesivas llamadas comenzar inicialmente sumando 0 cuando
nos hayamos salido de las dimensiones del vector.
La funcin en C que implementa la solucin recursiva al problema en curso tiene cuatro
parmetros: el tamao del vector (N), el propio vector (Vector), el valor a buscar (Objetivo) y el ndice
del primer elemento del vector a procesar (Primero) y su cdigo es el siguiente:
int ContarOcurrencias (int N, int *Vector, int Objetivo, int Primero)
{
if (Primero > N-1) return 0;
else
{
if (Vector[Primero] == Objetivo)
return(1 + ContarOcurrencias(N,Vector, Objetivo, Primero+1));
else return(ContarOcurrencias(N,Vector, Objetivo, Primero+1));
}
}
Combinaciones sin repeticin de n objetos tomados de k en k.
En este problema tpico de combinatoria se desea calcular cuntas combinaciones de k
elementos se pueden hacer de entre un total de n (combinaciones sin repeticin de n elementos
tomados de k en k). La funcin recursiva que implementaremos en C se llamar "ncsr" y tendr dos
parmetros: el nmero total de elementos de que disponemos, n, y el tamao del conjunto que se
desea generar, k.
Este enunciado nos sirve para ejemplificar cmo los casos bases pueden ser elaborados en
funcin de varias condiciones. Las respuestas a las cuatro preguntas son las siguientes:
[P1] El clculo del nmero de combinaciones de n elementos tomados de k en k se puede
descomponer en dos problemas: calcular el nmero de combinaciones de n-1 elementos tomados de
k en k y el nmero de combinaciones de n-1 elementos tomados de k-1 en k-1, siendo estos dos
nuevos problemas instancias ms pequeas que el original de donde provienen y del mismo tipo. El
resultado del problema inicial se obtendr, por tanto, sumando ambos resultados: ncsr(n, k) = ncsr(n1, k-1) + ncsr(n-1, k).

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

Si k > n entonces no hay suficientes elementos para formar combinaciones de tamao k,


en cuyo caso el total de combinaciones en este caso es 0.

Si k = 0, entonces este caso slo ocurre una vez.

Si k = n, el nmero total de elementos a tomar coincide con el tamao del grupo de


elementos de donde seleccionarlo, siendo uno el nmero de combinaciones en este
caso.

Resumiendo: ncsr(n, n) = 1, ncsr(n,0)=0 y ncsr(n, k)=0 con k > n.


[P3] Considerando que en una de las llamadas recursivas restamos uno al nmero de
elementos, y en la otra se resta la unidad tanto al nmero de elementos como al tamao de la
combinacin, nos aseguramos que en un nmero finito de pasos se habr alcanzado uno de los
casos bases.
[P4] Apenas se haya encontrado un caso base, uno de los sumandos tendr un valor.
Cuando el otro sumando alcance a un caso base, se podr hacer la suma y se devolver a un valor
que ir construyendo la solucin final conforme se produzcan las devoluciones de control.
Para concluir, la implementacin de la funcin que acabamos de disear es la siguiente:
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);
}

2.3 La pila del orden ador y la recursividad.


En esta seccin vamos a estudiar cmo gestiona un ordenador las llamadas recursivas
cuando ste ejecuta un mdulo recursivo. As, comprendiendo este funcionamiento, seremos
conscientes de los problemas que se nos pueden plantear si el diseo del mdulo recursivo no es
correcto.
La memoria de un ordenador a la hora de ejecutar un programa queda dividida en dos partes:
la zona donde se almacena el cdigo del programa y la zona donde se guardan los datos, utilizada
sta ltima fundamentalmente en las llamadas a rutinas y que se denomina pila (en ingls stack).

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.

Figura 3: estado de la pila cuando se llaman tres mdulos de manera anidada.

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

Figura 4. Notacin grfica para representar el registro de activacin.

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

Figura 6. Vaciado de la pila con el retorno de las llamadas recursivas.

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.

2.4 Paso de parmet ros a los mdulos recursivos.


Hay que hacer algunas consideraciones importantes a la hora de determinar los parmetros
formales que tendr un mdulo recursivo y si stos sern de entrada, salida o de entrada/salida (si se
pasarn por copia o por referencia) ya que puede influir en la correcta ejecucin del mdulo recursivo.
Centrmonos en primer lugar en la forma de pasar los parmetros formales.
Debemos tener mucho cuidado a la hora de decidir el tipo de paso de parmetros.
Supongamos que modificamos el cdigo de la funcin factorial y el parmetro formal n que
pasbamos por copia, ahora lo pasamos por referencia:

25

Programacin II

long factorial (long* n)


{
if (*n == 0) return 1;
else
{
*n = *n -1;
return (*n+1) * factorial(n);
}
El desarrollo de las llamadas recursivas sera correcto, de tal forma que n se va reduciendo a
0, momento en el cual se comenzarn a devolver las llamadas recursivas al haberse alcanzado el
caso base. Pero, qu ocurre en ese momento? Que *n siempre ser igual a 0, y por tanto, se
devolver siempre 1, por lo que finalmente el factorial de cualquier nmero ser 1, resultado a todas
luces incorrecto. Para aclarar lo anterior, estudiemos la traza correspondiente al siguiente trozo de
cdigo para calcular el factorial de 3;

long fact, tamanio= 3;


fact= factorial(&tamanio);
fact= factorial(*n == 3)
(*n == 2) * factorial (*n == 2)
(*n == 1) *factorial(*n == 1)
(*n==0)*factorial(*n==0)
return 1.

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

El otro aspecto a tener en cuenta en el diseo de la cabecera de un mdulo es el nmero y el


tipo de parmetros que se le pasarn. No es conveniente pasar ni muchos parmetros ni parmetros
que tengan un tamao muy grande (por ejemplo, vectores o estructuras extensas), ya que en cada
llamada recursiva la cantidad de memoria ocupada en la pila que se necesita para almacenar los
parmetros actuales es muy grande, y en pocas llamadas recursivas se podra agotar.
Por ejemplo, si en vez de haber diseado la cabecera de la funcin ContarOcurrencias como
int ContarOcurrencias (int N, int *Vector, int Objetivo, int Primero), hubiera hecho int
ContarOcurrencias ( int Vector[MAX], int Objetivo, int Primero), en cada llamada recursiva se har una
copia de Vector en la pila y si el tamao MAX es muy grande, se corre el riesgo de agotar la memoria
a las pocas llamadas.
Se podra solucionar el inconveniente del nmero y tamao de los parmetros de tres formas
diferentes:
a)

Declarar algunas variables como globales, y por tanto, no pasarlas como parmetros
a la funcin.

b)

Hacer pasos por referencia. De esta manera se introduce a la funcin punteros en


lugar de los propios objetos, reduciendo el tamao requerido (esta es la solucin
recogida en la funcin ContarOcurrencias).

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.

Generalmente pasar por copia los parmetros necesarios para determinar si se ha


alcanzado el caso base.

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.

2.5 Recursividad o iteracin ?


La recursividad es una herramienta muy poderosa para resolver problemas, los cuales
resueltos iterativamente podran tener una resolucin muy compleja, sobre todos aquellos cuya propia
definicin es recursiva, aunque esto no asegura que dichos algoritmos recursivos posean una
eficiencia alta, ya que suelen consumir un mayor tiempo de clculo.
En general, si un problema se puede describir en trminos de versiones ms pequeas de l
mismo, entonces la recursividad permite expresarlo, en la mayora de los casos, y por tanto
implementarlo ms fcilmente. De igual forma, cuando la recursividad nos permita solucionar
problemas cuyas soluciones iterativas sean difciles de implementar, utilizaremos esta primera
tcnica.
27

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.

2.6 Eliminacin de la recursividad.


Existen varios lenguajes en los que no se puede utilizar la recursividad como tcnica de
resolucin de problemas debido a que no estn preparados para ello, fundamentalmente por que no
establecen la zona de la pila en la memoria del ordenador. Tambin, y como hemos comentado en la
seccin anterior, la solucin recursiva debe ser evitada cuando implique ms inconvenientes que
ventajas. En estos casos, se deber estudiar la posibilidad de eliminar la recursin total o
parcialmente, utilizando para ello dos tcnicas principales:

28

Recursividad

a) Eliminacin de la recursin de cola.


Cuando existe una llamada recursiva en un mdulo que es la ltima instruccin de la rutina, a
esta llamada se le denomina llamada de recursin de cola. En algunos casos puede ser suficiente
eliminar esta llamada recursiva para obtener alguna ganancia en lo que se refiere a trminos de
eficiencia y de espacio de pila ocupado, que haga que sea una solucin viable la recursiva.
Para eliminar la recursin de cola, se sustituye la llamada recursiva por las sentencias que
cambian los valores de los parmetros que se utilizan en la llamada a suprimir por los valores con los
que se iba a realizar dicha llamada.
En general, frente a una estructura con una nica llamada recursiva de cola ajustndose al
siguiente patrn:
if (condicin)
{
Ejecutar una tarea.
Actualizar condiciones.
Llamada recursiva.
}
El paso a estructura iterativa es inmediato, ya que es equivalente a un ciclo while:
while (condicin)
{
Ejecutar una tarea.
Actualizar condiciones.
}
Sirva como ejemplo la funcin recursiva que implementa la solucin al problema de las Torres
de Hanoi, problema que se estudiar detalladamente en secciones posteriores, pero que nos servir
seguidamente para ejemplificar la eliminacin de la recursin de cola:
La rutina recursiva es:
void TorresHanoi(int n; char de; char hacia; char usando)
{
if (n==1) printf(Moviendo disco 1 desde el poste %c al %c\n,de, hacia);
else
if (n>1)
{
TorresHanoi(n-1, de, usando, hacia);
printf(Moviendo disco %i desde el poste %c al %c\n,n, de, hacia);
TorresHanoi(n-1, usando, a, de);
}
}
Tras eliminar la recursin de cola, sustituyendo la ltima llamada TorresHanoi(n-1, usando, a,
de) por las asignaciones correspondientes para que los parmetros actuales tengan los valores con
los que se haca esta llamada, la rutina quedara como sigue:
void TorresHanoi(int n; char de; char hacia; char usando)
{
char temp;
while (n>1)
{
29

Programacin II

TorresHanoi(n-1, de, usando, hacia);


printf(Moviendo disco %i desde el poste %c al %c\n,n, de, hacia);
n=n-1; temp= de; d= usando; usando= temp;
}
if (n==1) printf(Moviendo disco 1 desde el poste %c al %c\n,de, hacia);
}
Consideremos otro ejemplo: la funcin recursiva Imp_OrdenInversoVector, la cual imprime de
forma inversa un vector de enteros (es una variacin de la ya estudiada Imp_OrdenInverso):

void Imp_OrdenInversoVector(int *Vector, int Tamanio)


{
if (Tamanio > 0)
{
printf(%c, Vector[Tamanio-1]);
Imp_OrdenInversoVector(Vector, Tamanio-1);
}
}
Aplicando el cambio propuesto anteriormente, la funcin iterativa obtenida al eliminar la
recursin de cola quedara como sigue:
void Imp_OrdenInversoVector(int *Vector, int Tamanio)
{
while (Tamanio > 0)
{
printf(%c, Vector[Tamanio-1]);
--Tamanio;
}
}
b) Eliminacin de la recursin mediante la utilizacin de pilas.
Cuando se produce una llamada a una rutina, se introducen en la pila los valores de las
variables locales, la direccin de la siguiente instruccin a ejecutar una vez que finalice la llamada, se
asignan los parmetros actuales a los formales y se comienza la ejecucin por la primera instruccin
de la rutina llamada. Cuando acaba el procedimiento o funcin, se saca de la pila la direccin de
retorno y los valores de las variables locales, y por ltimo se ejecuta la siguiente instruccin.
En esta tcnica de eliminacin de recursividad, el programador implementa una estructura de
2
datos con la que simular la pila del ordenador, introduciendo los valores de las variables locales y
parmetros en la pila, en lugar de hacer las llamadas recursivas, y sacndolos para comprobar si se
cumplen los casos bases. Se aade un bucle, generalmente while, que iterar mientras la pila no est
vaca, hecho que significa que se han finalizado las llamadas recursivas.
Al meter en la pila los valores de las variables locales y los parmetros formales, as como en
algunos casos la direccin de retorno (el lugar donde deber volver el control cuando la invocacin
de la rutina termine), se simula la invocacin recursiva. Cuando se saca de la pila, se est simulando
el comienzo de la rutina recursiva, ya que se recuperan los valores de las variables locales y
parmetros formales con los que se hara la llamada recursiva.

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.

2.7 Algunas tcnicas de resolucin de problemas basadas


en la recursividad.
Son muchas las tcnicas de resolucin de problemas que utilizan la recursividad como idea
bsica. En esta seccin sern dos las que presentaremos someramente y de las que ofreceremos
algn ejemplo para comprender sus fundamentos.

32

Recursividad

2.7.1 Divide y vencers.


Esta tcnica soluciona un problema mediante la solucin de instancias ms pequeas de
dicho problema, de tal manera que dichos subproblemas tienen una resolucin ms fcil,
construyendo la solucin al problema original a partir de dichas "subsoluciones". As, teniendo en
cuenta este procedimiento de actuacin, si se desea encontrar un problema de tamao n, si ste
tamao es menor que un umbral dado, entonces se podr aplicar, en caso de que exista, un algoritmo
sencillo para resolverlo, ya que el tamao del problema permite que sea abordado por un algoritmo
que encuentre la solucin de forma simple y rpida. En caso contrario, se dividir el problema de
tamao n en m subproblemas distintos de menor tamao y se encontrar la solucin a cada uno de
ellos mediante una llamada recursiva. Finalmente, y una vez solucionados dichos subproblemas, de
alguna forma se combinarn para calcular la solucin al problema inicial.
Tanto el tamao umbral en el que se aplicar un algoritmo sencillo, como el nmero de
subproblemas en los que se dividirn el primero y la forma de combinar las soluciones depender
claramente del tipo de problema, no existiendo una regla que pueda determinar dichos valores, y ser
la persona que disee el algoritmo quien deber decidir.
Como se puede apreciar la filosofa de esta tcnica se corresponde totalmente con la
recursividad, aunque hay que avisar de nuevo que no todos los problemas son aptos para ser
resueltos por el "divide y vencers".
Bsqueda binaria.
En primer lugar estudiemos el algoritmo de bsqueda binaria recursiva de un valor en un
vector ordenado, que ya utilizamos en la primera seccin de este captulo para ayudarnos a explicar
el concepto de recursin.
Bsicamente, el problema se resuelve determinando si el elemento buscado est en la
posicin que ocupa la mitad del vector. Si estuviera, concluiramos la bsqueda. Si no es as,
determinamos en qu mitad debera estar el elemento: si fuera menor o igual que el valor de la
posicin de la mitad, deberamos buscar en la mitad izquierda, y si fuera mayor, en la parte derecha.
Una vez decidida la mitad donde deberamos buscar, el problema original (bsqueda de un elemento
en el vector completo) lo sustituimos por la bsqueda slo en una mitad (obviamente, el problema
original se reduce de tamao), en donde volveramos a repetir todo el proceso descrito anteriormente.
Cundo acabaramos? En el momento en que encontremos el valor buscado, o cuando el tamao
del vector sea cero, lo que indica que no est el valor buscado.
En este caso, y siguiendo las directrices de la tcnica "divide y vencers", el problema se
divide en dos subproblemas de igual tamao e independientes entre s, lo que implica que
posteriormente no habr que combinar las soluciones, pues la solucin a un subproblema ser la
solucin general.
La funcin recursiva en C que implementar la bsqueda binaria recursiva (BusqBinaria)
recibir como parmetros un vector de nmeros enteros (Vector), y tres parmetros formales enteros:
los lmites inferior (LimInf) y superior (LimSup) del vector y el valor a buscar (Valor), todos ellos
pasados por copia. Los lmites nos indicarn en cada momento el tamao y el subvector determinado
donde buscaremos.
El cdigo en C de la funcin que resuelve el problema es el siguiente:
int BusqBinaria(int *Vector, int LimInf, int LimSup, int Valor)
{
int Mitad;
if (LimInf > LimSup) return -1;
else
{
33

Programacin II

Mitad= (LimInf + LimSup)/2;


if (Valor == Vector[Mitad]) return Mitad;
else
if (Valor <= Vector[Mitad])
return BusqBinaria(Vector, LimInf, Mitad-1, Valor);
else return BusqBinaria(Vector, Mitad+1, LimSup,Valor);
}
}
Tras comprobar que los lmites pasados como parmetros actuales se han cruzado, en cuyo
caso el inferior es mayor que el superior, lo que implica que el valor buscado no est en el vector,
devolviendo la funcin -1 para indicarlo, y una vez calculada la posicin de la mitad, que esa posicin
no contenga dicho valor, se determina en qu mitad est: si lo est en la mitad izquierda (es menor o
igual que el valor de la posicin central), los nuevos lmites del vector donde se buscarn sern LimInf
y Mitad -1; si lo est en la mitad superior (el valor es mayor que el central), los lmites se convertirn
en Mitad + 1 y LimSup.
La invocacin inicial para un vector de enteros llamado VectEnt compuesto de 10 elementos,
en donde se desea buscar el valor 8 sera: Posicion= BusqBinaria(VectEnt, 0, 9, 8); Dejamos a cargo
del lector la comprobacin de las cuatro preguntas para validar esta solucin recursiva.
Las torres de Hanoi.
Los autores de [CHV88] hacen una descripcin bastante ilustrativa del problema que vamos a
abordar a continuacin. Expresada de manera algo abreviada, es la siguiente:
Cuenta la leyenda que el emperador de un pas con capital en la actual ciudad vietnamita de
Hanoi, deba elegir a un nuevo sabio del reino tras la jubilacin del actual. Para ello, el sabio todava
en ejercicio pens en un problema y aquella persona que lo solucionara ocupara su puesto en la
corte.
Utilizando tres postes (A, B y C) y n discos de diferentes tamaos y con un agujero en sus
mitades para que pudieran meterse en los postes, y considerando que un disco slo puede situarse
en un poste encima de uno de tamao mayor, el problema consista en pasarlos, de uno en uno, del
poste A, donde estaban inicialmente colocados, al poste B, utilizando como poste auxiliar el restante
(C), cumpliendo siempre la restriccin de que un disco mayor no se pueda situar encima de otro
menor. El estado inicial de los postes y el final es el que queda representado grficamente en la figura
7 mediante las situaciones 1 y 2:

34

Recursividad

Figura 7. Estados inicial y final y pasos intermedios a la resolucin.

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

TorresHanoi(NumDiscos-1, Auxiliar, Destino, Origen);


}
}
Para un total de 5 discos, la llamada a la funcin sera:

TorresHanoi(5, 'A', 'B', 'C');


Por ltimo, comentar a modo de curiosidad, que otra leyenda cuenta que hay un grupo de
monjes que tienen la misin de resolver el problema con un total de 40 discos de oro sobre tres
postes de diamante y que cuando finalicen, el mundo se acabar. Esperemos por nuestro bien que
estos monjes no utilicen un ordenador para solucionarlo ;-)

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.

[AHU88] A. Aho, J.E. Hopcroft, J. Ullman. Estructuras de datos y algoritmos. Addison-Wesley


(1.988).

[BB86] P. Berlioux, P. Bizard. Algorithms. The construction, proof and analysis of programs.
John Wiley and Sons (1.986).

[BB90] G. Brassad, P. Bratley. Algortmica. Concepcin y anlisis. Masson (1.990).

[CCP93] F.J. Cortijo, J.C. Cubero, O. Pons. Metodologa de la programacin. Programas y


estructuras de datos en Pascal. ISBN. 84-604-7652-9 (1.993).

[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).

[LAT97] Y. Langsom, M. Augenstein, A. Tenenbaum. Estructuras de datos con C y C++. 2


Edicin. Prentice Hall (1.997).

[Riv99] M. L. Rivero Cejudo. Programacin: parte terica. Coleccin Apuntes. Universidad de


Jan (1.999).
nd

[Sed88] R. Sedgewick. Algorithms in C. 2 Edition. Addison-Wesley (1.988).

[Sed98] R. Sedgewick. Algorithms in C. 3 Edition. Addison-Wesley (1.998).

[Wir86] N. Wirth. Algorithms and data structures. Prentice Hall (1986).

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.

Tericamente, lo que equivale a determinar matemticamente la cantidad de recursos


(tiempo de ejecucin y memoria) requeridos por la implementacin en funcin del tamao
de la entrada.

El problema que se origina al considerar un estudio emprico radica en la dependencia de los


resultados obtenidos del tipo de ordenador con el que se hayan realizado los experimentos, del
lenguaje de programacin usado y del traductor con el que se obtenga el cdigo ejecutable, e incluso
de la pericia del programador (si cambiamos alguno de estos elementos probablemente se obtengan
resultados diferentes, con lo cual no podemos establecer una eficiencia emprica absoluta).

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

Anlisis de la eficiencia de los algoritmos.

3.2 El tiempo de ejecucin de un algoritmo y su orden de


eficiencia.
Una vez definido el tamao de la entrada, resulta conveniente usar una funcin, T(n), para
representar el nmero de unidades de tiempo (segundos, milisegundos,...) que un algoritmo tardara
en ejecutarse con unos datos de entrada de tamao n. Como el tiempo de ejecucin de un programa
depende claramente del ordenador que se utilice para medirlo y del traductor con el que se haya
generado el cdigo objeto, sera preferible que T(n) no represente un tiempo, sino el nmero de
instrucciones simples (asignaciones, comparaciones, operaciones aritmticas,...) que se ejecutan, o
de forma equivalente, el tiempo de ejecucin del algoritmo en un ordenador idealizado, donde cada
una de las instrucciones simples consumen una unidad de tiempo. En general se suele dejar sin
1
especificar las unidades empleadas en T(n) y se asume que n0 y T(n) es positivo.
Un concepto que nos ayudar a entender por qu podemos evitar el uso de unidades de
tiempo en la funcin T(n) es el que se conoce como el principio de invarianza, vlido
independientemente tanto del ordenador que estemos utilizando, como del compilador. El principio
establece que dos implementaciones distintas de un mismo algoritmo, que toman t1(n) y t2(n)
unidades de tiempo, respectivamente, para resolver un problema de tamao n, no diferirn en
eficiencia en ms de una constante multiplicativa. Expresado matemticamente, existe un c>0 tal que
t1(n) ct2(n). As, se podr hacer que un programa vaya 10 1000 veces ms rpido cambiando de
mquina, pero slo un cambio de algoritmo nos permitir obtener una mejora mayor cuanto ms
crezca el tamao de los ejemplos, lo que nos llevar a ignorar las constantes multiplicativas a todos
los efectos.
Ahora bien, un mismo algoritmo puede ejecutarse con conjuntos de datos diferentes y su
tiempo de ejecucin puede ser distinto para cada uno de ellos: habr datos de entrada con los que el
algoritmo emplee ms tiempo, y otros con los que finalice antes, por lo que es conveniente indicar si
la expresin que se obtiene al realizar el anlisis de la eficiencia corresponde al caso peor, al caso
promedio, o al mejor caso. En el primero de ellos, estamos indicando un lmite superior, el mximo
valor, del tiempo de ejecucin para cualquier entrada al algoritmo, al contrario que ocurre con el mejor
caso, donde ofrecemos un lmite inferior, indicando que el algoritmo, no se ejecutar por debajo de
ese tiempo. El caso promedio ser una media ponderada de ambos casos, aunque los abundantes y
a veces complicados clculos para realizar un anlisis de eficiencia del caso promedio complican
notablemente su uso.
Aunque el peor caso suele ser bastante pesimista, y probablemente el comportamiento del
algoritmo sea algo mejor que el obtenido por el anlisis, se suele utilizar ms a menudo que el mejor
caso. En este captulo, salvo que se diga lo contrario, se calcular la expresin analtica del peor
caso.
Veamos un ejemplo: un algoritmo de ordenacin creciente de un vector mediante el mtodo
de seleccin cuando se aplica a un vector ya ordenado de forma creciente (mejor caso) tardar ms o
menos igual que si se aplica sobre el mismo vector ordenado de manera decreciente (peor caso).
Este es un ejemplo en el que ambos tiempos, el del peor y del mejor caso, coinciden. Sin embargo,
cuando tratamos con el algoritmo de ordenacin mediante insercin, el tiempo de ejecucin aplicado
a un vector ordenado crecientemente es proporcional al nmero de elementos del vector, n, y si lo
aplicamos al vector ordenado decrecientemente, dicho tiempo se incrementar de manera
proporcional al cuadrado de n. En este otro algoritmo, los tiempos son diferentes.

Hacerlo as no es restrictivo, ya que si T(n) es el tiempo de ejecucin en un ordenador ideal,


entonces el tiempo de ejecucin en un ordenador real ser T(n) por una cierta constante que depende
del ordenador y que se puede calcular empricamente.
41

Programacin II

Cmo se calcula el tiempo de ejecucin de un algoritmo? La respuesta es clara: sobre la


base de las instrucciones que componen el algoritmo. Aunque posteriormente lo estudiaremos con
detalle, esbozaremos seguidamente algunas ideas. La evaluacin de una expresin tendr como
tiempo de ejecucin lo que se tarde en realizar las operaciones que contenga. Una asignacin a una
variable simple de una expresin, al igual que una operacin de escritura de una expresin, suele
tardar el tiempo de evaluar dicha expresin ms un tiempo constante relativo a gestin interna de la
asignacin o del proceso de escritura. Una lectura de una variable requiere un tiempo constante. Una
secuencia de instrucciones, la suma de los tiempos de cada una de las instrucciones. En una
sentencia condicional, su tiempo de ejecucin ser el de evaluar la condicin ms el mximo de los
costes del bloque entonces y del bloque sino. Para un bucle, se evala el tiempo del cuerpo del bucle
y se multiplica por el nmero de iteraciones ms el tiempo de evaluar la condicin del bucle.
Todas aquellas instrucciones cuyo tiempo de ejecucin queda limitado superiormente por una
constante, que slo depende de la implementacin, se denominarn operaciones elementales. Por
tanto, al habernos desprendido de las constantes multiplicativas, para el anlisis de la eficiencia de un
algoritmo slo ser relevante el nmero de operaciones primitivas y no su duracin. Consideraremos,
por tanto, que las operaciones de suma, multiplicacin, resta, divisin, mdulo y similares son
operaciones elementales y por tanto, tendrn un costo unidad.
Hemos considerado hasta ahora que el anlisis de la eficiencia se hace siempre sobre un
algoritmo, sin tener en cuenta la implementacin en un lenguaje de alto nivel. En nuestro caso, y a
partir de este momento, los ejemplos sern implementados en C y a partir de dicha implementacin
se estudiar su eficiencia. La razn fundamentalmente es la coherencia con respecto al resto de
captulos de este libro ya que todos estn basados en C.
Veamos un primer ejemplo para identificar intuitivamente estos conceptos. La siguiente
funcin obtiene la posicin donde se encuentra el mnimo valor de un vector de nmeros enteros:
/* 1 */ int BuscarMinimo(int *Vector, int n)
/* 2 */ {
/* 3 */ int j, min;
/* 4 */ min= 0;
/* 5 */ for (j=1; j<n;j++)
if (Vector[j] < Vector[min])
/* 6 */
/* 7 */
min= j;
/* 8 */ return min;
/* 9 */ }
En el tiempo de ejecucin de este programa no se consideran todas las instrucciones: las
declaraciones de variables y la propia declaracin de la funcin no intervienen, por lo que
empezaremos por la lnea 4 a hacer el clculo del tiempo de ejecucin. La lnea 4 pertenece a una
asignacin, por lo que contabilizar una constante ca. En la quinta lnea se tendrn en cuenta dos
constantes: una por el incremento del contador j , ci, y otra por la evaluacin de la condicin booleana,
ce, que incrementarn el tiempo de ejecucin tantas veces como se ejecute el bucle: n-1 veces, ms
dos veces adicionales correspondientes al caso en que j == n+1. El cuerpo del bucle podr tener
como coste ce ce+ca, dependiendo de si la condicin del if se evala verdadera o falsa, y se ejecuta
o no el bloque then. Como hemos considerado trabajar siempre con el peor caso, tomaremos la
suma del tiempo de evaluacin de la condicin (lnea 6) ms el de la asignacin de la lnea 7. Por
ltimo, nos queda la lnea 8, que consumir una constante cr. Agrupando todo lo anterior en una nica
expresin tendramos: T(n)= ca + (ci + ce)(ce+ca)(n-1) + cr. Si consideramos que esas constantes son
la unidad, por provenir de operaciones elementales, haciendo clculos obtendremos T(n)= 4(n-1) + 4=
4n.

42

Anlisis de la eficiencia de los algoritmos.

En este momento ya estamos en condiciones de aclarar el concepto de eficiencia, el cual


hace referencia a la forma en que el tiempo de ejecucin (y en general cualquier recurso) necesario
para procesar una entrada de tamao n crece cuando se incrementa el valor de n. Es por esto, por lo
que se especificar mediante una funcin de crecimiento. Como se puede observar, dicha eficiencia
slo depende del tamao del problema, dejando a un lado cuestiones como la velocidad del
ordenador y la eficiencia del compilador.
Se dice que un algoritmo necesita un tiempo de ejecucin del orden de una funcin
cualquiera f(n), cuando existe una constante positiva c y una implementacin del algoritmo que
resuelve cada instancia del problema en un tiempo acotado superiormente por cf(n), siendo la funcin
f(n) la que marca cmo crecer dicho tiempo de ejecucin cuando aumente n. Un algoritmo ser, por
tanto, ms eficiente que otro si el tiempo de ejecucin del peor caso tiene un orden de crecimiento
menor que el segundo.
Supongamos que para un cierto problema dos algoritmos ,A y B, lo resuelven mediante las
2
funciones TA(n)=100n y TB(n)=2n , respectivamente. Cul deberamos usar? Si n<50, A es mejor
que B, pero en el caso de que n50, A es mejor que B, hacindose mayor la ventaja de A sobre B
conforme n crece (si n= 100, A es el doble de rpido que B; si n=1.000, A es 20 veces ms rpido que
2
B). Con este ejemplo podemos ver cmo es ms importante la forma de las funciones (n y n ), que las
constantes (2 y 100). Adems, como el tiempo de ejecucin de un algoritmo en un ordenador
concreto requiere que se multiplique por una constante slo medible mediante la experimentacin,
parece lgico que nos olvidemos de calcular los factores multiplicativos al analizar un algoritmo.
En vez de decir que el tiempo de ejecucin del algoritmo es T(n)=4n+3, por ejemplo, diremos
que T(n) es del orden de n, lo que implica que el tiempo de ejecucin es alguna constante
multiplicada por n, para tamaos de problemas de dimensin n.
Las funciones que nos marcan ms comnmente el orden de crecimiento de los algoritmos y
2
los nombres por las que se les conocen son las siguientes :
f(n)= 1 => Constante.

f(n)= log n => Logartmica.


f(n)= n => Lineal.
f(n)= nlog n => Quasilineal.
2
f(n)= n => Cuadrtica.
3

f(n)= n => Cbica, y en general, f(n)= n => polinmica.


n
f(n)= k => Exponencial.

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

Tabla 1. Crecimientos de las funciones ms comunes del orden de eficiencia.

Las tasas de crecimiento ms frecuentes se pueden comparar en la representacin grfica de


la figura 1.
Bsicamente, los rdenes de eficiencia son clases de equivalencia de funciones, y cuando se
calcula el orden de un algoritmo, lo que se hace es estimar el tiempo de ejecucin en funcin del
tamao de la entrada y seleccionar una de esas clases. Esas clases de equivalencia se forman con
funciones que son equivalentes segn el principio de invarianza expuesto anteriormente. Por ejemplo,
3
2
3
los tiempos de ejecucin T1(n)= 2n + 4n + 5 y T2(n)= n - 4n, son funciones que pertenecen a la
3
misma clase de equivalencia cuya representante es la funcin f(n)= n .
Hasta ahora hemos centrando toda la discusin en la velocidad de crecimiento del tiempo de
ejecucin como la piedra de toque para evaluar un algoritmo o programa, pero deberamos tener en
cuenta otros criterios en donde dicho tiempo de ejecucin del programa podra ser ignorado y que
deben ser tenidos en cuenta a la hora de elegir o disear un algoritmo. Nosotros destacamos dos
principalmente:

44

Si un programa se va a ejecutar pocas veces, el costo de escritura es el principal, no


influyendo en dicho costo el tiempo de ejecucin, debindose elegir un algoritmo cuya
aplicacin sea la ms fcil.

Anlisis de la eficiencia de los algoritmos.

Si un algoritmo se va a ejecutar con entradas pequeas, entonces la velocidad de


crecimiento del tiempo de ejecucin puede ser menos importante que las constantes
multiplicativas, al contrario que lo que dijimos varios prrafos atrs. Por ejemplo,
supongamos dos implementaciones de un mismo algoritmo cuyos tiempos de ejecucin
2
3
son 100n y 5n , respectivamente. Se cumple que para n < 20, el segundo programa ser
ms rpido que el primero, por lo que si el programa se va a ejecutar normalmente con
entradas pequeas, en este caso menores que 20, conviene ejecutar el segundo, ya que
3
2
debido a la constante 5, ya que 5n estar siempre por debajo que 100n . Otra cuestin
es que esas entradas sean muy grandes, en cuyo caso, nos decantaremos claramente
por la primera implementacin, ya que la velocidad de crecimiento es menor.

Figura 1. Representacin grfica de los rdenes de crecimiento ms frecuentes.

3.3 Las notaciones asintticas.


En esta seccin vamos a introducir las notaciones que sern utilizadas para razonar sobre la
eficiencia de los algoritmos. Dichas notaciones se califican como asintticas debido a que el estudio
de los rdenes de eficiencia se hace en casos lmite, es decir, se trata de determinar cmo crece el
tiempo de ejecucin con respecto al tamao de la entrada en el lmite: cuando el tamao de la
entrada se incrementa sin lmite.
Son tres las notaciones asintticas que vamos a tratar: la O mayscula, la o minscula y las
notaciones y , aunque nos centraremos algo ms en la primera que es la que representa el peor
comportamiento, y ser la que utilicemos de la siguiente seccin en adelante.

45

Programacin II

Las notaciones 0 mayscula y o minscula.


La notacin O mayscula, O(f(n)), representa el conjunto de funciones g que crecen como
mucho tan rpido como f, o lo que es lo mismo, las funciones g tales que f llega a ser en algn
momento una cota superior para g. En definitiva se trata de buscar una funcin sencilla, f(n), que
acote superiormente el crecimiento de otra g(n), en cuyo caso se notar como g(n) O(f(n)) (g es del
+
orden de f). La definicin formal es la siguiente: sea f:NR {0} una funcin cualquiera, el conjunto
de las funciones del orden de f(n), notado como O(f(n)), se define:
+

O(f(n)) = { g c0 R y n0 N, n n0 g(n) c0f(n) }


Esta definicin garantiza que si el tiempo de ejecucin de una implementacin de un
algoritmo es g(n), la cual es del orden de f(n), el tiempo g'(n) empleado por cualquier otra
implementacin que difiera de la primera en el lenguaje y en el compilador utilizado, o la propia
mquina, tambin ser del orden de f(n).
Veamos algunos ejemplos:
T(n)= 3n + 2 O(n), ya que existen dos constantes positivas n0= 2, y c0=4, tal que 3n+2 4n.
2

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:
+

O(f(n,m)) = { g c0 R y n0,m0 N, n n0 y m m0, g(n,m) c0f(n,m) }


Algunas propiedades interesantes de la notacin O(f), para cualesquiera que sean las
funciones f, f', g, h:
+

c R , g O(f) si y slo si c g O(f) [Invarianza multiplicativa].

c R , g O(f) si y slo si c+g O(f) [Invarizanza aditiva]

f O(f) [Reflexividad]

Si h O(g) y g O(f) entonces h O(f) [Transitividad.]

g O(f) si y slo si O(g) O(f) [Criterio de Caracterizacin].

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:

Si g O(f) y h O(f'), entonces g + h O(max(f, f')) [Regla de la suma].

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

Anlisis de la eficiencia de los algoritmos.

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 ).

Si g O(f) y h O(f'), entonces g h O(f f') [Regla del producto].

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:
+

o(f(n)) = { g c0 R y n0 N, n n0 g(n) c0f(n) }


Si comparamos las definiciones de O( ) y o( ) podemos observar que la nica diferencia es
que el cuantificador existencial de la primera se convierte en un cuantificador universal en la segunda,
indicando que independientemente del factor que se utilice, co, f siempre estar por encima de g. As,
2
2
2
por ejemplo, 2n o(n ), pero 2n o(n ).
Tambin se dice que g(n) o(f(n)) si y slo si el lmite cuando n tiende al infinito de g(n)/f(n)
es cero.
Por ltimo, indicar que se cumple la siguiente cadena de inclusiones:
2

O(1) O(log n) O(n) O(n ) ... O(n ) ... O(2 ) O(n!)

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:
+

k(f(n)) = { g c0 R y n0 N, n n0 g(n) c0f(n) }


+

(f(n)) = { g c0 R y n0 N, n n0 g(n) c0f(n) }


Si g(n) k(f(n)) entonces f es una cota inferior de g desde un punto en adelante, o lo que es
lo mismo, ofrece un mnimo del cual nunca baja (g crece ms deprisa que f). Por otro lado, si g(n)
(f(n)), entonces en una cantidad infinita de ocasiones g crece lo suficiente como para alcanzar a f.
(f(n)).

En este caso, si el lmite cuando n tiende a infinito de g(n)/f(n) es infinito, entonces g(n)

De igual manera que la notacin O( ) posee la notacin o( ), para ( ), existe la notacin ( ),


con el mismo significado que o, aunque teniendo en cuenta que tratamos ahora cotas inferiores.

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:
+

(f(n)) = { g c0, c1 R y n0 N, n n0, 0 c0f(n) g(n) c1f(n) }


En definitiva, si existen dos constantes por las que la funcin g queda embutida entre la
funcin f, entonces g(n) (f(n)) (ledo sera el orden exacto de g es f). En este caso, el lmite
cuando n tiende a infinito de g(n)/f(n) es igual a k>0, entonces g(n) (f(n)).

Relaciones entre las diferentes notaciones.


En el grfico de la figura 2, y a modo de aclaracin, se pueden observar ejemplos grficos del
significado de cada notacin:

Figura 2. Representaciones grficas de las diferentes notaciones asintticas.

3.4 Clculo del tiempo de ejecucin de un algoritmo


iterativo y de su orden de eficiencia.
En esta seccin vamos a establecer un conjunto de reglas simples que nos ayudarn a
estudiar la complejidad algortmica de un programa. A partir de este momento slo trabajaremos con
la notacin O( ), haciendo referencia, por tanto, al peor de los casos. El objetivo es pues obtener el
orden de eficiencia de un programa, para lo cual estudiaremos cmo se calcula el tiempo de
ejecucin de las diferentes sentencias que nos podemos encontrar en un lenguaje de programacin y
que son relevantes para dicho clculo: sentencias simples, condicionales e iterativas.
48

Anlisis de la eficiencia de los algoritmos.

El objetivo final que vamos buscando se puede alcanzar de dos formas diferentes:

Obtener el tiempo de ejecucin del programa, y posteriormente calcular su orden de


eficiencia.

Ir calculando el orden de eficiencia de las diferentes sentencias y bloques existentes en el


programa.

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*/

printf(" %i ", i);

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

Anlisis de la eficiencia de los algoritmos.

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:

En una asignacin, el tiempo de ejecucin de esa asignacin bsicamente ser el de la


funcin. Si hay ms de una funcin invocada en la sentencia, corresponder a la suma de
tiempos. En el caso de rdenes, corresponde al mximo de cada uno.

En una condicin de un bucle (cualquier tipo), se sumar el tiempo de ejecucin de la


funcin al tiempo de ejecucin de cada iteracin, multiplicndose finalmente ese tiempo
por el nmero de iteraciones que realiza el bucle. Adems, para bucles controlados por
centinela, habr que sumar el tiempo de ejecucin de la funcin al costo de la evaluacin
por primera vez de la condicin en caso de que no se itere ninguna vez (aunque
estudiando el peor de los casos, esto no se dara). Para los controlados por contador, si
la llamada est en la inicializacin del bucle, se deber sumar el tiempo de ejecucin de
la funcin al costo total del bucle.

En la condicin de una sentencia condicional se suma el tiempo de ejecucin de la


funcin al obtenido al evaluar la sentencia condicional.

En una secuencia de sentencias: simplemente aplicar la regla de la suma, si tratamos con


rdenes, o sumar tiempos si son tiempos de ejecucin.

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

Anlisis de la eficiencia de los algoritmos.

long SumaSubsecuenciaMaxima(int *Vector, int n)


{
int i, j, k;
/*1*/ int SumMax=0, PosInicioSec=0, PosFinSec=0, SumaActual;
/*2*/ for (i=0;i<=n; i++)
for (j=i;j<=n; j++)
/*3*/
/*4*/
{
/*5*/
SumaActual=0;
for (k=i;k<=j; i++)
/*6*/
/*7*/
SumaActual+= Vector[k];
If (SumaAcutal > SumaMax)
/*8*/
/*9*/
{
/*10*/
SumaMax= SumaActual;
/*11*/
PosInicioSec= i;
/*12*/
PosFinSec= j;
/*13*/
}
/*14*/
}
/*15*/ return SumMax;
}
Entre las lneas 4 y 14 tenemos una asignacin, un bucle for y un if. En este caso, aplicando
la regla de la suma, tomaremos el orden que sea mayor de los tres. La asignacin, como ya
sabemos, es O(1), el condicional podemos considerarlo tambin como O(1). Por otro lado, el bucle
repetir j-i+1 veces una asignacin, lo que implica que pertenecer a O(j-i+1). Como debemos
ponernos en el peor de los casos, el bucle se podra repetir n veces, con lo cual nos debemos quedar
con O(n) como el mximo de los tres rdenes. Claramente el tiempo de ejecucin de la funcin
vendr dada por la siguiente suma:
n

T (n) =

1 , la cual determina el nmero de veces que se ejecuta la instruccin de la


i =1 j = i k = i

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

sumatoria, bucle de la lnea 2, aplicada a la expresin anterior:


n

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

Teniendo en cuenta el orden de complejidad de la funcin, y observando la implementacin,


nos damos cuenta que se podra evitar ese orden si se elimina un bucle for, consiguiendo un orden
de eficiencia cuadrtico:
long SumaSubsecuenciaMaxima(int *Vector, int n)
{
int i, j, k;
/*1*/ int SumMax=0, PosInicioSec=0, PosFinSec=0, SumaActual;
53

Programacin II

/*2*/ for (i=0;i<=n; i++)


/*3*/
{
/*4*/
SumaActual=0;
for (j=i;j<=n; j++)
/*5*/
/*6*/
{
/*7*/
SumaActual+= Vector[k];
If (SumaAcutal > SumaMax)
/*8*/
/*9*/
{
/*10*/
SumaMax= SumaActual;
/*11*/
PosInicioSec= i;
/*12*/
PosFinSec= j;
/*13*/
}
/*14*/
}
/*15*/ }
/*16*/ return SumMax;
}
Este es un ejemplo de cmo un estudio de la complejidad algortmica puede hacer que nos
replanteemos el algoritmo, dando lugar finalmente a un diseo mucho ms eficiente. De hecho, se
puede reducir ms su orden de complejidad mediante un algoritmo recursivo a O(nlgn), tarea que le
proponemos al lector.

3.5 Clculo del tiempo de ejecucin de algoritmos


recursivos y de su orden de eficiencia.
Cuando se analiza la eficiencia de un programa recursivo, suele ocurrir habitualmente que las
funciones del tiempo de ejecucin que se obtengan sean tambin recursivas, es decir, expresiones de
la forma T(n)=E(n), apareciendo la propia funcin T en la expresin E. Este tipo de ecuaciones se
denominan relaciones recurrentes o recurrencias. Una vez que tenemos una recurrencia, para poder
obtener su orden de eficiencia deberamos encontrar una expresin no recursiva para la funcin T(n).
De nuevo, se ignorarn las constantes multiplicativas.
Veamos el siguiente ejemplo que se corresponde con la implementacin recursiva del clculo
del factorial y que nos sirve para introducir el mtodo de expansin de recurrencias:
long factorial (long n)
{
/*1*/ if (n <= 0) return 1;
/*2*/ else return n * factorial(n-1);
}
Podemos observar que si n 1, el tiempo de ejecucin de la funcin recursiva es T(n)=c y
que si n > 1, se cumplir que T(n) = d + T(n-1).
Cmo resolvemos esta recurrencia? Ms adelante estableceremos formalmente varios
mtodos, pero por ahora obtendremos la expresin no recursiva de T(n) mediante la expansin de la
misma, es decir, sustituyendo T(n-1) por su valor correspondiente, y as sucesivamente hasta que se
elimine la recursividad de la expresin del tiempo de ejecucin: hasta que lleguemos al caso donde
T(n) no est expresada en funcin de s misma.
T(n) = d + T(n-1)= d + d+ T(n-2)= d + d + d + T(n-3). Si repetimos el proceso i pasos, la
recurrencia tendra la siguiente forma: T(n)=id+T(n-i), n>i. En particular, para i=n-1, tenemos que
T(n)= (n-1)d + T(n-(n-1))= (n-1)d + T(1)= (n-1)d+c, ya que conocemos que T(1) es 1. Al final el tiempo
de ejecucin pertenece a O(n).

54

Anlisis de la eficiencia de los algoritmos.

Continuemos con el estudio de la eficiencia del un algoritmo de ordenacin basado en el


mtodo de seleccin recursivo:
void OrdenarVector (int *Vector, int n)
{
int i, MaxPos;
if (n>1)
{
MaxPos=0;
for (i=1; i<n;i++)
if (Vector[i] > Vector[MaxPos])
MaxPos= i;
if (MaxPos != 0)
{
i= Vector[0];
Vector[0]= Vector[MaxPos];
Vector[MaxPos]= i;
}
OrdenarVector(Vector+1, n-1);
}
}
La ecuacin de recurrencia est definida en dos partes: la primera corresponde al caso base,
es decir, cuando el vector a ordenar tenga una longitud menor o igual que uno, en cuyo caso, T(1)=1,
ya que slo tendremos que contar el tiempo que tarda la evaluacin de la condicin, el cual
suponemos 1. En el caso contrario, n 2, tendremos un bucle con un cuerpo constante, que se repite
n veces ms el tiempo de la llamada recursiva, pero esta vez aplicada a un vector con un tamao en
una unidad menor que el de la llamada original. Por tanto T(n) = T(n-1) + n.
Una vez planteada la ecuacin recurrente procederemos a su resolucin mediante la tcnica
de la expansin, para lo cual realizaremos expansiones sucesivas:
T(n)= T(n-1) + n = T(n-2) + (n-1) + n = T(n-3) + (n-2) + (n-1) + n,
y en general:
T (n) = T (n i ) +

i 1

( n j ), n i + 1 .
j =0

Si particularizamos para el valor i=n-1, podremos eliminar el trmino recurrente, obteniendo


T (n) = T (1) +

n 2

(n j )
j =0

Por ltimo nos queda resolver la sumatoria:


T (n) = 1 + 2 + ... + n =

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

3.6 Resolucin de recurrencias homogneas.


Las recurrencias lineales homogneas tienen la forma:
a0tn + a1tn-1 + ... + aktn-k = 0

[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

es solucin para la ecuacin recurrente lineal

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

En primer lugar pongamos la expresin recurrente anterior con la forma de la recurrencia


lineal homognea mediante un cambio de notacin [1]:
tn= 3tn-1 - 4tn-2 => tn - 3tn-1 - 4tn-2 = 0
n

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

, con objeto de eliminar la solucin trivial

- 4x

n-k

Posteriormente dividimos cada trmino por x = x


y conseguimos la ecuacin caracterstica:
2

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

tn= c1(-1) + c24

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

Anlisis de la eficiencia de los algoritmos.


n

n-k

Cambiando de notacin, sustituyendo tn=x y dividiendo por x = x


caracterstica:

n-2

se obtiene la ecuacin

tn-tn-1-tn-2=0 => x -x-1=0


1/2

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

tn= c11 + c22 + c3n2

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 ).

3.7 Resolucin de recurrencias no homogneas.


Las recurrencias que vamos a tratar a continuacin son ms generales que las anteriores y
tienen el siguiente aspecto:
n

a0tn + a1tn-1 +...+ aktn-k = b p(n) [3]


Donde p(n) es un polinomio de grado d, y b es una constante (el resto es igual que [1]). A
partir de ah, se intentar obtener la ecuacin caracterstica, que tendr el siguiente aspecto:
k

(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

tn= c12 + c21 + c3n1 = c12 + c2 +c3n


Con t0, se obtienen t1 y t2, y se sustituyen en la ecuacin anterior, obteniendo el siguiente
sistema de tres ecuaciones con tres incgnitas:
c1 + c2 = 0, 2c1 + c2 + c3 = 1, 4c1 + c2 + 2c3 = 4
Las soluciones son c1 = 2, c2 = -2 y c3 = -1, con lo que finalmente:
n

n+1

tn =2 2 - n -2 = 2

n+1

- n - 2 O(2

De esta misma manera, se pueden resolver recurrencias de la forma:


n

a0tn + a1tn-1 +...+ aktn-k = b1 p1(n) + b2 p2(n) + b3 p3(n) +... [5]


Siendo d1 el grado de p1(n), d2 el de p2(n) y as sucesivamente. Las ecuaciones
caractersticas tendrn el siguiente patrn:
k

(a0x + a1x

k-1

+ ...+ ak)(x - b1)

d1+1

(x - b2)

d1+2

...= 0 [6]
n

Resolvamos la siguiente recurrencia: T(n)= 2T(n-1) + n + 2 , n1 y T(0)= 0. En primer lugar


cambiemos la notacin y reorganicemos la expresin:
tn = 2tn-1 + n + 2n => tn - 2tn-1 = n + 2n
La parte derecha de la igualdad debemos expresarla de la forma:
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

tn=c11 + c2n1 + c32 + c4n2 = c1 + c2n + c32 + c4n2

Sustituyendo las condiciones iniciales y resolviendo el sistema de cuatro ecuaciones con


n
cuatro incgnitas, concluiremos que T(n) O(n2 ).
58

Anlisis de la eficiencia de los algoritmos.

3.8 Resolucin de recurrencias mediante cambio de


variable.
A menudo se pueden resolver recurrencias ms complicadas mediante un cambio de
variable. En esta seccin mostramos cmo se llevara a cabo con dos ejemplos. El primero lo
resolveremos cambiando de variable y seguidamente expandiendo. El segundo, tras realizar el
cambio de variable, obtenemos una ecuacin recurrente no homognea, que pasamos a resolver
como se ha comentado en la seccin anterior.
Esta ecuacin recurrente corresponde al tiempo de ejecucin de la bsqueda binaria
recursiva:
n
T ( ) + n, n 2
T ( n) = 2

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

) + 1, m 1, siendo el caso base T(2 )= 1. Realizamos varias expansiones:


m

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 )+i, m i


Particularizando para m=i para as poder eliminar la recurrencia, tendramos:
m

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

Al hacer el mismo cambio de variable que en el ejercicio anterior, se llega a


m

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

tm= c14 + c2k4 .


2

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).

[BB90] G. Brassard, P. Bratley. Algoritmica. Concepcin y anlisis. Masson. (1.990).

[CLR??] T.H. Cormen, C. E. Leiserson, R. L. Rivest. Introduction to algorithm. MIT Press.

59

Programacin II

60

[FGG98] J. Fernndez, A. Garrido, M. Garca. Estructuras de datos. Un enfoque prctico


usando C. Universidad de Granada (1.998).

[HSR97] E. Horowitz, S. Sahni, S. Rajasekaran. Computer algorithms. Computer Science


Press (1.997).

[Har92] D. Harel. Algorithms. The spirit of computing. 2 Edition. (1.992).

[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).

[Pe93] R. Pea. Diseo de programas. Formalismo y abstraccin. Prentice Hall (1.993).

[Sed98] R. Sedgewick. Algorithms in C++. 3 Edition. Addison-Wesley (1.998).

[Smi89] J.D. Smith. Design and analysis of algorithms. PWS-KENT publishing company
(1.989).

[Wei95] M. A. Weiss. Estructuras de datos y algoritmos. Addison-Wesley (1.995).

nd

rd

Anda mungkin juga menyukai