You are on page 1of 15

Apuntes de Estructuras de Datos

Captulo 2

Recursividad

2.1 Competencia especfica a desarrollar

Aplica la recursividad en la solucin de problemas valorando su pertinencia en el uso


eficaz de los recursos.

2.2 Actividades de aprendizaje

Redactar una definicin propia del concepto de recursividad despus de consultar en


diferentes fuentes bibliogrficas y comentarla en trinas.
Enumerar las ventajas y desventajas del uso de la recursividad en una plenaria.
Trasladar un catlogo de problemas iterativos a recursivos, donde distinga el segmento
recursivo y la condicin de salida, elaborar un reporte de prctica de ejercicios.
Desarrollar programas en los cuales se aplique la recursividad y entregar informe.

2.3 Definicin

Entendemos por recursividad la definicin de un objeto en trminos de s mismo.

En programacin la recursividad nos da la posibilidad de definir un tipo de dato en funcin de si


mismo, o bien nos permite definir un problema en funcin de si mismo.

Recursividad de Definicin: por ejemplo aplicada a Estructuras de Datos se entiende como la


posibilidad de definir un tipo de dato en trminos de s mismo, como se ver ms adelante en
la definicin de una estructura de datos enlazadas:

63
Apuntes de Estructuras de Datos

public class Nodo {


protected Object info;
protected Nodo next;

public Nodo ( ) {
/* Crea un Nuevo objeto nodo */
}

}

Recursividad de Ejecucin o recursividad funcional: es aquella que se aplica a la solucin de


los problemas, que es la posibilidad de definir un problema en funcin del propio problema, lo
cual consiste en que un mtodo pueda llamarse a s mismo una o varias veces y sta puede ser
directa o indirecta.

La recursividad directa o simple es cuando un mtodo se llama a s mismo una o ms veces


directamente desde un punto del cdigo (simple) o desde varios (mltiple).:
mtodoX ( .. ) { mtodoY ( .. ) {

mtodoX ( .. ); mtodoY ( .. );

} mtodoY ( .. );
}
Mientras que la recursividad indirecta o mutua, es cuando un mtodo llama a otro y ste
llama a su vez al que lo llam:
mtodoX ( .. ) mtodoY ( .. )
{ {

mtodoY ( .. ); mtodoX ( .. );

} }

64
Apuntes de Estructuras de Datos

Esto pareciera un crculo vicioso, sin embargo la clave est en que el mtodo recursivo se
llama a s mismo o a otro, pero con instancias diferentes. Para que esto pueda darse, deben
establecerse las siguientes consideraciones:

1. Debe existir al menos un punto de terminacin (instruccin condicional).


2. Debe existir al menos un punto de reactivacin (donde se llama a s mismo o a otro).

Es decir, cada solucin recursiva involucra dos partes o casos principales:

1) El caso(s) base: en el cual el problema es lo suficientemente simple para ser resuelto


directamente (sin recursin) y

2) El caso(s) recursivo: un caso recursivo tiene tres componentes:


1. dividir el problema en una o ms partes mas pequeas o simples del problema
2. llamar a la funcin (recursivamente) en cada parte y
3. combinar las soluciones de las partes en una solucin para el problema

Dependiendo del problema, cualquiera de estos casos puede ser trivial o complejo.

Se dice tambin que una estructura de datos que est parcialmente compuesta por otras
instancias de la estructura de dato, es recursiva. Por ejemplo un rbol est compuesto de
rboles ms pequeos, los subrboles y los nodos hojas, y una lista puede tener otras listas
como elementos. Las estructuras de datos recursivas son a menudo mejor manejadas con un
algoritmo recursivo.

2.4 Funcionamiento de la recursividad

Se puede considerar que en la ejecucin de una recursin, cada mtodo no ocupa una parte
diferente de la misma computadora, sino que est corriendo en otra mquina. As, cuando un
mtodo invoca a otro, activa la mquina correspondiente, y cuando el otro termina su trabajo,
enva la respuesta a la primera mquina, la cual puede entonces reanudar su tarea.

65
Apuntes de Estructuras de Datos

Java al igual que C++, implementa los mtodos recursivos mediante una pila de registros de
activacin o pila de marcos activos, en donde cada registro o marco contiene informacin
relevante sobre el mtodo como los valores de los parmetros y las variables locales, como se
muestra en el siguiente esquema.

Variables Direccin valores Valor de


Locales de retorno la funcin

Pila de registros de activacin o pila de marcos activos

