Recursividad.
Unidad II.- Recursividad. 2.1 Concepto de recursividad. recursividad.
La recursividad es una técnica de programación importante. Se utiliza para realizar una llamada a una funcion desde la misma funcion. Como ejemplo útil se puede presentar el cálculo de números factoriales. Él factorial de 0 es, por definición, 1. Los factoriales de números mayores se calculan mediante la multiplicación de 1 * 2 *…, incrementando el número de 1 en 1 hasta llegar al número para el que se está calculando el factorial. El siguiente parrafo muestra una función, expresada con palabras, que calcula 8-un factorial. “Si el número es menor que cero, se rechaza. Si no es un entero, se redondea al siguiente entero. Si el número es cero, su factorial es uno. Si el número es mayor que cero, se multiplica por él factorial del número menor inmediato.” Para calcular el factorial de cualquier número mayor que cero hay que calcular como mínimo el factorial de otro número. La función que se utiliza es la función en la que se encuentra en estos momentos, esta función debe llamarse a sí misma para el número menor inmediato, para poder ejecutarse en el número actual. Esto es un ejemplo de recursividad. La recursividad es un concepto importante en informatica. Muchos algoritmos se pueden describir mejor en terminos de recursividad. Supongamos que P es un procedimiento que contiene una sentencia de Llamada a si mismo o una sentencia de Llamada a un segundo procedimiento que puede eventualmente llamar de vuelta al procedimiento original P. Entonces P se dice que es u procedimiento recursivo. Como el progrma no ha de continuar ejecutandose indefinidamente, un procedimiento recursivo ha de tener las dos siguientes propiedades:
(1) Debe existir un cierto criterio, llamado criterio base, por el que el procedimiento no se llama asi mismo. (2) Cada vez que el procedimiento se llame a si mismo (directa o inderectamente), debe estar mas cerca del criterio base. Un procedimiento recursivo con estas dos propiedades se dice que esta bien definido. Similarmente, una funcion se dice que esta definida recursivamente si la definicion de la funcion se refiere a si misma. De nuevo, para que la definicion no sea circular, debe tener las dos siguientes propiedades: (1) Debe haber ciertos argumentos, llamados valores base, para los que la funcion no se refiera a si misma. (2) Cada vez que la funcion se refiera a si misma, el argumento de la funcion debe acercarse mas al valor base. Una funcion recursiva con estas dos propiedades se dice tambien que esta bien definida. Tipos.
Podemos distinguir dos tipos de recursividad: •
•
Directa: Cuando un subprograma se llama a si mismo una o mas veces directamente. Indirecta: Cuando se definen una serie de subprogramas usándose unos a otros. Características.
Un algoritmo recursivo consta de una parte recursiva, otra iterativa o no recursiva y un acondición de terminación. La parte recursiva y la condición de terminación siempre existen. En cambio la parte no recursiva puede coincidir con la condición de terminación. Algo muy importante a tener en cuenta cuando usemos la recursividad es que es necesario asegurarnos que llega un momento en que no hacemos más llamadas recursivas. Si no se cumple esta condición el programa no parará nunca. Ventajas e inconvenientes.
La principal ventaja es la simplicidad de comprensión y su gran potencia, favoreciendo la resolución de problemas de manera natural, sencilla y elegante; y facilidad para comprobar y convencerse de que la solución del problema es correcta. El principal inconveniente es la ineficiencia tanto en tiempo como en memoria, dado que para permitir su uso es necesario transformar el programa recursivo en otro iterativo, que utiliza bucles y pilas para almacenar las variables.
2.2 Procedimientos recursivos.
Un procedimiento o función recursiva es aquella que se llama así misma. Esta característica permite a un procedimiento recursivo repetirse valores diferentes de parámetros. La recursion es una alternativa a la iteración muy elegante en la solución de problemas, especialmente si estos tienen naturaleza recursiva. Normalmente, una solución recursiva es menos eficiente en términos de tiempo de computadora que una solución iterativa debido al tiempo adicional de llamada a procedimientos. En muchos casos, la recursion permite especificar una solución mas simple y natural para resolver un problema que en otro caso seria difícil. Por esta razón la recursion (recursividad) es una herramienta muy potente para la resolución de problemas de programación. En el siguiente ejemplo se muetra un procedimiento recursivo. using using using using using using using
System; System.Collections.Generic; System.ComponentModel; System.Data; System.Drawing; System.Text; System.Windows.Forms;
namespace WindowsApplication1 { public partial class Recursividad : Form { public Recursividad() { InitializeComponent(); } double r; int fin = 0; private void button1_Click(object sender, EventArgs e) { fin = int.Parse(textBox4.Text.ToString()); listBox1.Items.Clear();
listBox1.Items.Add("x\ty"); evaluar(); } //Procedimiento recusivo public void evaluar() { while (fin <= int.Parse(textBox5.Text.ToString())) { r = int.Parse(textBox1.Text.ToString()) * (fin * fin) + int.Parse(textBox2.Text.ToString()) * fin + int.Parse(textBox3.Text.ToString()); listBox1.Items.Add(fin.ToString() + "\t" + r.ToString()); fin++; evaluar(); } } }
}
Mecánica de recursión.
Un objeto recursivo es aquel que forma parte de si mismo. Esta idea puede servir de ayuda para la definición de conceptos matematicos. Asi, la definición del conjunto de los numeros naturles es aque conjunto en el que se cumplen las siguientes caracteristicas: • •
0 es un número natural. El siguiente número de un número natural es otro numero natural.
Mediante una definición finita hemos representado un conjunto infinito. El concepto de la recursividad es muy importante en programación. La recursividad es una herramienta muy eficaz para resolver diversos tipos de problemas existen muchos algoritmos que se describiran mejor en terminos recursivos. Dentro de la teoría de la recursión, se tiene que existen diferentes tipos de recursión: Recursión directa.
Cuando el código F tiene una sentencia que involucra a F.
Recursión indirecta o cruzada
Cuando la función F involucra una función G que invoca a la ves una función H, y así sucesivamente, hasta que se involucra la función F. Por ejemplo el algoritmo de Par o impar. int par (int n) { if (n==0) return n+1; return impar(n-1); int impar(int n) if (n==0) return 0; return par(n-1); }
A continuación se expone otro ejemplo de programa que utiliza recursión indirecta, y nos dice si un número es par o impar. Cabe mencionar que existen otros métodos mucho más sencillos para determinar la solución a este problema, basta con determinar el resto de la división entre dos. Por ejemplo: si hacemos par(2) devuelve 1 (cierto). Si hacemos impar (4) devuelve 0 (falso). int par(int n); int impar(int n); int par(int n) { if (n == 0) return 1; return impar(n-1); } int impar(int n) { if (n == 0) return 0; return par(n-1); }
Recursión simple.- Aquella en cuya función solo aparece una llamada recursiva. Se puede transformar con facilidad en algoritmos iteractivos. Recursión múltiple.- Se da cuando hay más de una llamada a sí misma dentro del cuerpo de la función, resultando más difícil de transformar a iteractiva. Poe ejemplo el algoritmo de Fibonacci. int fibo(int n) { if (n<=1) return 1 return fibo(n-1) + fibo(n-2) }
4.3.1.5.- Recursión anidada.- En algunos de los argumentos de la llamada hay una nueva llamada a sí misma. Por ejemplo la función de Ackerman: int Ack(int m, int n) { if (m==0) return n+1 if (n=00) return Ack(n-1, 1) return Ack(m-1, Ack(m, n-1)); }
Un requisito para que un algoritmo recursivo sea correcto es que no genere una secuencia infinita de llamadas sobre sí mismo. Cualquier algoritmo que genere una secuencia de este tipo no terminará nunca. En consecuencia, la definición recursiva debe incluir un componente base (condición de salida ) en el que f(n) se define directamente (es decir, no recursivamente) para uno o más valores de n. Debe existir una <> de la secuencia de llamadas recursivas. Al estar trabajando con recursividad, la memoria de la computadora hace uso de una pila de recursión, la cual se divide de manera lógica en 4 segmentos: 1. Segmento de código.- Parte de la memoria donde se guardan las instrucciones del programa en código máquina. 2. Segmento de datos.- Parte de la memoria destinada a almacenar las variables estáticas. 3. Montículo.- Parte de la memoria destinada a las variables dinámicas. 4. Pila de programa.- parte destinada a las variables locales y parámetros de la función que está siendo ejecutada. Funciones mutuamente recursivas.
Cuando se trabaja llamados a la ejecución de las funciones, se provocan las siguientes operaciones: 1. Reservar espacio en la pila para los parámetros de la función y sus variables locales. 2. Guardar en la pila la dirección de la línea de código desde donde se ha llamado a la función. 3. Almacenar los parámetros de la función y sus valores de pila. 4. Al terminar la función, se libera la memoria asignada en la pila y se vuelve a la instrucción actual.
Pero cuando se trabaja con funciones recursivas, se provoca además lo siguiente: 1. La función se ejecutará normalmente hasta la llamada a sí misma. En ese momento se crean en la pila nuevos parámetros y variables locales. 2. El nuevo ejemplar de función comienza a ejecutarse. 3. Se crean más copias hasta llegar a la primera llamada (ultima en cerrarse). La ciencia de la computación hace tiempo que descubrió que se puede reemplazar a un método que utiliza un bucle para realizar un cálculo con un método que se llame repetidamente a sí mismo para realizar el mismo cálculo. El hecho de que un método se llame repetidamente a sí mismo se conoce como recursion. La recursión trabaja dividiendo un problema en subproblemas. Un programa llama a un método con uno o más parámetros que describen un problema. Si el método detecta que los valores no representan la forma más simple del problema, se llama a sí mismo con valores de parámetros modificados que describen un subproblema cercano a esa forma. Esta actividad continúa hasta que el método detecta la forma más simple del problema, en cuyo caso el método simplemente retorna, posiblemente con un valor, si el tipo de retorno del método no es void. La pila de llamadas a método empieza a desbobinarse como una llamada a método anidada para ayudar a completar una evaluación de expresión. En algún punto, la llamada el método original se completa, y posiblemente se devuelve un valor. Recursión infinita
La iteración y la recursión pueden producirse infinitamente. Un bucle infinito ocurre si la prueba o test de continuación del bucle nunca se vuelve falsa. Una recursión infinita ocurre si la etapa de recursión no reduce el problema en cada ocasión de modo que converja sobre el caso base o condición de la salida. En realidad, larecursión infinita significa que cada llamada recursiva produce otra llamada recursiva y esta a su vez otra llamada recursiva, y así para siempre. En la práctica, dicha función se ejecutará hasta que la computadora agote la memoria disponible y se produzca una terminación anormal del programa. El flujo de control de una función recursiva requiere de tres condiciones para una terminación normal.
Cuando no utilizar recursividad.
La solucion recursiva de ciertos problemas simplifica mucho la estructura de los programas. Como contrapartida, en la mayoria de los lenguajes de programación las llamadas recursivas a procedimientos o funciones tienen un coste de tiempo mucho mayor que sus homologos iterativos. Se puede, por lo tanto, afrimar que la ejecución de un programa recursivo va a ser mas lenta y menos eficiente que el programa iterativo que soluciona el mismo problema, aunque, a veces, la sencilles de la estructura recursiva justifica el mayor tiempo de ejecucion. Los procedimientos recursivos se pueden convertir en no recursivos mediante la introducción de una pila y asi emular las llamadas recursivas. De esta manera se puede eliminar la recursion de aquellas partes de los programas que se ejecutan más frecuentemente. Ejemplo de mecánica de la recursividad Las torres de Hanoi
Algoritmo para trasladar la torre 1…n del poste X al poste Z, usando como auxiliar el poste Y. Si n = 1, lleva el disco 1 de X a Z y termina. Traslada la torre 1…n 1 usando este mismo algoritmo, de X a Y, usando como auxiliar Z. Lleva el disco n de X a Z. Traslada la torre 1…n 1 usando este mismo algoritmo, de Y a Z, usando como auxiliar X. Este algoritmo siempre me ha parecido sorprendente, parece más la definición de un problema que su resolución. Si eres como yo tendrás que implementarlo en un lenguaje de programación concreto para convencerte de que funciona. En realidad, lo único que hace es especificar unos pasos inevitables. Por ejemplo, como vimos antes, para resolver el puzzle es obligatorio llevar el disco n de A a C, y para ello es obligatorio llevar antes la torre 1…n a B, etc. La secuencia de movimientos que produce este algoritmo es la única solución óptima (o sea, de longitud mínima) posible. El número de movimientos es M3(n) = 2n 1, como se puede demostrar fácilmente por inducción sobre el número de discos. −
−
−
Se cumple para n = 1 M3(1) = 1 = 21 1. Si se cumple para n, se cumple para d+1 −
Al ejecutarse el algoritmo para n+1 se llama a sí mismo dos veces para n, más un movimiento del disco n+1. Así que M3(n+1) = 2 × M3(n) + 1 = 2 × (2n 1) + 1 = 2n+1 2+1 = 2n+1 1. −
−
−
Para n = 8 el número de movimientos es de 28 1 = 255. Para n = 64, de 264 1 = 18.446.744.073.709.551.615. Si los bramanes de Benarés cambiaran un disco de sitio cada segundo necesitarían más de quinientos ochenta mil millones de años para terminar su tarea, unas cuarenta veces la edad del Universo. −
−
Los algoritmos recursivos funcionan bien con ordenadores, pero son difíciles de aplicar para un ser humano. Si intentas resolver la torre de ocho discos aplicando el método descrito es fácil que te pierdas a no ser que vayas tomando notas de por dónde vas. Incluso para los ordenadores los algoritmos recursivos suelen ser «poco económicos», en el sentido de que consumen bastante memoria (y es que ellos también necesitan «tomar notas»). La alternativa a los algoritmos recursivos son los iterativos, en los que no hay llamadas a sí mismo, sino uno o varios bucles. Muy a menudo no existe o no se conoce una alternativa iterativa para un algoritmo recursivo, y cuando sí se conoce, suele ser mucho más complicada que la versión recursiva. Sin embargo, en el caso de la Torre de Hanoi, existen varios algoritmos iterativos muy simples.
Transformación de algoritmos recursivos a iterativos.
El concepto de recursividad va ligado al de repetición. Son recursivos aquellos algoritmos que, estando encapsulados dentro de una función, son llamados desde ella misma una y otra vez, en contraposición a los algoritmos iterativos, que hacen uso de ciclos while, do-while, for, etc. Algo es recursivo si se define en términos de sí mismo (cuando para definirse hace mención a sí mismo). Para que una definición recursiva sea válida, la referencia a sí misma debe ser relativamente más sencilla que el caso considerado. Ejemplo: definición de nº natural: → →
El N º 0 es natural El Nº n es natural si n-1 lo es En un algoritmo recursivo distinguimos como mínimo 2 partes:
a). Caso trivial, base o de fin de recursión: Es un caso donde el problema puede resolverse sin tener que hacer uso de una nueva llamada a sí mismo. Evita la continuación indefinida de las partes recursivas. b). Parte puramente recursiva: Relaciona el resultado del algoritmo con resultados de casos más simples. Se hacen nuevas llamadas a la función, pero están más próximas al caso base. Codigo Recursivo using using using using using using using
System; System.Collections.Generic; System.ComponentModel; System.Data; System.Drawing; System.Text; System.Windows.Forms;
namespace WindowsApplication1 { public partial class Form1 : Form { class cap_recursivo { public float capital(float m, int n, float x) { if (n == 0) { return m; } else { return capital(m, n - 1, x) * (1 + (x/100)); } } } public Form1() { InitializeComponent(); private void Form1_Load(object sender, EventArgs e) { } private void button1_Click(object sender, EventArgs e) {
cap sayo = new cap();
label4.Text = int.Parse(sayo.capital()); } private void label4_Click(object sender, EventArgs e) {
} } }
Diseño
Codigo Iterativo using using using using using using using
System; System.Collections.Generic; System.ComponentModel; System.Data; System.Drawing; System.Text; System.Windows.Forms;
namespace WindowsApplication7 { public partial class Form1 : Form { class cap { public float capital(float m, int n, float x) { if (n == 0) { return m; }
else { //x = x / 100;float inn; //x = x * m; x = x / 100; for (int b = n; b > 0; b--) { m = m + (x * m); //m = x + m; //n = n - 1;(m ) + } } return m; //return capital(m, n - 1, x) * (1 + (x / 100)); } } } public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { } private void button1_Click(object sender, EventArgs e) { cap sayo = new cap();
label4.Text = int.Parse(sayo.capital()); } private void label4_Click(object sender, EventArgs e) {
} } }
Diseño Explicacion
Este programa lo que hace es calcular el interes de un monto inicial de acuerdo a cuanto se pidio prestado, a cuanto porcentaje de interes y el tiempo por el que fue prestado. Al pasar el primer año, se calcula el monto inicial + interes = monto total, y al segundo año o mas se calcula el monto total + interes y asi sucesivamente hasta terminar el periodo del prestamo. La forma recursiva se vuelve a llamar, osea que vuelve a hacer el procedimiento dentro de si misma, y la iteraria es de forma seguida y larga. No importa si va primero o despues, los dos tipos llegan al mismo objetivo, son dos diferentes metodos de hacer la misma accion. El recursivo manda a llamar la misma funcion dentro de si misma y iterativo hace todo el trabajo seguido.
Recursividad en diseño.
Un procedimiento recursivo es aquel que se llama a sí mismo, para poder funcionar tiene que tener una condición de salida que del número de veces que se va a llamar a sí mismo el procedimiento. La recursividad en diseño se refiere a la manera de cómo representar los procedimientos recursivos al momento de diseñar los programas. Dependiendo del método que utilicemos para diseñar la representación gráfica de la recursividad va a ser diferente sin embargo el procedimiento va a ser similar ya que el fin es el mismo poner una condición que nos diga si llamamos al método o que nos mande terminarlo. Un ejemplo en una función genérica seria este:
METODO1 { Procedimiento x; Condición ( ) { Llamada al metodo1; } Fin; }
En un diagrama seria algo así:
NOTA: Estos son dos ejemplos en ambos utilizamos una condición, pero se pueden utilizar ciclos siempre asegurándonos de que nos den salida en algún momento. Lo importante y que tenemos que tomar en cuenta es que la si se cumple el criterio para llamar el procedimiento hay que apuntar a el inicio del método, y al no cumplirse debe apuntar al siguiente paso en el procedimiento o al final o viceversa. Ejemplo: Diseñar un programa que despliegue el contenido de un arreglo el siguiente arreglo: {a, e, i, o, u} Seudocódigo:
1. Creamos la forma que contendrá una listbox para despliegue. 2. Creamos el arreglo que contendrá los elementos (vocales) 3. Después creamos el método despliegue. El cual contendrá: a. Un ciclo for (int i=0;i 4;i++) dentro del cual se hará el despliegue en el índice i del arreglo vocales b. llamará al método despliegue. 4. En el constructor mandamos llamar al método despliegue. Y listo al abrir nuestro programa vamos a desplegar dentro del listbox el arreglo mediante la recursividad. Para ver un programa visual de recursividad ver el tema recursividad.
Complejidad en los algoritmos recursivos.
Un algoritmo recursivo es un algoritmo que se define en términos de sí mismo. Son implementados en forma de subprogramas (funciones, subrutinas, procedimientos, etc) de tal forma que dentro de un subprograma recursivo hay una o más llamadas a él mismo. FUNCIÓN Factorial(n) INICIO SI (n<2) ENTONCES Factorial = 1; SINO Factorial = n * Factorial(n-1); FIN-SI FIN Generalmente, si la primera llamada al subprograma se plantea sobre un problema de tamaño u orden N, cada nueva ejecución recurrente del mismo se planteará sobre problemas, de igual naturaleza que el original, pero de un tamaño menor que N. De esta forma, al ir reduciendo progresivamente la complejidad del problema a resolver, llegará un momento en que su resolución sea más o menos trivial (o, al menos, suficientemente manejable como para resolverlo de forma no recursiva). En esa situación diremos que estamos ante un caso base de la recursividad.