7 INTRODUCCIÓN A LA RECURSIVIDAD.

Anuncio
Laboratorio de Programación.
_____________________________________________________________________________
7
INTRODUCCIÓN A LA RECURSIVIDAD.
Contenido
_____________________________________________________________________________
7.1.- Concepto de recursión.
7.2.- Ejemplos de programas recursivos.
7.3.- Búsqueda y ordenación usando recursión.
2.3.1.- Búsqueda.
2.3.2.- Ordenación.
Ejercicios
_____________________________________________________________________________
7.1.- CONCEPTO DE RECURSION
Se dice que un proceso es recursivo si se puede definir en términos de si mismo, y a dicha
definición se le denomina definición recursiva. La recursividad es una nueva forma de ver las
acciones repetitivas permitiendo que un subprograma se llame a sí mismo para resolver una
versión más pequeña del problema original.
La función factorial es una función que se puede definir recursivamente y cuyo dominio
es el de los enteros positivos. La función factorial, que se representa con el símbolo de
exclamación, se define como:
n! = n X (n - 1) X (n - 2) X ... X 1
lo cual significa que n! es igual al producto de todos los enteros no negativos entre n y 1,
inclusivos. Consideremos los factoriales de los enteros no negativos del 1 al 5:
1! = 1
2! = 2 X 1
3! = 3 X 2 X 1
4! = 4 X 3 X 2 X 1
5! = 5 X 4 X 3 X 2 X 1
____________________________________________________________________________________
Introducción a la Recursión. Pág 1
Laboratorio de Programación.
_____________________________________________________________________________
Si nos fijamos atentamente en las operaciones anteriores, podremos extraer una propiedad
bastante interesante de la función factorial. Empecemos con el 5! , que es igual a:
5! = 5 X (4 X 3 X 2 X 1)
pero lo que hay dentro del paréntesis es 4!, es decir (5 - 1)!, lo cual significa que :
5! = 5 X 4!
Similarmente, podemos ver que:
4! = 4 X 3!
y que 3! = 3 X 2!, y así sucesivamente. Si definimos
0! = 1
entonces n! se puede definir como:
1