Se utiliza una pila (estructura lifo) ya que la terminacin de los mtodos se produce en sentido
inverso a la invocacin de los mismos. El tema de la pilas ser visto a mayor detalle en la
siguiente unidad.

Por la cercana relacin entre recursin y pilas se dice que los procesos recursivos siempre se
pueden implementar en forma iterativa con una pila explcita.

La recursividad a final de cuentas es un ciclo, slo que en este ciclo no se utiliza ninguna
estructura de control cclica como el for, el while o el do while existentes en la mayora de
los lenguajes de programacin. Sin embargo se deben considerar los mismos principios que en
estas estructuras de control. Esto se ejemplificar de la siguiente manera, comparando uno con
otro.

Si se implementa un mtodo cualquiera haciendo uso de un ciclo while (iterativo):


public void mtodoX ( int i, int n ) {
Cada vez que el CICLO se
while ( i < n ) { vuelve a ejecutar, la variable de
System.out.println ( "i = " + i ); control i tiene que
++ i; incrementarse para que no se
} cicle.
}

Ahora el mismo mtodo pero recursivo ( while cambia por if ):

66
Apuntes de Estructuras de Datos

public void mtodoX ( int i, int n ) { Cada vez que el MTODO se


if ( i < n ) { vuelve a ejecutar, la variable de
System.out.println ( "i = " + i ); control i tiene que
mtodoX ( ++ i, n ); incrementarse para que no se
} cicle.
}

La diferencia, adems del uso del if en el proceso recursivo, es que en un proceso iterativo, si
nunca se cumple la condicin, el programa se cicla y ste tiene que ser abortado a travs del
teclado. En el proceso recursivo, el programa se aborta automticamente al consumirse la
memoria.

2.5 Procedimientos recursivos

Se pueden encontrar muchos ejemplos del uso de recursividad sobre todo en los problemas
matemticos, ya que muchos de ellos tienen una definicin matemtica recursiva, en estos
casos, la escritura de la rutina se simplifica, es decir, la recursividad es una herramienta muy
potente para la solucin de problemas complejos ya que estos se reducen a problemas mas
simples del mismo tipo. En algunos casos, los algoritmos que se expresan en forma natural en
modo recursivo, deben reescribirse sin recursividad para conseguir una mayor eficiencia.

Dentro de las funciones matemticas que pueden ser definidas recursivamente estn:

El factorial de un nmero
La serie de Fibonacci
El mximo comn divisor con el algoritmo de Euclides (mcd): O((log a)(log b))
La transformada de Fourier
Clculo de la suma de todos los nmeros desde 1 hasta n
Clculo de 2 a la potencia de un nmero entero positivo
Clculo de cualquier nmero a la potencia de un nmero entero positivo
Etc.

67
Apuntes de Estructuras de Datos

Adems, muchos problemas de juegos pueden ser resueltos recursivamente, como el conocido
como las Torres de Hanoi, que emplea en su solucin la estructura de datos Pila, o algn otro
ms complejo como el ajedrez. Algunos algoritmos de bsqueda o de ordenacin tambin
pueden ser definidos recursivamente. Otra de las estructuras de datos ms importantes que
utiliza recursividad son los rboles que se revisarn ms adelante.

2.6 Ejercicios empleando recursividad

Uno de los ejemplos ms simples de una definicin recursiva es el clculo del factorial de un
nmero, que puede definirse as:

S i N = 0 factorial = 1 punto de terminacin


N!
Si N > 0 factorial = N(N1)! punto de reactivacin

Por lo que una forma natural de escribir un mtodo para este clculo sera:

public class Factorial {


public static long factorial ( int n ) {
if ( n <= 1 ) // caso base
return 1;
else
return n * factorial ( n 1 ); // punto de reactivacin
}
public static void main ( String [ ] args ) { // prueba simple del programa
for ( int i = 1; i <= 10; i++ )
System.out.println ( factorial ( i ) );
}
}

Note que este mtodo se llama a s mismo para evaluar el siguiente trmino. Eventualmente
asumir la condicin de terminacin y cesar la ejecucin. Sin embargo, antes de llegar a la
condicin de terminacin, habr introducido n registros o marcos de activacin en la pila. Por
lo que la complejidad del mtodo es O(n). Se hablar en la ltima unidad del programa acerca
de cmo determinar la complejidad de los algoritmos.

68
Apuntes de Estructuras de Datos

La condicin de terminacin es obviamente de suma importancia cuando se trata con procesos


