PDF de programación - Capítulo 4 - Diseño de algoritmos recursivos

Imágen de pdf Capítulo 4 - Diseño de algoritmos recursivos

Capítulo 4 - Diseño de algoritmos recursivosgráfica de visualizaciones

Publicado el 23 de Julio del 2019
692 visualizaciones desde el 23 de Julio del 2019
429,2 KB
44 paginas
Creado hace 8a (25/11/2015)
Diseño de algoritmos recursivos1

Capítulo 4

Para entender la recursividad
primero hay que entender la recursividad

Anónimo

Resumen: En este tema se presenta el mecanismo de la recursividad como una
manera de diseñar algoritmos fiables y fáciles de entender. Se introducen distintas
técnicas para diseñar algoritmos recursivos, analizar su complejidad, modificarlos para
mejorar esta complejidad y verificar su corrección.

1.

Introducción











La recursión aparece de forma natural en programación, tanto en la definición de
tipos de datos (como veremos en temas posteriores) como en el diseño de algoritmos.

Hay lenguajes (funcionales y lógicos) en los que no hay iteración (no hay bucles),
sólo hay recursión. En C++ tenemos la opción de elegir entre iteración y recursión.

Optamos por una solución recursiva cuando sabemos cómo resolver de manera directa
un problema para un cierto conjunto de datos y para el resto de los datos somos
capaces de resolverlo utilizando la solución al mismo problema con unos datos “más
simples”.

Cualquier solución recursiva se basa en un análisis (clasificación) de los datos, ~x, para
distinguir los casos de solución directa y los casos de solución recursiva:

caso(s) directo(s): ~x es tal que el resultado ~y puede calcularse directamente de
forma sencilla.

caso(s) recursivo(s): sabemos cómo calcular a partir de ~x otros datos más pe-
queños ~x′, y sabemos además cómo calcular el resultado ~y para ~x suponiendo
conocido el resultado ~y ′ para ~x ′.

Para implementar soluciones recursivas en un lenguaje de programación tenemos que
utilizar una acción que se invoque a sí misma con datos cada vez “más simples”:
funciones o procedimientos.

1Gonzalo Méndez es el autor principal de este tema.

49

50







Capítulo 4. Diseño de algoritmos recursivos

Intuitivamente, el subprograma se invoca a sí mismo con datos cada vez más simples
hasta que son tan simples que la solución es inmediata. Posteriormente, la solución
se puede ir elaborando hasta obtener la solución para los datos iniciales.

Tres elementos básicos en la recursión:

Autoinvocación: el proceso se llama a sí mismo.
Caso directo: el proceso de autoinvocación eventualmente alcanza una solución
directa (sin invocarse a sí mismo) al problema.
Combinación: los resultados de la llamada precedente son utilizados para obte-
ner una solución, posiblemente combinados con otros datos.

Para entender la recursividad, a veces resulta útil considerar cómo se ejecutan las
acciones recursivas en una computadora, como si se crearan múltiples copias del mis-
mo código, operando sobre datos diferentes (en realidad sólo se copian las variables
locales y los parámetros por valor). El ejemplo clásico del factorial:

{n ≥ 0}
fun factorial (int n) return int r
{r = n!}

int factorial ( int n )
{

int r;

if
else

( n == 0 ) r = 1;

// n > 0

r = n * factorial(n-1);

return r;

}

¿Cómo se ejecuta la llamada factorial(3)?

Figura 1: Ejecución de factorial(3)

¿Y qué ocurre en memoria?

Estructura de Datos y Algoritmos

1. Introducción

51

Figura 2: Llamadas recursivas de factorial(3)

Figura 3: Vuelta de las llamadas recursivas de factorial(3)



¿La recursión es importante?

Un método muy potente de diseño y razonamiento formal.

Tiene una relación natural con la inducción y, por ello, facilita conceptualmente
la resolución de problemas y el diseño de algoritmos.

1.1. Recursión simple



Una acción recursiva tiene recursión simple (o lineal) si cada caso recursivo realiza
exactamente una llamada recursiva. Puede describirse mediante el esquema general:

Facultad de Informática - UCM

52

Capítulo 4. Diseño de algoritmos recursivos