n! = 
n x (n − 1)!
î
si n = 0
si n > 0
Esta es la definición recursiva de la función factorial, ya que se define en términos de si
misma. La primera regla de la definición, o caso base, establece la condición de terminación. Las
definiciones recursivas nos permiten definir un conjunto infinito de objetos mediante una
sentencia finita.
La función factorial recursiva, se puede escribir como:
/**************************************************
* Autor:
* Fecha:
Versión:
***************************************************/
#include <iostream.h>
#include <stdlib.h>
int factorial(int param)
{
if (param == 0)
{
return 1 ;
}
else
{
return param * factorial(param-1) ;
}
}
int main()
{
int N;
cout << "Introduzca numero:";
cin >> N << endl;
____________________________________________________________________________________
Introducción a la Recursión. Pág 2
Laboratorio de Programación.
_____________________________________________________________________________
cout << factorial(N)<< endl;
system("PAUSE");
return 0;
}
Como puedes ver, la función Factorial se llama a si misma para hallar el valor de (N-1)!.
A este tipo de recursión, donde un procedimiento se llama a sí mismo, se le denomina recursión
explícita o recursión directa. Si un procedimiento P llama a otro Q, Q llama a R, R llama a S,
... , y Z llama de nuevo a P, entonces también tenemos recursión, y a este tipo de recursión se le
denomina recursión implícita o recursión indirecta.
Consideremos ahora la ejecución de la función recursiva Factorial. En C++, cuando se
llama a un procedimiento (o función), se guarda la dirección de la sentencia ‘llamante’ como
dirección de retorno, se asigna memoria a las variables locales del procedimiento, y al finalizar
la ejecución del procedimiento, se libera la memoria asignada a las variables locales y se
devuelve la ejecución al punto en que se hizo la llamada haciendo uso de la dirección de retorno.
Pero ¿qué es la dirección de retorno?. Cuando una sentencia escrita en un lenguaje de alto nivel
se traduce a código máquina, dicha sentencia suele representar varias líneas de código máquina,
es decir, la traducción no es una a una sino una a muchas. Cuando nos referimos a la dirección de
retorno, nos estamos refiriendo a la dirección de la instrucción que sigue a la instrucción de
llamada al procedimiento. Esta dirección podría estar en medio de la traducción de una sentencia
de alto nivel (como en el caso de las llamadas a funciones) o podría ser la primera instrucción de
la traducción de la sentencia de alto nivel, así como la propia llamada a la función.
Podemos extraer entonces las siguientes conclusiones:
1. Para poder resolver un problema de forma recursiva se debe poder definir
en términos de una versión más pequeña del mismo problema.
2. En cada llamada recursiva debe disminuir el tamaño del problema.
3. El diseño de la solución del problema ha de ser tal que asegure la
ejecución del caso base y por tanto, el fin del proceso recursivo.
Por otro lado, tenemos que estudiar dos conceptos más. Uno es la pila (stack) y el otro
son los registros de activación de los procedimientos. Una pila es una forma especial de
organizar la memoria en la que la información siempre se añade en la cima y la información que
se necesita, también se coge de la cima de la pila, al igual que una pila de hojas de papel. Debido
a este comportamiento a las pilas también se las conoce como estructuras ultimo-en-entrarprimero-en-salir (Last-In-First-Out=(LIFO)).
El registro de activación de un procedimiento es un bloque de memoria que contiene
información sobre las constantes y variables declaradas en el procedimiento, junto con una
dirección de retorno.
Ahora estamos en disposición de trazar la ejecución del programa para el caso de N = 3.
Para llevar a cabo la recursión, las computadoras usan pilas. Al comienzo de la ejecución de un
procedimiento, la pila está vacía (Figura 1.a). Cuando la ejecución alcanza la sentencia:
cout << factorial(N);
se produce una llamada a la función Factorial con N = 3. Esto hace que se cree un registro de
activación, y este registro se inserta en la pila (Figura 1b). Cuando se ejecuta la parte ELSE de la
función Factorial (cuando la sentencia IF no es TRUE), se produce una nueva llamada a
Factorial, pero esta vez el argumento es 3 -1 =2.
____________________________________________________________________________________
Introducción a la Recursión. Pág 3
Laboratorio de Programación.
_____________________________________________________________________________
RF
RF
RF
RF
N=1
A
N=3
(a)
RF
N=2
N=2
A
A
N=3
N=3
B
B
B
(b)
(c)
(d)
N=0
A
RF
N=1
N=1
RF
1
1
A
A
N=2
N=2
N=2
A
A
A
N=3
N=3
N=3
B
(e)
B
RF
(f)
6
(i)
RF
RF
2
N=3
B
B
(g)
(h)
6
(j)
Figura 1. Fases de la evolución de la pila durante la ejecución del programa
Esta invocación se insertará en la cima de la pila (Figura 1.c). Esta vez, la dirección de
retorno es la “A”. Debido a esta nueva invocación se tiene que volver a ejecutar el programa
Factorial. La condición de la sentencia if aún es FALSE, y por tanto se volverá a invocar a la
parte ELSE, pero esta vez con 2-1 =1 como argumento. Se inserta en la cima de la pila el registro
de activación de esta nueva invocación (Figura 1.d). Una vez más se tiene que volver a ejecutar
el procedimiento Factorial , en este caso el if también es FALSE y al ejecutarse la parte else se
volverá a llamar a Factorial pero esta vez con argumento 1-1=0 (Figura 1.e). En esta
invocación la condición del if se evalúa a TRUE y se devuelve un 1. Lo primero que se hace es
almacenar este valor en el registro de la función (RF), y usando la dirección de retorno del
registro de activación podremos volver a la sentencia que hizo la llamada. En este instante, como
se ha completado la ejecución del procedimiento Factorial(cuando N = 0), el computador no
necesitará este último registro de activación, así que se puede eliminar (Figura 1.f). La sentencia
a la que retornamos necesita el valor de la función Factorial(cuando N = 0), que se puede
obtener del registro de la función (RF). Este valor se multiplicará por el valor de N que es 1. El
resultado se copiará en el registro de la función y así concluirá la ejecución de
Factorial(cuando N = 1) , además su registro de activación será borrado (Figura 1.g). Los
pasos anteriores se volverán a repetir dando lugar a un valor de 2 en el registro de la función y a
un solo registro de activación en la pila (Figura 1.h). Esta vez, el valor obtenido del registro de la
____________________________________________________________________________________
Introducción a la Recursión. Pág 4
Laboratorio de Programación.
_____________________________________________________________________________
función, que es 2, será multiplicado por N, cuyo valor actual es 3; el resultado, 6, se copiará en el
registro de la función, y se usará la dirección “B”, con lo que retornaremos a la localización de la
llamada original. Al final, la pila estará de nuevo vacía (Figura 1.j).
Debido a la sobrecarga (overhead) que producen las operaciones sobre la pila, la creación
y borrado de los registros de activación, los procedimientos recursivos consumen más tiempo y
memoria que los programas no recursivos. Pero, algunas veces, debido a la estructura de datos
usada en el problema o al planteamiento del mismo, surge de forma natural, y evitar la recursión
es bastante más difícil que dar una solución recursiva al problema.
7.2.- EJEMPLOS DE PROGRAMAS RECURSIVOS
La recursión, si se usa con cuidado, nos permitirá solucionar de forma elegante algunos
problemas. En este apartado se van a resolver varios problemas usando procedimientos
recursivos. En cada caso, sugerimos que antes de que veas el algoritmo no iterativo, lo intentes
codificar tu mismo. Empezaremos con un problema simple. El término n-ésimo de la sucesión de
Fibonacci se puede definir como:
1

F(N ) = 
 F ( N − 1) + F ( N − 2)