recursivos. Si se omite, entonces el mtodo continuar llamndose a s mismo hasta que el
programa sature el espacio de la pila, con resultados fallidos, como se mencion
anteriormente.

Otro ejemplo comnmente utilizado como mtodo recursivo es el clculo de los nmeros de
Fibonacci, que puede definirse de la siguiente manera:

S iN=0 oN=1 fib = 1 punto de terminacin


N
Si N > 1 fib = (N 1) + (N 2) punto de reactivacin

Que puede escribirse como:

public static long Fib ( int n ) {


if ( n <= 1 ) // caso base
return 1;
else
return Fib (n 1) + Fib (n 2); // dos puntos de reactivacin
}

En este caso, aunque la rutina es corta y elegante, tiene un problema subyacente, ya que lleva a
cabo una multitud de clculos repetidos, es decir, para calcular Fib(n), se calcula
primeramente en forma recursiva Fib(n1), cuando la llamada recursiva termina, se calcula
entonces Fib(n2) recursivamente, aunque ya se haba calculado Fib(n2) al calcular Fib(n
1), entonces la llamada a Fib(n2) es un clculo repetido. Como ste, hay muchos clculos
repetidos, por lo que el tiempo de este algoritmo es exponencial O(2n).

Por ejemplo, si n = 6, entonces:

69
Apuntes de Estructuras de Datos

Fib(6)
_______________________________________
Fib(5) Fib(4)
__________________________ ______________
Fib(4) Fib(3) Fib(3) Fib(2)
________________ __________ __________
Fib(3) Fib(2) Fib(2) Fib(1) Fib(2) Fib(1)
____________
Fib(2) Fib(1) etc.

Cuando n es muy grande, la definicin recursiva ya no resulta conveniente, la definicin


iterativa resulta ms eficiente, ya que es de complejidad lineal O(n):

public static long Fib ( int n ) {


int previous = 1;
int result = 1;
for ( int i = 0; i <= n; ++i ) {
int sum = result + previous;
previous = result;
result = sum;
}
return result;
}

Se revisar ahora un mtodo de bsqueda: la bsqueda binaria, para encontrar algn elemento
en particular en un arreglo.
Caractersticas:

1. El arreglo de elementos debe estar ordenado


2. La cantidad de elementos en el arreglo es conocida

El mtodo consiste en dividir el arreglo en 2 subarreglos e identificar en cul de ellos puede


encontrarse el elemento. El proceso vuelve a repetirse sucesivamente hasta encontrarlo o
determinar que no est.

Definicin Matemtica:

70
Apuntes de Estructuras de Datos

x = elemento a buscar
a = arreglo
LI = lmite inferior del arreglo, LS = lmite superior del arreglo
pm = punto medio del arreglo
Si LI > LS entonces DIR = 1 punto de terminacin
BUSBIN pm = (LI + LS) / 2
Si x = a[pm] entonces DIR = pm punto de terminacin
Si x est en parte derecha punto de reactivacin
LI = pm + 1
Si x est en parte izquierda punto de reactivacin
LS = pm 1

Cuya implementacin se muestra en la siguiente rutina:

public static int BusBin ( int [ ] a, int li, int ls, int x ) {
int pm;
if ( li > ls ) // caso base
return 1; // punto de terminacin
else {
pm = ( li + ls ) / 2;
if ( x == a[pm] ) // caso base
return pm; // punto de terminacin
else if ( x < a[pm] )
return BusBin ( a, li, pm1, x ); // buscar a la izquierda
else return BusBin ( a, pm+1, ls, x ); // buscar a la derecha
}
}

La implementacin en forma iterativa sera:

public static int BusBin ( int [ ] a, int x ) {


int pm, li = 1, ls = n;
while ( li < ls ) {
pm = ( li + ls ) / 2;
if ( x > a[pm] )
li = pm + 1;
else ls = pm 1;
}
if ( x == a[li] )
return li;
else return 1;
}

71
Apuntes de Estructuras de Datos

La bsqueda binaria recursiva tiene la misma estrategia que la versin iterativa, por lo que
para encontrar la complejidad, lo primero es identificar cuntas veces se va a ejecutar el ciclo
(o llamadas). Ya que cada paso a travs del ciclo decrementa el arreglo a la mitad, entonces
dado un tamao n cuntas veces necesita partir a la mitad el arreglo hasta encontrarlo o llegar
a 1? n/2, n/4, n/8, n/16, , n/2k, qu ocurre primero? Si se resuelve la ecuacin n/2k < 1
se obtiene que k > log n, por lo que se puede fijar a k = log n (techo de x x, es el entero
ms pequeo que es mayor o igual que x, por ejemplo, 6.1 = 7), entonces se puede decir que
despus de muchas iteraciones en el ciclo, se encontrar el valor o se concluir que no est en
el arreglo, entonces la complejidad del algoritmo es O(log n), lo que es muy rpido.
Ahora se revisar un mtodo de ordenacin: por mezcla o MERGE/SORT.