void nombreProc ( τ1x1 , ... , τnxn , δ1 & y1 , ... , δm & ym )
{
// P : P r e c o n d i c i ó n
// d e c l a r a c i ó n de c o n s t a n t e s

τ1x′
δ1y′

1 ; ... ; τnx′
1 ; ... ; δmy′

n ; // ~x′ , p a r á m e t r o s de l a l l a m a d a r e c u r s i v a
m ; // ~y′ ,
r e s u l t a d o s de l a l l a m a d a r e c u r s i v a

if ( d(~x) )
~y = g(~x);

else if ( r(~x) )
{

// c a s o d i r e c t o

// s o l u c i ó n a l c a s o d i r e c t o

// c a s o no d i r e c t o

~x′ = s(~x);
nombreProc(~x′, ~y′);// l l a m a d a r e c u r s i v a
~y = c(~x, ~y′); // f u n c i ó n de c o m b i n a c i ó n : c o m p o s i c i ó n de s o l u c i ó n

// f u n c i ó n s u c e s o r : d e s c o m p o s i c i ó n de d a t o s

}

// Q: P o s t c o n d i c i ó n
}

donde:

~x representa a los parámetros de entrada x1, ... , xn, ~x′ a los parámetros de la
llamada recursiva x′
1, ... ,
y′
m, e ~y a los parámetros de salida y1, ... , ym
d(~x) es la condición que determina el caso directo

n, ~y′ a los resultados de la llamada recursiva y′

1, ... , x′

r(~x) es la condición que determina el caso recursivo

g calcula el resultado en el caso directo

s, la función sucesor, calcula los argumentos para la siguiente llamada recursiva

c, la función de combinación, obtiene la combinación de los resultados de la
llamada recursiva ~y′ junto con los datos de entrada ~x, proporcionando así el
resultado ~y.



Veamos cómo la función factorial se ajusta a este esquema de declaración:

{n ≥ 0}
fun factorial (int n) return int r
{r = n!}

int factorial ( int n )
{

int r;

if ( n == 0 ) r = 1;
else

// n > 0

r = n * factorial(n-1);

return r;

}

d(n) = (n == 0)
g(n) = 1

Estructura de Datos y Algoritmos

1. Introducción

53

r(n) = (n > 0)
s(n) = n − 1
c(n, f act(s(n))) = n ∗ f act(s(n))



El esquema de recursión simple puede ampliarse considerando varios casos directos
y también varias descomposiciones para el caso recursivo. d(~x) y r(~x) pueden des-
doblarse en una alternativa con varios casos. Lo importante es que las alternativas
sean exhaustivas y excluyentes, y que en cada caso sólo se ejecute una llamada
recursiva.



Ejemplo: multiplicación de dos naturales por el método del campesino egipcio.

{(a ≥ 0) ∧ (b ≥ 0)}
fun prod (int a, int b) return int r
{r = a ∗ b}

int prod ( int a, int b )
{

int r;

if

( b == 0 )

{r = 0;}

else if ( b == 1 )

{r = a;}

else if (b % 2 == 0)

// b > 1

{r = prod(2*a, b/2);}

else

// b > 1 && ( b % 2 == 1 )

{r = prod(2*a, b/2) + a;}

return r;

}

d1(a, b) = (b == 0)
g1(a, b) = 0
r1(a, b) = ((b > 1) && par(b))
s1(a, b) = (2 ∗ a, b/2)
c1(a, b, prod(s1(a, b))) = prod(s1(a, b))

d2(a, b) = (b == 1)
g2(a, b) = 1
r2(a, b) = ((b > 1) && impar(b))
s2(a, b) = (2 ∗ a, b/2)
c2(a, b, prod(s2(a, b))) = prod(s2(a, b)) + a

1.2. Recursión final







La recursión final o de cola (tail recursion) es un caso particular de recursión simple
donde la función de combinación se limita a transmitir el resultado de la llamada
recursiva. Se llama final porque lo último que se hace en cada pasada es la llamada
recursiva.

El resultado será siempre el obtenido en uno de los casos base.

Los algoritmos recursivos finales tienen la interesante propiedad de que pueden ser
traducidos de manera directa a soluciones iterativas, más eficientes.

Facultad de Informática - UCM

54

Capítulo 4. Diseño de algoritmos recursivos

void nombreProcItr ( τ1x1 , . . . , τnxn , δ1 & y1 , . . . , δm & ym ) {
// P r e c o n d i c i ó n
// d e c l a r a c i ó n de c o n s t a n t e s

τ1x′

1 ; ... ; τnx′

n ; // ~x′

~x′ = ~x;
while ( r(~x′) )

~x′ = s(~x′);

~y = g(~x′);
}

// P o s t c o n d i c i ó n
}