î
si N = 1 ó N = 2
si N > 2
Puedes observar que en esta definición ,al igual que en la definición de la función factorial,
tenemos un caso base que nos permite conocer el valor de la función. Esta regla es la condición
de terminación. La otra regla es la relación de recurrencia. El ejemplo 1 ilustra un programa
que lee varios valores de N y calcula el número correspondiente de la sucesión de Fibonacci.
Ejemplo1. Programa que computa el N-ésimo término de la sucesión de
haciendo uso de una función recursiva .
Fibonacci
/**************************************************
* Autor:
* Fecha:
Versión:
***************************************************/
#include <iostream.h>
#include <stdlib.h>
double Fib(double N)
{
if (N<= 2)
{
return 1;
}
else
{
return Fib(N-1) + Fib(N - 2);
}
}
____________________________________________________________________________________
Introducción a la Recursión. Pág 5
Laboratorio de Programación.
_____________________________________________________________________________
int main()
{
double i, N;
cout << "Teclea un entero positivo: ";
cin>> N << endl;
for (i=1; i<=N; i++)
{
cout << i<< " ésimo término de Fibonacci es: ";
cout << Fib(i)<< endl;
}
system("PAUSE");
return 0;
}
Analicemos ahora la ejecución de este programa; cuando N = 6 vamos a hallar el número
de veces que se invoca la función Fib. Para hallar Fib(6), previamente tenemos que hallar Fib(5)
y Fib(4); para hallar Fib(4), tenemos que hallar Fib(3) y Fib(2); y así sucesivamente. Las
invocaciones de la función Fib se pueden ilustrar de la siguiente forma:
Fib(6)
Fib(5)
Fib(4)
Fib(3)
Fib(2)
Fib(4)
Fib(3)
Fib(3)
Fib(2)
Fib(1)
Fib(2)
Fib(2)
Fib(1)
Fib(1)
Como se puede observar en el diagrama anterior, para hallar F(6) tenemos que llamar a la
función Fib 15 veces. De esas 15 llamadas, tres tienen como argumento el 1, cinco tienen como
argumento el 2, tres tienen como argumento el 3, dos tienen el 4 como argumento y con
argumentos 5 y 6 sólo hay dos llamadas. Esto significa que el primer número de la serie de
Fibonacci, se calcula tres veces, el segundo se calcula 5 veces, etc. Este análisis demuestra el por
qué una función recursiva puede llegar a ser una herramienta muy costosa para solucionar un
problema. El Ejemplo 2 muestra la versión iterativa, la cual es mucho más eficiente y fácil de
escribir.
____________________________________________________________________________________
Introducción a la Recursión. Pág 6
Laboratorio de Programación.
_____________________________________________________________________________
Ejemplo2. Programa que computa el N-ésimo término de la sucesión de Fibonacci
sin usar recursión.
/**************************************************
* Autor:
* Fecha:
Versión:
***************************************************/
#include <iostream.h>
#include <stdlib.h>
double Fib(double N)
{
double i, Primero, Segundo, Siguiente;
Primero = 1;
Segundo = 1;
Siguiente = 1;
for (i=3; i<=N ; i++)
{
Siguiente = Primero + Segundo;
Primero = Segundo;
Segundo = Siguiente;
}
return Siguiente;
}
int main()
{
double i,N;
cout << "Teclea un entero positivo: " ;
cin >> N;
for (i=1; i<=N; i++)
{
cout << i << " i-esimo termino de Fibonacci es: ";
cout << Fib(i);
cout << endl;
}
system("PAUSE");
return 0;
}
Aunque el programa del ejemplo anterior es más eficiente, en este programa es más difícil
ver la relación entre las sentencias de la función Fib y la definición de la función Fibonacci.
Una forma de hacer que el procedimiento recursivo sea más eficiente es la de usar algún
tipo de memoria que nos permita ‘recordar’ los cálculos que ya hemos realizado y que por tanto
no tengamos que repetirlos. Dicha memoria podría ser un array que inicialicemos a cero ya que
todos los números que componen la sucesión de Fibonacci son mayores que cero. Podemos usar
un array de tamaño 10. Asumimos que el i-ésimo elemento de este array se corresponde con el iésimo término de la sucesión de Fibonacci.
Al principio, conocemos los dos primeros términos de la sucesión de Fibonacci. Durante el
cálculo del N-ésimo término de la sucesión de Fibonacci, si N es menor o igual que 10,
comprobaremos el N-ésimo elemento de dicho array. Si dicho elemento es mayor que cero,
simplemente devolveremos ese valor; en otro caso tendremos que calcular el N-ésimo término de
la sucesión de Fibonacci, almacenarlo en la posición correspondiente del array y devolverlo al
programa principal. Si N es mayor que 10, tendremos que calcular su valor, pero eventualmente
dicha computación hará uso de los valores ya precalculados y almacenados en el array. El
Ejemplo 3 usa esta técnica para calcular el N-ésimo término de Fibonacci. Esta aproximación
____________________________________________________________________________________
Introducción a la Recursión. Pág 7
Laboratorio de Programación.
_____________________________________________________________________________
reduce el número de llamadas al procedimiento; por tanto su ejecución ahorra tiempo a expensas
de hacer uso de más espacio de memoria.
Ejemplo3. Programa que computa el N-ésimo término de la sucesión de
usando un algoritmo recursivo con memoria .
Fibonacci
/**************************************************
* Autor:
* Fecha:
Versión:
***************************************************/
#include <iostream.h>
#include <stdlib.h>
const int Tamanyo = 10;
typedef int TipoMemoria[Tamanyo];
TipoMemoria Memoria;
void DefinirMemoria( TipoMemoria &M)
{
int i;
M[1] = 1;
M[2] = 1;
for (i=3; i<=Tamanyo; i++)
{
M[i] = 0;
}
}
int Fib(int N)
{
int K ;
if (N < Tamanyo)
{
if (Memoria[N] != 0)
{
return Memoria[N];
}
else
{
K = Fib(N-1) + Fib(N-2);
Memoria[N] = K;
return K;
}
}
else
{
return (Fib(N-1) + Fib(N-2));
}
}
int main()
{
int N;
DefinirMemoria(Memoria);
Cout << "¿Termino Fibonacci?: ";
Cin >> N ;
Cout << endl;
if (N > 0)
{
cout << N;
cout << " ésimo término de Fibonacci es: ";
cout << Fib(N);
}
else
{
cout<<"Error en la entrada.
";
____________________________________________________________________________________
Introducción a la Recursión. Pág 8
Laboratorio de Programación.
_____________________________________________________________________________
cout << " N debería ser mayor que cero.";
}
system("PAUSE");
return 0;
}
Ahora resolveremos un problema donde la recursión simplifica nuestro programa. El programa
consiste en encontrar el equivalente binario de los números decimales. Como su nombre indica,
los números decimales se escriben usando diez dígitos (0 al 9) y los números binarios se escriben
usando dos dígitos (0,1). La representación binaria de un número decimal se puede hallar
fácilmente considerando el siguiente algoritmo:
Sea N un número positivo decimal. Hallar N % 2 y N / 2, y almacenamos el primero.
Reemplazamos N por N/2. Repetimos el proceso hasta que N sea igual a cero. Ahora, si
reescribes los valores almacenados en sentido contrario a como los obtuviste, obtendrás el
equivalente binario de N. Por ejemplo: supongamos que N es 23.
N
N%2
23
11
5
2
1
0
1
1
1
0
1
N/2
11
5
2
1
0
La representación binaria de 23 es
10111
Para resolver este problema sin usar recursión, necesitaremos un array para almacenar los dígitos
binarios que vamos calculando, para después poder escribirlos al revés. El problema a la hora de
usar arrays es que no conocemos el número de elementos que vamos a usar del array. Veamos,
ahora, como nos puede ayudar la recursión. El equivalente binario de 11, que es igual a 23 / 2, es
1011
Así que, si podemos imprimir el equivalente binario de 11, será fácil escribir el
equivalente binario del 23; lo único que tenemos que hacer es imprimir 23 % 2. Ahora
observemos lo siguiente. El equivalente binario de 5, que es igual a 11 / 2, es 101. Si añadimos
11 % 2 = 1, nos da el equivalente binario del 11. Por tanto, si consideramos la representación
binaria de un número como una cadena de dígitos binarios, podemos llegar a la siguiente
definición:
0 si N = 0