El Merge/Sort es un algoritmo de ordenacin basado en la estrategia divide y vencers que


se refiere a la descomposicin del problema en subproblemas ms simples (dividir), cada uno
de los cuales se resuelve aplicando el mismo enfoque hasta que el subproblema es tan pequeo
que puede ser resuelto inmediatamente (vencer), es decir, consiste en dividir en dos un arreglo
con valores desordenados, y cada una de las partes volverla a dividir sucesivamente (parte
izquierda y parte derecha) hasta que se llegue a conseguir un arreglo de un elemento, el cual se
considera ya ordenado, y a partir de ah, se van regresando en forma ordenada e intercalando
la parte izquierda y derecha ya ordenadas (combinar).

Definicin Matemtica:
Si LI = LS elemento ordenado punto de terminacin
pm = (LI + LS) /2 dividir en 2 el arreglo
MERGE/SORT SORT ordenar parte izquierda: LS = pm punto de reactivacin
SORT ordenar parte derecha: LI = pm + 1 punto de reactivacin
MERGE intercalar parte izquierda y derecha ya ordenada

La implementacin quedara as:


public static void MergeSort ( int [ ] a, int li, int ls ) {
int pm;
if ( li < ls ) {
pm = ( li + ls ) / 2; //dividir en 2 el arreglo
MergeSort ( a, li, pm ); // ordenar parte izquierda
MergeSort ( a, pm+1, ls ); // ordenar parte derecha
Intercala ( a, li, ls ) // intercalar parte izquierda y derecha ya ordenada

72
Apuntes de Estructuras de Datos

}
}

La complejidad de este mtodo es O(n log n) porque siempre divide el arreglo a la mitad. Los
dos factores involucrados en el mtodo son el nmero de llamadas recursivas y el tiempo que
toma la mezcla.

Cada llamada recursiva podr ser un caso base o resultar en dos llamadas recursivas. La
primera llamada empieza haciendo dos llamadas, cada una de ellas hace cuatro y as
sucesivamente, como en las ramificaciones de un rbol binario. Por ejemplo, para ordenar un
arreglo m de 4 elementos (posiciones de: 0 3), las divisiones quedaran:
m(0 3)
m(0 1) m(2 3)
m(0 0) m(1 1) m(2 2) m(3 3)

Ahora, si doblamos el nmero de elementos en el arreglo (8), cada Merge/Sort en el ltimo


nivel tendr el doble del nmero de elementos, por lo que se necesita una llamada adicional
por cada elemento, por lo que la profundidad total del rbol es log(n) + 1, el nmero de veces
que se necesita dividir el nmero de elementos en el arreglo para obtener el caso base.

Ahora, qu sucede con la cantidad de trabajo hecho por la llamada recursiva. Primero se puede
pensar que cada mezcla en el rbol es en tiempo O(n), aunque esto es incorrecto, ya que en
cada nivel, el nmero de elementos se reduce dramticamente; al final de la rama cada llamada
al Merge/Sort es mezclado exactamente la mitad de la lista. En el nodo raz es la nica vez que
la lista entera se mezcl junta en un solo nodo.

Como resultado es ms prctico pensar en el Merge/Sort en trminos del nmero de


operaciones ejecutadas en un slo nivel del rbol. En cada nivel se realizan un total de n
operaciones y hay log(n) + 1 niveles, consecuentemente la complejidad completa es O(n * log
n), o sea O(n log n).

Para el procedimiento de intercalacin, se siguen los siguientes pasos:

73
Apuntes de Estructuras de Datos

1. Calcular el punto medio


2. Establecer los lmites para los subarreglos izquierdo y derecho y asignar el arreglo a un
arreglo auxiliar
3. Mientras el lmite inferior de la parte izq. sea menor o igual que el punto medio y el lmite
inferior de la parte derecha sea menor o igual que el lmite superior
3.1 Si elemento de la izquierda es menor que elemento de la derecha
entonces mover el elemento de la izquierda al auxiliar
sino mover el elemento de la derecha al auxiliar
3.2 Pasar al siguiente elemento
4. Mientras el arreglo auxiliar no se termine de llenar
4.1 Si hay elementos en la parte izquierda
entonces mover los elementos de la izquierda al auxiliar
sino mover los elementos de la derecha al auxiliar
5. Asignar el arreglo auxiliar al arreglo principal