Como ejemplo de función recursiva final veamos el algoritmo de cálculo del máximo
común divisor por el algoritmo de Euclides.

{(a > 0) ∧ (b > 0)}
fun mcd (int a, int b) return int r
{r = mcd(a, b)}

int mcd( int a, int b )
{

int m;

( a == b ) m = a;

if
else if ( a >
else

// a < b

b ) m = mcd(a-b, b);

m = mcd(a, b-a);

return m;

}

que se ajusta al esquema de recursión simple:

d(a, b) = (a == b)
r1(a, b) = (a > b)
s1(a, b) = (a − b, a) s2(a, b) = (a, b − a)

g(a, b) = a
r2(a, b) = (a < b)

y donde las funciones de combinación se limitan a devolver el resultado de la llamada
recursiva

c1(a, b, mcd(s1(a, b))) = mcd(s1(a, b))
c2(a, b, mcd(s2(a, b))) = mcd(s2(a, b))

Si traducimos esta versión recursiva a una iterativa del algoritmo obtenemos:

int itmcd( int a, int b )
{

int auxa =a; int auxb=b;
while (auxa!=auxb)

{

if (auxa>auxb)

{auxa = auxa-auxb;}

else

Estructura de Datos y Algoritmos

1. Introducción

55

{auxb = auxb-auxa;}

};

return auxa;

}

1.3. Recursión múltiple



Este tipo de recursión se caracteriza por que, al menos en un caso recursivo, se rea-
lizan varias llamadas recursivas. El esquema correspondiente es el siguiente:

void nombreProc ( τ1x1 , ... , τnxn , δ1 & y1 , ... , δm & ym ) {
// P r e c o n d i c i ó n
// d e c l a r a c i ó n de c o n s t a n t e s

τ1x11 ; ... ; τnx1n ; ... ; τ1xk1 ; ... ; τnxkn ;
δ1y11 ; ... ; δmy1m ; ... ; δ1yk1 ; ... ; δmykm ;

if ( d(~x) )

~y = g(~x);

else if ( r(~x) ) {

~x1 = s1( ~x );
nombreProc(~x1, ~y1);
...
~xk = sk( ~x );
nombreProc(~xk, ~yk);
~y = c(~x, ~y1, ... , ~yk);

}

// P o s t c o n d i c i ó n
}

donde

k > 1, indica el número de llamadas recursivas

~x representa los parámetros de entrada x1,..., xn, ~xi a los parámetros de la i-
ésima llamada recursiva xi1,..., xin, ~yi a los resultados de la i-ésima llamada
recursiva yi1,..., yim, para i = 1, ..., k, e ~y a los parámetros de salida y1,..., ym
d(~x) es la condición que determina el caso directo

r(~x) es la condición que determina el caso recursivo

g calcula el resultado en el caso directo

si, las funciones sucesor, calculan la descomposición de los datos de entrada
para realizar la i-ésima llamada recursiva, para i = 1, ... , k

c, la función de combinación, obtiene la combinación de los resultados ~yi de
las llamadas recursivas, para i = 1, ... , k, junto con los datos de entrada ~x,
proporcionando así el resultado ~y.



Otro ejemplo clásico, los números de Fibonacci:

{n ≥ 0}
fun fib (int n) return int r
{r = f ib(n)}

Facultad de Informática - UCM

56

Capítulo 4. Diseño de algoritmos recursivos

int fib( int n )
{

int r;

( n == 0 ) r = 0;
if
else if ( n == 1 ) r = 1;
else

//
  • Links de descarga
http://lwp-l.com/pdf16363

Comentarios de: Capítulo 4 - Diseño de algoritmos recursivos (0)


No hay comentarios
 

Comentar...

Nombre
Correo (no se visualiza en la web)
Valoración
Comentarios...
CerrarCerrar
CerrarCerrar
Cerrar

Tienes que ser un usuario registrado para poder insertar imágenes, archivos y/o videos.

Puedes registrarte o validarte desde aquí.

Codigo
Negrita
Subrayado
Tachado
Cursiva
Insertar enlace
Imagen externa
Emoticon
Tabular
Centrar
Titulo
Linea
Disminuir
Aumentar
Vista preliminar
sonreir
dientes
lengua
guiño
enfadado
confundido
llorar
avergonzado
sorprendido
triste
sol
estrella
jarra
camara
taza de cafe
email
beso
bombilla
amor
mal
bien
Es necesario revisar y aceptar las políticas de privacidad