Representación Binaria de N = 1 si N = 1
Representación binaria de( N / 2)||( N %2)
î
donde || representa la concatenación. Esta aproximación no requiere arrays y puede ser
programada fácilmente. El programa se ilustra en el ejemplo 4.
____________________________________________________________________________________
Introducción a la Recursión. Pág 9
Laboratorio de Programación.
_____________________________________________________________________________
Ejemplo4. Programa para hallar el equivalente binario de los números decimales.
/**************************************************
* Autor:
* Fecha:
Versión:
***************************************************/
#include <iostream.h>
#include <stdlib.h>
const int Base = 2;
int N;
void PrintBinario(int N)
{
if (N > 0)
{
PrintBinario(N/Base);
Cout << N % Base;
}
}
// Imprime equivalente binario de N/2
int main()
{
cout << "Introduzca un entero positivo: ";
cin >> N ;
cout << endl;
cout << " El Número Decimal "<< N <<" es igual a ";
PrintBinario(N);
Cout << " en binario " << endl;
system("PAUSE");
return 0;
}
Un ejemplo simple de la ejecución de este programa sería:
Entra un entero positivo: 256
El Número Decimal 26 es igual a 100000000 en binario.
Entra un entero positivo: 32767
El Número Decimal 32767 es igual a 111111111111111 en binario.
Entra un entero positivo: 23
El Número Decimal 23 es igual a 10111 en binario.
Entra un entero positivo:
El programa del Ejemplo 4 se puede modificar para hallar la representación de un número
decimal en cualquier base B. Debemos notar que, si la nueva base B es mayor que 10,
necesitaremos más símbolos además de los dígitos del 1 al 9. Tradicionalmente, se han usado los
caracteres A, B, C, D, ..., y así sucesivamente para representar el 10,11, 12, 13, ... . Por ejemplo,
el número 30 en decimal es equivalente en hexadecimal (B=16) a 1E, donde E representa el 14.
Para manipular estos caracteres en este programa modificado, introducimos un array de
caracteres llamado Dígitos. En este nuevo programa, en vez de imprimir el valor de
N % Base
____________________________________________________________________________________
Introducción a la Recursión. Pág 10
Laboratorio de Programación.
_____________________________________________________________________________
usaremos el valor obtenido, para indexar el array Digitos e imprimir el carácter almacenado en
dicho elemento (Ejemplo 5).
Ejemplo5. Programa para convertir números decimales a cualquier base hasta la
base 16
/**************************************************
* Autor:
* Fecha:
Versión:
***************************************************/
#include <iostream.h>
#include <stdlib.h>
//Límite superior de la base
const int BaseMax = 16;
typedef char TDigitos[BaseMax];
int N, NuevaBase;
TDigitos Digitos;
void DefinirDigitos(TDigitos &Digitos)
{
char C;
int i;
C = '0';
for (i=0; i<=9; i++)
{
Digitos[i] = C;
C++;
}
C = 'A';
for (i=10; i<=BaseMax-1; i++)
{
Digitos[i] = C;
C++;
}
}
void PrintNuevaBase(int N,int NuevaBase)
{
if (N > 0)
{
PrintNuevaBase(N / NuevaBase, NuevaBase);
cout<<Digitos[N % NuevaBase];
}
}
int main()
{
DefinirDigitos(Digitos);
Cout << "Introduzca un entero positivo: ";
Cin >> N;
Cout << endl;
Cout << "Entra la nueva base (2-16): ";
Cin >> NuevaBase;
Cout << endl;
Cout << " El Número Decimal "<<N<<" es igual a ";
PrintNuevaBase(N, NuevaBase);
Cout << " en base "<<NuevaBase<<endl;
system("PAUSE");
return 0;
}
Un ejemplo simple de la ejecución de este programa sería:
____________________________________________________________________________________
Introducción a la Recursión. Pág 11
Laboratorio de Programación.
_____________________________________________________________________________
Entra un entero positivo: 123
Entra la nueva base (2-16): 16
El Número Decimal 123 es igual a 7B en base 16.
Entra un entero positivo: 32676
Entra la nueva base (2-16): 8
El Número Decimal 32676 es igual a 77644 en base 8.
Entra un entero positivo: 32676
Entra la nueva base (2-16): 10
El Número Decimal 32676 es igual a 32676 en base 8.
Entra un entero positivo: 32676
Entra la nueva base (2-16): 12
El Número Decimal 32676 es igual a 16AB0 en base 12.
7.3.- BUSQUEDA Y ORDENACION USANDO RECURSION
7.3.1.- Búsqueda
Asumimos que tenemos un array llamado Ordenado de N elementos y que está ordenado en
orden creciente. Consideremos el problema de determinar si un elemento dado, Item, está
presente en dicho array. Si Item está presente en la lista, nos gustaría determinar su localización
(Loc). Si Item no está presente en la lista, deberemos poner Loc a cero.
Este problema se puede simplificar si reducimos el tamaño del array. Una forma de hacer
esto es dividir el array en dos partes cogiendo el elemento X-ésimo del array, donde X es un
índice seleccionado al azar entre 1 y N. Se nos presentan así tres posibilidades:.
1. Ordenado[X] = Item : En este caso, ya hemos encontrado el Item en el array, y
ponemos Loc a X.
2. Ordenado[X] > Item : No podemos decir si el Item está o no en el array, pero ya que
los elementos del array están en orden creciente, podemos ignorar la segunda parte del
array;la siguiente vez, consideraremos sólo los elementos desde el primero al X-1.
3. Ordenado[X] < Item : No podemos decir si el Item está o no en el array, pero ya que
los elementos del array están en orden creciente, podemos ignorar la primera parte del
array; la siguiente vez, consideraremos sólo los elementos desde el X+1 al N.
En los casos 2 y 3, el tamaño del array se reduce, y el mismo proceso, seleccionar un
índice aleatorio X entre los valores del índice válidos (entre 1 y X-1 si se usa la primera parte;
entre X +1 y N se usa la segunda parte) y comparar ese elemento con Item, se puede repetir
sobre una porción más pequeña del array Ordenado. Eventualmente el tamaño del array bajo
consideración será cero si el Item no está en el array; en otro caso se habrá localizado
previamente. En vez de usar un índice X aleatorio , se suele coger la mitad del array. A este
método se le denomina búsqueda binaria porque el elemento intermedio de un array divide al
array en dos partes iguales o casi iguales (Si N es par). El programa principal del Ejemplo 6 tiene
un programa principal que primero lee el número de elementos que va a contener el array
Ordenado y que después lee los elementos. Dentro del bucle WHILE que sigue a la parte de
entrada, el programa lee el Item a localizar y después invoca al procedimiento Buscar, que
requiere cinco parámetros. Estos parámetros son, en orden:
2.Array Ordenado.
____________________________________________________________________________________
Introducción a la Recursión. Pág 12
Laboratorio de Programación.
_____________________________________________________________________________
3.Primero: Cota inferior de la parte del array Ordenado en el que estamos
interesados.
4.Ultimo: Cota superior de la parte del array Ordenado en el que estamos
interesados.
5.Item: El valor que estamos buscando.
6.Loc: Si se encuentra el Item, Loc contendrá un índice a la posición dentro del
array donde está el Item, sino contendrá cero.
El procedimiento Buscar , después de comparar con el elemento mitad de la porción actual del
array que estamos tratando, decide qué parte de la porción actual hay que ignorar. Si el elemento
intermedio es menor que el Item, se ignora la primera parte, y se llama otra vez a Buscar con las
cotas inferiores y superiores configuradas con los valores Mitad+1 y Ultimo. En otro caso, se
llaman con las cotas Primero y Mitad-1 como cotas inferior y superior.
Ejemplo6. Búsqueda Binaria recursiva
/**************************************************
* Autor:
* Fecha:
Versión:
***************************************************/
#include <iostream.h>
#include <stdlib.h>
// Tamaño del array
const int Tamanyo = 100;
typedef int Lista[Tamanyo];
Lista Ordenado;
int N, i, Loc,Item;
void Buscar(Lista Ordenado, int Primero, int Ultimo, int Item, int &Loc)
{
int Mitad;
if (Primero > Ultimo)
{
Loc = 0;
}
else
{
Mitad = (Ultimo + Primero) / 2;
if (Ordenado[Mitad] == Item)
{
Loc = Mitad;
}
else
{
if (Ordenado[Mitad] < Item)
{
Buscar(Ordenado, Mitad+1, Ultimo, Item, Loc);
}
else
{
Buscar(Ordenado, Primero, Mitad-1, Item, Loc);
}
}
}
}
int main()
{
// Lectura del array Ordenado
cout << "Número de elementos en Ordenado: ";
cin >> N;
cout << endl;
if (N > Tamanyo)
{
N = Tamanyo; // No se pueden leer más de Tamanyo elementos
____________________________________________________________________________________
Introducción a la Recursión. Pág 13
Laboratorio de Programación.
_____________________________________________________________________________
}
cout<< "Introduzca los elementos: ";
for ( i = 0; i<=(N-1);i++)
{
cin >> Ordenado[i] ;
}
cout << endl;
cout << "Item a Buscar: ";
cin >> Item ;
Buscar(Ordenado, 0, N-1, Item, Loc);
cout << Item ;
if (Loc == 0)
{
cout << " no está en la lista.";
}
else
{
cout << " es igual al ";
cout << Loc;
cout << "-esimo elemento de la lista.";
}
cout << endl;
system("PAUSE");
return 0;
}
7.3.2.- Ordenación
La ordenación está muy relacionada con la mezcla. Mezclar es un término usado por un proceso
que combina dos listas ordenadas preservando el orden. Examinemos la mezcla de dos listas
ordenadas (asumiremos que nuestras listas contienen enteros y están ordenadas de forma
creciente) antes de ver el papel de la mezcla en la ordenación.
El proceso de mezcla se puede realizar seleccionando sucesivamente el valor más
pequeño que aparece en cualquiera de las dos listas y moviendo dicho valor a una nueva lista,
creando así una lista ordenada. Por ejemplo, mientras mezclamos las siguientes dos listas:
Lista 1: 13 27 58
Lista 2: 8 36
podemos obtener las siguientes trazas:
Lista 1: 13 27 58
Lista 2:36
Nueva Lista : 8
Lista 1: 27 58
Lista 2:36
Nueva Lista : 8 13
Lista 1: 58
Lista 2:36
Nueva Lista : 8 13 27
Lista 1: 58
Lista 2 :
Nueva Lista : 8 13 17 36
____________________________________________________________________________________
Introducción a la Recursión. Pág 14
Laboratorio de Programación.
_____________________________________________________________________________
Lista 1:
Lista 2:
Nueva Lista : 8 13 17 36 58
Si estas dos listas se almacenan en el mismo array, dicho array tendrá el siguiente aspecto:
Lista:
13
Primero
27
58
8
36
Mitad
Ultimo
donde los elementos Primero a Mitad-1 representan a la Lista1, mientras que los elementos
Mitad a Ultimo representa a la Lista2. En el proceso de mezcla, se va a necesitar un área
temporal (Temp) ,del mismo tamaño que la Lista, para almacenar los resultados. Al final, Temp
se volverá a copiar en la Lista. El procedimiento para realizar la operación de mezcla se ilustra a
continuación:
void MezclaSimple(Lista &K, int Primero, int Mitad, int Ultimo)
// Mezcla dos listas que están ordenadas en el mismo array
{
Lista Temp;
int Indice1, Indice2, IndiceMezcla, i;
Indice1 = Primero;
Indice2 = Mitad;
// Inicializa el número de elementos en Temp
IndiceMezcla = 0;
/* Mientras haya elementos en cualquier parte de las listas ordenadas,
mezclalos */
while((Indice1 < Mitad) && (Indice2<= Ultimo))
{
IndiceMezcla++;
if (K[Indice1] <= K[Indice2])
{
Temp[IndiceMezcla] = K[Indice1];
Indice1++;
}
else
{
Temp[IndiceMezcla] = K[Indice2];
Indice2++;
}
}
/* En este punto o la primera parte o la segunda parte
está agotada. Copiamos cualquier parte remanente de la
segunda parte a Temp. */
while (Indice2 <= Ultimo)
{
IndiceMezcla++;
Temp[IndiceMezcla] = K[Indice2];
Indice2++;
}
// Copiamos cualquier parte que quede de la primera parte a Temp
____________________________________________________________________________________
Introducción a la Recursión. Pág 15
Laboratorio de Programación.
_____________________________________________________________________________
while (Indice1 < Mitad)
{
IndiceMezcla++;
Temp[IndiceMezcla] = K[Indice1];
Indice1++;
}
/* Copiamos Temp en el array K*/
for (i=1 ;i<=IndiceMezcla; i++)
{
K[Primero+i-1] = Temp[i];
}
}
Este proceso de mezcla simple se puede generalizar para mezclar K listas ordenadas en
una sola lista ordenada. A este proceso se le llama mezcla múltiple. La mezcla múltiple se puede
llevar a cabo realizando una mezcla simple, repetidamente. Por ejemplo, si tenemos ocho listas
para mezclar, podemos mezclarlas a pares para obtener así cuatro listas ordenadas. Estas listas, a
su vez, se pueden mezclar a pares para obtener dos listas ordenas. Y una operación de mezcla
final nos dará la lista ordenada.
Veamos ahora como se puede usar la mezcla para ordenar un array de ocho elementos.
Asumimos que la lista contiene:
81
8
13
7
79
54
1
5
Dividamos esta lista en dos partes iguales (una barra vertical indica el punto de división).
Obtenemos:
81
8
13
7
|
79
54
1
5
Como no están ordenadas, no podemos mezclar estas dos listas. Dividamos la primera sublista en
dos piezas:
81
|
8
13
7
|
79
8
|
13
7
|
54
1
5
54
1
Una vez más:
81
|
79
5
La primeras dos sublistas contienen sólo un elemento, y obviamente una lista que contiene un
solo elemento está ordenada(!). Esto significa que se pueden mezclar estas dos obteniendo
8
81
|
13
7
|
79
54
1
5
Ahora, si la parte B de esta lista se divide en dos partes y las listas resultantes formadas por
elementos simples se mezclan, obtenemos:
8
81
|
7
13
|
79
54
1
5
Como las dos primeras listas están ahora ordenadas, podemos mezclarlas :
7
8
13
81
|
79
54
1
5
Como puedes ver, la primera mitad del array ya está ordenada. Si aplicamos la misma técnica a la
segunda sublista y después las mezclamos con la primera, estará ordenado todo el array. En el
____________________________________________________________________________________
Introducción a la Recursión. Pág 16
Laboratorio de Programación.
_____________________________________________________________________________
Ejemplo 7, el procedimiento OrdenacionPorMezcla divide el array de entrada en sublistas
más y más pequeñas hasta que alcancen el tamaño de un elemento. Entonces, usando el
procedimiento MezclaSimple, se mezclan esas sublistas, obteníendose la lista ordenada.
Ejemplo7. Programa recursivo para la ordenación por mezcla.
/**************************************************
* Autor:
* Fecha:
Versión:
***************************************************/
#include <iostream.h>
#include <stdlib.h>
// Tamaño del array
const int Tamanyo = 10;
typedef int Lista[Tamanyo];
void MezclaSimple(Lista &K, int Primero,int Mitad,int Ultimo)
// Mezcla dos listas que están ordenadas en el mismo array
// El cuerpo del procedimiento anterior
void OrdenacionPorMezcla(Lista &K,int Primero, int Ultimo)
{ // Ordena el array K usando ordenación por mezcla recursiva
int Tam, Mitad;
// Determina el tamaño de la porción actual
Tam = Ultimo - Primero + 1;
if (Tam <= 1) // No más divisiones
{
return;
}
else
{ // Divide en dos y ordena
Mitad = Primero + Tam / 2 -1;
OrdenacionPorMezcla(K, Primero, Mitad);
OrdenacionPorMezcla(K, Mitad+1, Ultimo);
// Mezcla ambas mitades ordenadas
MezclaSimple(K, Primero, Mitad+1,Ultimo);
}
}
int main()
{
Lista Desordenado;
int Elemento ,Primero, Segundo, Tercero, i, K;
// Entrada de datos desordenados
i = 0;
cin >> K;
while(i < Tamanyo-1)
{
i++;
Desordenado[i] = K;
Cin >> K;
}
Tercero = i;
OrdenacionPorMezcla(Desordenado, 1, Tercero);
// Imprime el array ordenado
for( i = 1; i<=Tercero; i++)
{
cout<<Desordenado[i]<<endl;
}
system("PAUSE");
return 0;
}
____________________________________________________________________________________
Introducción a la Recursión. Pág 17
Laboratorio de Programación.
_____________________________________________________________________________
EJERCICIOS
1.- ¿ Cual será la salida del siguiente programa ?. Intenta resolverlo primero sobre papel.
#include <iostream.h>
#include <stdlib.h>
void ImprimeResto()
{
const char Punto='.';
char X;
cin >> X;
if (X != Punto)
{
ImprimeResto();
}
cout << X;
}
int main()
{
ImprimeResto();
cout << endl;
system("PAUSE");
return 0;
}
si la entrada es :
Si algo puede salir mal, saldrá mal.
2.- ¿ Cual será la salida del siguiente programa ?. Intenta resolverlo primero sobre papel.
#include <iostream.h>
#include <stdlib.h>
int Uno(int A, int B)
{
if (B != 0)
{
A++;
B--;
return Uno(A,B);
}
else
{
return A ;
}
}
int main()
{
cout << Uno(5,6) << endl;
system("PAUSE");
return 0;
}
____________________________________________________________________________________
Introducción a la Recursión. Pág 18
Laboratorio de Programación.
_____________________________________________________________________________
3.- La relación recurrente
1