Esto queda implementado de la siguiente manera:

public static void Intercala ( int [ ] a, int li, int ls ) {


int pm, lii, lid, lia, aux[Tam], i = 0;
pm = ( li + ls ) / 2;
lii = lia = li;
lid = pm + 1;
while ( lii <= pm && lid <= ls )
if ( a[lii] < a[lid] )
aux[lia++] = a[lii++];
else
aux[lia++] = a[lid++];
while ( lia <= ls )
if ( lii <= pm )
aux[lia++] = a[lii++];
else
aux[lia++] = a[lid++];
for ( i = li; i <= ls; i++ )
a[i] = aux[i];
}

74
Apuntes de Estructuras de Datos

Otro ejemplo: Chequeo de gramticas. El proceso de la recursin resulta til en la escritura de


un compilador, aunque en realidad la construccin de ste resulte un proceso complicado en
extremo, que merece un estudio aparte.

Las reglas del lenguaje estn compuestas de dos partes conocidas como la sintaxis y la
semntica del lenguaje. Las reglas de sintaxis definen cmo las palabras (o vocabulario) del
lenguaje pueden ser puestas juntas para formar frases. Las reglas de semntica atribuyen
sentido y significado a estas combinaciones de palabras. Las reglas de semntica son
usualmente establecidas con menos formalidad que las reglas de sintaxis.

Todo lenguaje de programacin tiene sus propias reglas de sintaxis, las cuales determinan si el
cdigo especificado por el programador puede ser traducido por el compilador del lenguaje al
cdigo mquina.

Una notacin formal generalizada muy empleada en la definicin de la sintaxis de los


lenguajes de programacin y que es sencilla de utilizar es la notacin de Bakus-Naur Form
(BNF).

Bajo esta notacin un smbolo se define dando las reglas para reemplazarlo:

Smbolo := alternativa1 | alternativa2

Lo que puede ser interpretado como sigue: lo que est a la izquierda del operador := es un
smbolo no terminal que puede ser reemplazado por las alternativas sealadas a la derecha, las
cuales utilizan el separador | y que stas a su vez pueden ser tambin no terminales. Los
smbolos no terminales son aquellos que requieren ser definidos y deben ir encerrados entre
los operadores < >. Por otro lado, un smbolo terminal es aquel que ya est definido.
Todo aquello que est encerrado entre corchetes [] indica que es opcional, y lo encerrado
entre llaves {} indica que se puede repetir ms de una vez.

75
Apuntes de Estructuras de Datos

En este caso, el proceso que se sigue para verificar que un identificador est correctamente
escrito, consiste en dividir el identificador en dos partes: "el primer carcter" del
identificador, el cual es evaluado; y el "resto" del identificador. El proceso se repite de la
misma manera para el "resto" hasta terminar con el ltimo carcter del identificador.

Bajo la notacin de Bakus-Naur se puede describir su sintaxis de la siguiente forma:

<identificador> ::= <letra> | <letra> <resto>

Esta regla es leda como: Un identificador est definido como una letra, o una letra seguida
de un resto.

Por medio de otras reglas se van definiendo letra y resto, que son tambin smbolos no
terminales:
<letra> ::= A | B | C | ... | Z | a | b | c | ... | z
<resto> ::= <letra> | <dgito> | <subrayado> | <letra> <resto> | <dgito> <resto> |
<subrayado> <resto>
<dgito> ::= 0 | 1 | 2 | ... | 9
<subrayado> ::= _

Esto puede implementarse de la siguiente manera:

public static boolean Identificador ( char [ ] palabra ) {


if ( palabra[0] >= a && palabra[0] <= z || // verifica el primer carcter
palabra[0] >= A && palabra[0] <= Z ) {
if ( palabra[1] != \0 ) // verifica si hay resto
return ValResto ( palabra, 1 ); // llamada al proceso recursivo
else return true;
}
else return false;
}

76
Apuntes de Estructuras de Datos

public static boolean ValResto ( char [ ] resto, int i ) { // proceso recursivo


if ( resto[i] >= a && resto[i] <= z
resto[i] >= A && resto[i] <= Z
resto[i] >= 0 && resto[i] <= 9 resto[i] = < ) {
if ( resto[++i] != \0 )
return ValResto ( resto, i );
else return true;
}
else return false;
}

77

You might also like