x n =  x * x n −1
 n +1
î x /x
si n = 0
si n > 0
si n < 0
define x como la potencia n-ésima de todos los enteros. Escribir un procedimiento recursivo
llamado Potencia que calcule x a la n-ésima potencia para un entero x.
____________________________________________________________________________________
Introducción a la Recursión. Pág 19
Laboratorio de Programación.
_____________________________________________________________________________
4.- El máximo común divisor de dos enteros positivos M y N se puede definir como:
N

MCD( M , N ) =  MCD( N , M )
 MCD( N , M mod N )
î
si N ≤ M y M mod N = 0
si M < N
en otro caso
Escribir una función recursiva que permita calcular el máximo común divisor de dos enteros
positivos.
7.- En general, el N-ésimo término de la sucesión de Fibonacci, se define como:
A

F ( A, B, N ) =  B
 F ( B, A + B, N − 1)
î
si N = 1
si N = 2
si N > 2
Donde A y B son los números que originan la secuencia. Escribir un programa que compute el Nésimo término de la sucesión de Fibonacci usando diferentes números para originar la secuencia.
8.- La función Q se define como:
1

Q( N ) = 
 Q ( N − Q ( N − 1))
î
si N ≤ 2
si N > 2
Escribir dos procedimientos, uno recursivo y otro no recursivo, para computar el valor de esta
función para varios valores de N. ¿Puedes escribir una función que haga uso de más memoria
para así aumentar la velocidad de los cálculos recursivos?.
9.- El valor del N-ésimo término del polinomio de Legendre se puede calcular usando las
siguientes fórmulas:
P0(x) = 1
P1(x) = x
PN ( x ) =
2* N − 1
N −1
* x * PN −1 ( x ) −
* PN − 2 ( x )
N
N
Escribir una furnción recursiva que compute el N-ésimo término del polinomio de Legendre.
____________________________________________________________________________________
Introducción a la Recursión. Pág 20
Laboratorio de Programación.
_____________________________________________________________________________
10.- Escribir una función recursiva para computar la función de Ackermann, la cual se define
como:
N + 1
si M = 0

A(M, N) =  A(M − 1, N)
si M ≠ 0 and N = 0
 A(M -1, A(M, N -1))
si M ≠ 0 and N ≠ 0
î
donde M y N son números cardinales.
11.- Para cualesquiera enteros no negativos N y K, donde 0<=K<=N, el coeficiente binomial
C(N,K) se define como:
C( N , K ) =
N!
K !* ( N − K )!
Esta fórmula verifica que:
1. C(N,K) = C(N, N-K); es decir, la lista de coeficientes binomiales es simétrica con
respecto a su valor central.
2. C(N,N) = 1 y por simetría C(N,0) = 1.
3. C(N,N-1)=N y por simetría C(N,1) = N.
El problema para computar C(N,K) haciendo uso de la fórmula anterior estriba en que N! crece
muy rápidamente.Por lo que necesitamos un método alternativo para calcular dichos coeficientes.
Para ello podemos hacer uso del hecho de que para cualquier K que satisfaga 1<=K<=N-1:
C( N , K ) =
C ( N , K − 1) * ( N − K + 1)
K
Escribir un programa que calcule el coeficiente binomial C(N,K) para varios N y K.
12.- Un programa bastante popular es hacer que un ratón encuentre un queso de cabrales en un
laberinto. Vamos a suponer que el laberinto es un recinto rectangular dividido en cuadrados,
estando cada cuadrado ocupado por un obstáculo o libre. El perímetro del rectángulo está
ocupado por obstáculos, excepto en una o más salidas. Comenzamos en algún lugar dentro del
laberinto, y tenemos que encontrar el camino de salida. Podemos movernos de cuadrado en
cuadrado en cualquier dirección (excepto diagonalmente), pero no podemos atravesar un
obstáculo.
Podemos representar el laberinto en un ordenador mediante un array de caracteres ( o
booleano) bidimensional, por ejemplo:
BBBBBBBBBBBB
B..BBB....BB
BB...B.BBB.B
B..B.B.B...B
BBB....BBBBB
B...BBB....B
B.B.....BB.B
BBBBBBBBBB.B
Construir un programa recursivo que tenga como entrada la posición del ratón dentro del
laberinto y que determine una de sus posibles salidas.
____________________________________________________________________________________
Introducción a la Recursión. Pág 21
Laboratorio de Programación.
_____________________________________________________________________________
(* El siguiente ejercicio no se debe incluir en la relación de problemas *)
12.- El método de ordenación rápida (QuickSort) es un método de ordenación rápido y
relativamente moderno desarrollado por Hoare. Es esencialmente un esquema de
inserción/intercambio que, en cada etapa, coloca al menos un elemento en su sitio adecuado.
Saber que este elemento está bien posicionado se usa para reducir el número de comparaciones
que se necesitan para posicionar el resto de elementos.
La filosofía del QuickSort consiste en dividir la lista de elementos tomando como
referencia un determinado elemento, llamado pivote, de forma que, en las listas resultantes de la
división, todas los elementos que precedan al pivote sean menores o iguales al mismo y todos los
elementos que estén después del pivote sean mayores o iguales que el mismo.
Izqda
IFin
Dinicio
Dcha
. . .
≤pivote
≥pivote
pivote
Como resultado de la partición, el pivote está correctamente colocado. El mismo proceso
de partición se aplica entonces a las dos sublistas a ambos lados del pivote. De esta forma la lista
original es sistemáticamente reducida a una serie de sublistas, cada una de longitud uno, que
están, por supuesto, ordenadas y, más importante, correctamente posicionadas en relación a las
otras.
La característica más importante de este algoritmo es la partición. La entrada seleccionada
como el pivote suele ser la primera entrada. Habiendo elegido el pivote, el algoritmo busca en la
lista ,desde dicho pivote hasta el final, el primer elemento que no pertenezca a ese lado del
pivote. Estos dos elementos se intercambian, y se reanuda la búsqueda con los elementos
adyacentes. Cuando coinciden dos búsquedas, el pivote está posicionado entre dos sublistas en
orden para mantener la ordenación relativa.
Dados los índices de los elementos más a la izquierda (Izquierda) y a la derecha
(Derecha) de la sublista a ordenar, el algoritmo queda como:
SI Izquierda < Derecha
parte Lista[Izquierda] a Lista[Derecha] en dos secciones
más pequeñas de forma que:
Lista[Izquierda] ...Lista[Final] <= Lista[Pivote] <=
Lista[DechaInicio] ... Lista[Derecha]
ordena Lista[Izquierda]...Lista[Final]
ordena Lista[DechaInicio]...Lista[Derecha]
Escribir un programa que ordene una lista de enteros usando este método.
____________________________________________________________________________________
Introducción a la Recursión. Pág 22
Descargar