adda - análisis y diseño de datos y algoritmos

615
Escuela Técnica Superior de Ingeniería Informática SEGUNDO Análisis y Diseño de Datos y Algoritmos Departamento de Lenguajes y Sistemas de Información Miguel Angel Cifredo Campos

Upload: ercifredo

Post on 21-Nov-2015

128 views

Category:

Documents


55 download

DESCRIPTION

Análisis y Diseño de Datos y Algoritmos

TRANSCRIPT

  • Escuela Tcnica Superior de Ingeniera Informtica

    SEGUNDO

    Anlisis y Diseo de Datos y Algoritmos Departamento de Lenguajes y Sistemas de Informacin

    Miguel Angel Cifredo Campos

  • --- REVERSO DE PORTADA ---

  • Anlisis y Diseo de Datos y Algoritmos

    1

    1 Introduccin a la Recursividad y su relacin con la Iteracin.

    1.1 Introduccin.

    Objetivos del tema:

    Aprender a definir e implementar algoritmos recursivamente sin y con memoria.

    Introducir el concepto de generalizacin y tamao del problema.

    Discutir la relacin y posible transformacin de los algoritmos recursivos con los algoritmos iterativos.

    Aprender cmo se puede comprobar la correccin de los algoritmos recursivos

    1.2 Elementos de recursividad.

    La definicin recursiva de un problema es una especificacin de la solucin del mismo en base a la de otros problemas de la misma naturaleza pero de un tamao menor. En estas definiciones aparecen los conceptos de:

    Tamao del problema: Entero 0 que nos da una idea de la complejidad del problema.

    Caso base: Problema de tamao pequeo cuya solucin es directa, no recursiva.

    Caso recursivo: Resto de problemas.

    Un algoritmo recursivo es aqul que expresa la solucin de un problema en trminos de llamada o llamadas a s mismo. Cada llamada a s mismo se denomina llamada recursiva.

    La recursividad se presenta como una alternativa a la iteracin.

    Los algoritmos recursivos son apropiados si la definicin del problema est planteada de forma recursiva.

    Para que una definicin recursiva sea vlida debe tener al menos un caso base, y cada caso recursivo definirse en base a otro de menor tamao.

    Veamos un ejemplo: queremos definir el problema factorial de n (n!) y plantearlo de forma recursiva. La definicin ser de la siguiente forma:

    ! = { 1 = 0 ( 1)! > 0

    La idoneidad de la definicin podemos verla con un ejemplo:

    3! = 3 2! = 3 2 1! = 3 2 1 0! = 3 2 1 1

    Con la tcnica conocida como Divide y Vencers, el problema original es dividido en sub-problemas ms pequeos (en este caso en 1 sub-problema).

    Lo primero que debemos tener en cuenta es que cuando vamos a hacer una definicin recursiva de un problema siempre debemos apreciar un conjunto de problemas, llamado tambin dominio. En este caso, el conjunto de problemas viene dado por n! para todo n >= 0. A cada uno de esos problemas le asociaremos un tamao. El tamao del problema deber ser un entero mayor o igual que cero que nos d una idea de la complejidad del problema. Problemas de tamao mayor sern ms complejos que otros de tamao menor. Puede haber distintas formas para escoger el tamao de un problema. En este caso el tamao del problema factorial de n (n!) ser n.

    En lo que sigue representaremos los problemas por p, p1, p2, , pr. Un conjunto de problemas lo representamos por P. Cada problema tendr unas propiedades x. Cada propiedad especfica la representaremos mediante un superndice: x = (x1, , xn). Dentro de un conjunto de problemas P los valores de sus propiedades identifican al problema de manera nica.

    Un problema podemos pensarlo como un objeto. El diseo de los problemas lleva a cabo con la metodologa orientada a objetos. Por la misma razn las propiedades de un problema pueden clasificarse en bsicas y derivadas, individuales y compartidas, consultables y modificables, etc.

    Miguel Angel [email protected]

  • 2

    Para cada problema del conjunto de problemas podemos indicar una invariante del problema, I(x), que es una expresin lgica que debe ser vlida para las propiedades de cada problema en particular. Tambin podemos indicar el dominio, D(x), que es una expresin que es vlida para las propiedades de todos los problemas que estn incluidos en el conjunto de problemas de inters. En general llamaremos aserto, A(x), a cualquier expresin lgica construida con las propiedades del problema. Al escribir los asertos usamos los operadores lgicos NOT,

    AND, OR representados como: , , .

    A cada problema podemos asociar el concepto de tamao que es una nueva propiedad derivada del mismo. El tamao de un problema es una medida de la cantidad de informacin necesaria para representarlo. Normalmente representaremos el tamao de un problema mediante n y lo calcularemos mediante una funcin sobre las propiedades del mismo. Lo representamos por n = t(x) o n = t(p). El tamao del problema deber ser un entero mayor o igual que cero que nos d una idea de la complejidad del mismo. Problemas de tamao mayor sern ms complejos que otros de tamao menor. Puede haber distintas formas para escoger el tamao de un problema.

    Dentro de un conjunto de problemas aquellos que tienen una solucin directa los llamamos casos base. Estos suelen tener un tamao pequeo. En la definicin recursiva del factorial aparece un problema cuya solucin es directa, es el problema 0! (es decir, el problema de tamao n = 0) por lo que en el conjunto de problemas el problema 0! es un caso base, donde su solucin es 1. Existen problemas en los que puede haber ms de un caso base. El resto de problemas del conjunto considerado (en este caso todos los que tienen n>0) los denominaremos casos recursivos. Un caso recursivo se define en funcin de otros problemas de tamao menor. Estos los denominaremos sub-problemas.

    Un mismo problema puede tener diferentes definiciones recursivas. Otra definicin recursiva para el factorial en el que se presentan dos casos base sera:

    ! = { 1 = 0 1 1 = 1 2 ( 1)! > 1 1

    Otra definicin recursiva con menor evidencia pero importante es que ahora el conjunto de problemas es (n,m), n 0, m 1 y la definicin recursiva de la solucin sera:

    ! = (, 1), (,) = { = 0( 1, ) > 0

    En este caso hemos definido el problema n! en base a otro que depende de dos parmetros: el problema fac(n,m). Damos una definicin recursiva de este problema teniendo en cuenta que el tamao de fac(n,m) es n y el de (n,m) tambin es n. El problema original (n!) lo hacemos igual a fac(n,1). Podemos comprobar con un ejemplo que la definicin es adecuada. En efecto:

    3! = (3, 1) = (2, 3 1) = (1, 2 3 1) = (0, 1 2 3 1) = 1 2 3 1

    En este tema aprenderemos a transformar unas definiciones recursivas en otras y a escoger la mejor de ellas para diferentes propsitos.

    Pero para que una definicin recursiva sea vlida debe tener al menos un caso base, y cada caso recursivo definirse en base a otro de menor tamao. Por lo tanto las siguientes son propiedades del factorial:

    0! = 1( + 1)!

    + 1= !

    Pero juntas no forman una definicin recursiva:

    { 1 = 0

    ( + 1)!

    + 1 > 0

    La definicin anterior no constituye un algoritmo correcto porque el caso recursivo se ha definido en base a otros problemas de tamao mayor. Es necesario que los sub-problemas usados para definir el caso recursivo tengan un tamao menor para conseguir que en cada paso recursivo (paso de un problema a los sub-problemas que lo

  • Anlisis y Diseo de Datos y Algoritmos

    3

    definen) se reduzca el tamao. Como este deber ser mayor o igual a cero para todos los problemas de conjunto considerado que en algn momento llegaremos al caso base. Esto no ocurre en la definicin incorrecta anterior.

    Junto con las definiciones recursivas de problemas tenemos algoritmos recursivos. Un algoritmo es recursivo cuando se llama a s mismo.

    int factorial(int n) { int r; assert(n >= 0); if (n == 0) { r = 1; } else { r = n * factorial(n - 1); } return r; }

    Este algoritmo recursivo es la transcripcin mimtica de la primera definicin recursiva que dimos para el factorial. Las otras definiciones tienen algoritmos similares. Hemos escrito la asercin assert(n>=0) para insistir en que el conjunto de problemas que hemos considerado est formado por aquellos que cumple que n 0.

    Otro ejemplo similar lo tenemos para el clculo de la edad de una persona conocido su ao de nacimiento y el ao actual. Podramos resolverlo recursivamente de la siguiente forma:

    (, ) = { 0 = 1 + ( + 1, )

    Veamos una traza de la solucin propuesta:

    edad(2010, 2013) = 3

    1+edad(2011, 2013) = 1+2 = 3 nac act

    1+edad(2012, 2013) = 1+1 = 2 nac act

    1+edad(2013, 2013) = 1+0 = 1 nac act

    0 nac = act

    int edad(int nac, int act) { int r; assert(nac

  • 4

    Otro ejemplo de problema definido recursivamente tenemos el mximo comn divisor ( m.c.d. (a, b) ). Son conocidas varias propiedades del mximo comn divisor:

    {

    (, ) = (, )(, 0) = (, ) = (, %)

    Para hacer una definicin recursiva (disear un algoritmo recursivo) debemos escoger un dominio (conjunto de problemas) y un tamao. Suponemos que el dominio est dado para todos los valores enteros a, b que son mayores o iguales a cero pero no ambos iguales a cero. El tamao de un problema dado los escogemos como el valor de b. Con estas ideas, y las propiedades anteriores, una definicin recursiva es:

    (, ) = { = 0(, %) > 0

    Vemos que el problema est bien definido: el sub-problema tiene un tamao menor que el del problema (dado que el resto de una divisin entera es menor que el dividendo y el divisor), es decir, el menor elemento; y todos los problemas del dominio tienen un tamao mayor o igual a cero.

    Podemos comprobar la idoneidad de la definicin con algunos ejemplos:

    (9,6) = (6,9%6) (6,3) = (3,0) = 3

    (8,12) = (12,8%12) (12,8) = (12,12%8) (8,4) = (4,4%8) (4,0) = 4

    int mcd(int a, int b) { int r; assert(a >= 0 && b >= 0 && !((a == 0) && (b == 0))); if (b == 0) { r = a; } else { r = mcd(b, a % b); } return r; }

    Otro ejemplo, la suma de una lista de enteros, definida como: suma(lista, tamao, pos):

    (, , ) = {

    { } = 0[] = ( 1)

    [] + (, , + 1) < ( 1)

    import java.util.Arrays; import java.util.List; public class SumaListaEnteros { public static Integer suma(List lista, Integer tamao, Integer pos) { Integer r; if (pos == tamao - 1) { r = lista.get(pos); } else { r = lista.get(pos) + suma(lista, tamao, pos + 1); } return r; } public static void main(String[] args) { List lista = Arrays.asList(12, 3, 5, 8); Integer tamao = lista.size(); Integer pos = 0; System.out.println(suma(lista, tamao, pos)); } }

  • Anlisis y Diseo de Datos y Algoritmos

    5

    Otro ejemplo, dada una lista de enteros, obtener el valor mayor, definida como: valMayor(lista, tamao, pos):

    (, , ) = {

    { } = 0[] = 0

    max( [], (, , 1) ) > 0

    import java.util.Arrays; import java.util.List; public class MayorEnLista { public static Integer mayor(List lista, Integer tamao, Integer pos) { Integer r; if (pos == 0) { r = lista.get(pos); } else { r = Math.max(lista.get(pos), mayor(lista, tamao, pos - 1)); } return r; } public static void main(String[] args) { List lista = Arrays.asList(2, 30, 5, 8); Integer tamao = lista.size(); Integer pos = tamao - 1; System.out.println(mayor(lista, tamao, pos)); } }

    Miguel Angel [email protected]

  • 6

    1.3 El operador de Asignacin Paralela.

    Se denotada como: (x, y) (y, x) y asigna el antiguo valor de x a y, y viceversa.

    Es decir, se intercambian los valores de x e y. Sin embargo, indicar { x = yy = x no es una implementacin correcta.

    La idea general es usar variables nuevas, asignar a estas los valores de las expresiones, y posteriormente asignar las nuevas variables a las antiguas, es decir, asigna los elementos de una tupla a los de otra del mismo tipo. Igualmente se puede usar entre listas pero en este caso todos los elementos tienen el mismo tipo.

    Es conveniente conocer con detalle la forma de implementar este operador, su relacin con asertos sobre las variables antes y despus de su ejecucin y, tambin, su relacin con los bloque bsicos de cdigo. Veamos cada uno de estos conceptos y sus relaciones.

    Si designamos por el valor de la variable antes de la asignacin paralela:

    (x1, x2, , xm) ( e1(x1, x2, , xm), e2(x1, x2, , xm), , em(x1, x2, , xm) )

    entonces los valores posteriores de esas variables son = (1, 2, , )

    En una asignacin paralela se toman los valores previos de cada una de las variables y, de forma independiente

    (paralela), se calculan los valores finales de cada una de las variables asignadas. Por otra parte, un bloque bsico

    (en los lenguajes imperativos) es una secuencia de asignaciones que van produciendo efectos laterales:

    x1 = g1(x); x2 = g2(x); ; xn = gn(x);

    Una asignacin paralela y un bloque bsico son conjuntos de asignaciones bastante diferentes. Los lenguajes de

    programacin usuales disponen de bloques bsicos pero no de asignacin paralela. Debemos aprender a

    convertir una asignacin paralela en un bloque bsico y viceversa.

    Veamos un ejemplo: la asignacin paralela: (x, y) (y, x) asigna el antiguo valor de x a y, yviceversa. Es decir,

    intercambia el valor de la x por el de la y, y viceversa. Sin embargo x=y; y=x, es un bloque bsico que hace que

    tanto x como y tomen el antiguo valor de la y. Aunque el bloque bsico se parece a la asignacin paralela el

    resultado es diferente. Sin embargo a=x; x=y; y=a, es un bloque bsico que s intercambia, como la asignacin

    paralela, los valores de x y de y, aunque usa una variable adicional. Como ahora veremos es posible obtener un

    bloque bsico a partir de una asignacin paralela y viceversa. Los bloques bsicos son los elementos necesarios

    a la hora de implementar el algoritmo en un lenguaje de programacin como Java o C. Las asignaciones paralelas

    son ms cmodas cuando hablamos de esquemas algortmicos de las transformaciones de unos en otros.

    Otro elemento importante es la transformacin que sufren los asertos sobre variables cuando hacemos una

    asignacin paralela. Si A(x) es un aserto A(x)[(x1|e1), (x2|e2), , (xm|em)] entonces podemos designar la

    sustitucin simblica de cada una de las variables por sus respectivas expresiones. En la expresin anterior

    usaremos indistintamente A(x)[] o A(x)(). Con esa notacin ya podemos mostrar una relacin muy importante

    entre la asignacin paralela, los asertos en un punto dado del cdigo y la sustitucin simblica. El aserto A(x)

    cuando se ejecuta la asignacin paralela:

    (x1, x2, , xm) ( e1(x1, x2, , xm), e2(x1, x2, , xm), , em(x1, x2, , xm) )

    se transforma en

    A(x) A(x) [(x1|e1), (x2|e2), , (xm|em)] A(x)[ x e(x) ]

    Donde hemos expresado la identidad entre la sustitucin simblica y la asignacin paralela. Por ejemplo el aserto

    A(x, y) x = M y = N se transforma, cuando se ejecuta la asignacin paralela (x, y ) (y, x) (o el bloque bsico

    equivalente) en A(x, y ) y = M x = N. Es decir, se intercambian los valores de las variables.

  • Anlisis y Diseo de Datos y Algoritmos

    7

    En el caso particular que el aserto A(x) no dependa de una variable xi la sustitucin simblica anterior debe

    entenderse como: A(x)[xi|ei] A(x) xi = ei(x).

    Veamos ahora cmo conseguir un bloque bsico equivalente a una asignacin paralela. La idea general es usar

    variables nuevas, asignar a estas variables los valores de las expresiones, posteriormente asignar las nuevas

    variables a las antiguas y simplificar el bloque bsico. El esquema es entonces:

    (x1,x1,,xm) (e1,e2,,em)

    {

    a1 = e1;

    a1 = e2;

    am = em;

    x1 = a1;

    x2 = a2;

    xm = am;

    Por ejemplo,

    (, ) (y, x) {

    = ; = ; = ; = ;

    { = ; = ; = ;

    En el tercer paso hemos eliminado la variable a, la ecuacin donde se defina y hemos sustituido su uso por su

    valor en la ecuacin que define la variable x.

    Veamos ahora cmo conseguir una asignacin paralela equivalente a un bloque bsico (y consecuentemente

    una sustitucin simblica equivalente). La idea es partir de los valores iniciales de las variables (que

    representaremos como: , , etc.) e ir acumulando los valores de las variables a partir de los iniciales. Cuando

    tengamos los valores de las variables al final del bloque bsico a partir de sus valores iniciales tendremos la

    asignacin paralela equivalente:

    { = = =

    {

    = ; (|)

    = ; (|, |)

    = ; (|, |, |) (x, y)(y, x)

    Como hemos visto el operador de asignacin paralela se reduce a un bloque bsico formado por una secuencia

    de asignaciones cuando las variables del lado izquierdo y del lado derecho son distintas. Si la asignacin paralela

    es entre listas, con variables distintas, la implementacin puede hacer con una estructura for de la forma:

    [1, 2, , ] [1, 2, , ]

    ( = 0; < ; + +) {

    = }

    Pero cuando hay variables comunes en el lado derecho y en el izquierdo debemos usar variables temporales en

    el bloque bsico para implementar la asignacin paralela.

    Veamos otro ejemplo: (a, b) (b, a%b).

    Como en el lado derecho y en el izquierdo comparten variables, la implementacin directa como secuencia de

    asignaciones, es incorrecta. En efecto la secuencia de asignaciones siguiente no es equivalente a la asignacin

    paralela anterior porque la primera asignacin cambia el valor de la a y este valor se usa en la segunda en lugar

    del valor original, esto es, a = b; b = (a%b);, luego la implementacin correcta es: c = b; d = (a%b); a = c; b = d;.

    Que puede ser simplificada eliminando la variable d, c = b; b = (a%b); a=c;.

    Miguel Angel [email protected]

  • 8

    Hemos visto la relacin existente ente asignaciones paralela, bloques bsicos, asertos y sustituciones simblicas.

    Estas relaciones podemos generalizarlas al caso de las asignaciones paralelas generalizadas y los bloques

    bsicos generalizados.

    Una asignacin paralela generalizada es de la forma:

    {

    1() 1() 2() 2()

    0() 1()2()

    O en forma de cdigo:

    if (g1(x)) { x e1( x); } else if (g2(x)) { x e2( x); } else { x e0( x); }

    Llamamos bloque bsico generalizado a una secuencia de boques if, else if, else con bloque bsicos en

    las ramas del if. El bloque bsico generalizado equivalente a la asignacin paralela generalizada es de la forma:

    if (g1) { b1; } else if (g2) { b2; } else { b0; }

    Donde b1, b2, , b0 son bloques bsicos equivalentes a las asignaciones paralelas x e1(x), x e2(x), , x e0.

    Asumiendo que s1, s2, , s0 sean las sustituciones equivalentes respectivas entonces el bloque bsico

    generalizado transforma un aserto en la forma A(x) a otro de la forma A(x).

    A(x) (A(x) g1(x))[s1] (A(x) g2(x))[s2] (A(x) g0(x))[s0]

    donde g0(x) = g1(x) g2(x)

  • Anlisis y Diseo de Datos y Algoritmos

    9

    1.4 Generalizacin de problemas.

    En muchos casos es mucho ms fcil encontrar una definicin recursiva de un problema si lo consideramos como un caso particular de un conjunto de problemas ms amplio (es decir, definidos por ms parmetros). Desde este punto de vista el problema que tenemos que resolver se obtiene dando valores concretos a algunos parmetros del problema ms general (problema generalizado).

    Para ello se hace una definicin general del problema que luego se ajusta en el problema concreto que queramos resolver dando valores a los parmetros correspondientes.

    Habr que diferenciar entre:

    Parmetros compartidos: Comunes a todos los problemas particulares de un conjunto de problemas.

    Parmetros individuales: Especficos de cada problema concreto.

    Esta distincin es importante porque nos permite identificar a un problema dentro del conjunto slo por sus parmetros individuales.

    Veamos un ejemplo, sea una definicin recursiva para el clculo del mximo valor de una lista de enteros proporcionada la lista (dt) y el nmero total de elementos que contiene (n): maxVal(dt, n)

    Problema: E = (dt, n) Resultado: R = tipo de elementos de dt

    Con estos datos es casi imposible plantear una definicin recursiva. Por lo tanto, generalizamos:

    Definicin generalizada: Calcular el mximo valor de una lista dt considerando los elementos comprendidos entre i hasta el tamao n de la lista: mv (i, dt, n)

    Problema: E = (i, dt, n) Resultado: R = tipo de elementos de dt

    Ahora es ms fcil dar una definicin para mv(i, dt, n), basndose en la idea que el mximo de los valores contenidos en las posiciones de un sub-lista (i, n) es el mximo entre la primera posicin y el mximo del valor contenido en las posiciones restantes excluyendo la primera.

    Por ltimo, hay que escoger qu problema generalizado (i, dt, n), equivale al problema original, (dt, n).

    La funcin maxVal(dt, n) se encargar de instanciar el problema generalizado escogido mv(i, dt, n) y controlar que los parmetros de entrada (globales) sean correctos.

    No obstante, podemos plantear otras soluciones a la descrita anteriormente. Veamos en primer lugar una posible definicin recursiva y luego el algoritmo correspondiente.

    (, ) = (0, , , )

    (, , , ) = { [] = 1

    max([], ( + 1, , , )) > 1

    max(, ) = ? :

    La definicin recursiva anterior se basa en la siguiente idea: el mximo de los valores contenidos en las posiciones de una lista es el mximo entre la primera posicin y el mximo del valor contenido en las posiciones restantes excluyendo la primera.

    Miguel Angel [email protected]

  • 10

    El algoritmo correspondiente es:

    int maxVal(int * dt, int n) { return mv(0, n, dt, n); }

    int mv(int i, int j, int * dt, int n) { int r; assert(j > i && i >= 0 && j = b ? a : b; }

    Veamos los elementos de esta definicin recursiva. En primer lugar hemos generalizado el problema. El problema original era encontrar el mximo valor de las posiciones de una lista dt con n elementos. El problema generalizado se define como encontrar el mximo valor de las posiciones i (incluida) hasta la j (no incluida) de una lista dt con n elementos. El problema original se obtiene dando los valores 0 y n a los parmetros i y j, respectivamente, del problema generalizado.

    Los parmetros del problema generalizado podemos clasificarlos en: compartidos (dt, n) e individuales (i, j).

    Por tanto, un problema concreto dentro del conjunto considerado lo identificamos con el par de parmetros i, j. Los otros dos parmetros son compartidos por todos los problemas.

    El resto de las funciones del esquema son:

    d(i, j, dt, n) = j>i && i>=0 && j

  • Anlisis y Diseo de Datos y Algoritmos

    11

    1.5 Esquemas Recursivos sin Memoria.

    En general, los algoritmos recursivos siguen el siguiente esquema:

    R f(E x) {

    R r;

    LR s;

    LE y;

    assert(D(x));

    if(b(x)){

    r = sb(x);

    } else {

    y = sp(x);

    assert(t(y) < t(x));

    s = f(y);

    r = c(x,s);

    }

    return r;

    }

    Llamaremos a este esquema recursivo sin memoria. Este esquema es denominado usualmente como divide y vencers sin memoria.

    Una versin ms compacta sera:

    () = {() ()

    (, (())) ! ()

    O en forma de esquema:

    R f(E x) {

    R r;

    assert(D(x));

    if(b(x)){

    r = sb(x);

    } else {

    r = c(x,f(sp(x)));

    }

    return r;

    }

    Si queremos hacer explcito el caso de un nmero variable de sub-problemas:

    R f(E x) {

    R r;

    S s;

    T y;

    assert(D(x));

    if(b(x)){

    r = sb(x);

    } else {

    y = sp(x);

    s = [];

    for(E z: y){

    assert(t(z) < t(x));

    s = s+f(z);

    }

    r = c(x,s);

    }

    return r;

    }

    Miguel Angel [email protected]

  • 12

    Como hemos comentado antes, al hacer una definicin recursiva debemos considerar un conjunto de problemas. Representamos los problemas por p, p1, p2, , pr. Un conjunto de problemas lo representaremos por P. Cada problema tendr m propiedades. Cada propiedad especfica la representaremos mediante un superndice: x = (x1, , xn). Puede haber k sub-problemas. Entonces tenemos:

    y = [x1, x2, , xk] s = [s1, s2, , sk] sp(x) = [sp1(x), sp2(x), , spk(x)]

    Por ltimo un problema puede devolver u resultados y por tanto: r = (r1, r2, , ru).

    En el prrafo anterior hemos escogido la notacin () para representar tuplas de valores, es decir, agregados de valores de distintos tipos. Hemos representado por [] listas de valores de un tipo dado.

    Por tanto: LR Lista, LE Lista y los tipos R y E son tuplas de valores.

    Segn el nmero de llamadas que realice el algoritmo recursivo hay dos tipos de recursividad:

    Recursividad simple o lineal: En cada llamada recursiva se ejecuta a lo sumo, una llamada a la propia funcin.

    ! = { 1 = 01 = 1 ( 1)! > 1

    Recursividad mltiple: En cada llamada recursiva puede ejecutarse ms de una llamada a la propia funcin.

    () = { 0 = 01 = 1( 1) + ( 2) > 1

    Los algoritmos simples siguen el siguiente esquema:

    R f(E x) {

    R r;

    R s;

    E y;

    if(b(x)){

    r = sb(x);

    } else {

    y = sp(x);

    s = f(y);

    r = c(x, s);

    }

    return r;

    }

    Los algoritmos mltiples siguen el siguiente esquema:

    R f(E x) {

    R r;

    LR s;

    LE y;

    if(b(x)){

    r = sb(x);

    } else {

    y = sp(x);

    for(E z: y){

    s = s + f(z);

    }

    r = c(x, s);

    }

    return r;

    }

    E: Tipo de las propiedades del problema.

    R: Tipo del resultado de la solucin.

    E: Tipo de las propiedades del problema (individuales

    y compartidas ).

    R: Tipo del resultado de la solucin.

    LE: Lista de tipo E, de los diversos sub-problemas.

    LR: Lista de tipo R, de resultados intermedios de los sub-

    problemas.

    +: Smbolo que indica la unin de un elemento a una lista.

  • Anlisis y Diseo de Datos y Algoritmos

    13

    En general los algoritmos recursivos siguen el siguiente esquema:

    T f(T1 p1, , Tn pn) {

    T r, rp1, ;

    assert(d(p1,,pn));

    if(b1(p1,,pn)){

    r = sb1(p1,,pn);

    }

    else {

    = sp1(p1,,pn);

    assert(t(q1,,qn) < t(p1,,pn));

    rp1 = f(q1,,qn);

    r = c(p1,,pn,rp1,);

    }

    return r;

    }

    Como hemos comentado antes, al hacer una definicin recursiva debemos considerar un conjunto de problemas. Representaremos los problemas por letras maysculas P, Q, a su vez cada problema, del conjunto de problemas, viene representado por un conjunto de parmetros: P = , Q = ,

    El algoritmo recursivo busca la solucin para un problema dado tomando los parmetros que lo representan, es decir, p1 de tipo T1, , pn de tipo Tn.

    En el esquema recursivo aparecen las siguientes funciones y variables:

    d(p1,,pn): Es una funcin lgica especfica del dominio de conjunto de problemas. Es decir, verdadero si el problema pertenece al conjunto de problemas. Se recoge en un aserto el hecho de que el problema sea uno de los considerados.

    t(p1,,pn): Es una funcin lgica especfica del dominio de conjunto de problemas. Es decir, verdadero si el problema pertenece al conjunto de problemas. Cada sub-problema debe ser de un tamao inferior al problema de partida. Esto se recoge en un aserto del esquema.

    b1(p1,,pn): Es una funcin lgica que devuelve verdadero si el problema es un caso base. En general podramos tener varios casos base: b1(p1,,pn), b2(p1,,pn),

    sb1(p1,,pn): Es una funcin que devuelve un valor de tipo T que es la solucin del caso base 1. Igualmente podramos tener sb2(p1,,pn),

    sp1(p1,,pn): Es una funcin que calcula los parmetros del primer sub-problema al que se reduce el problema original. En el esquema hemos escrito = sp1(p1,,pn). Donde son los parmetros del primer sub-problema que, en general, pueden ser varios. La forma de implementar esto es con una secuencia de expresiones para calcular cada uno de los parmetros: q1 = e11(p1,,pn), , qn = e1n(p1,,pn). En general, en una definicin recursiva, puede haber varios sub-problemas.

    rp1, rps,: Son variables de tipo T que guardan la solucin del sub-problema 1, 2, . r es una variable de tipo T que guarda la solucin del problema. En aquellos casos en que el tipo T sea void no habr return en el esquema. La solucin ser devuelta en un parmetro de entrada-salida.

    c(p1,,pn,rp1,): Es una funcin, que llamaremos funcin de combinacin, que obtiene el resultado combinando los parmetros del problema con el resultado de los sub-problemas (resultados de las llamadas recursivas).

    Debe tenerse en cuenta las siguientes interpretaciones, donde es el operador de asignacin paralela:

    t([x1, x2, , xk]) < t(x) t(x1) < t(x2) < t(x) t(xk) < t(x)

    f([x1, x2, , xk]) [f(x1), f(x2), , f(xk)]

    s = f(x) [s1, s2, , sk] f([x1, x2, , xk]) s1 = f(x1); s2 = f(x2); ; sk = f(xk) y = sp(x) [x1, x2, , xk] [sp1(x), sp2(x), , spk(x)]) x1 = sp(x); x2 = sp(x); ; xk = sp(x)

    Miguel Angel [email protected]

  • 14

    Veamos cmo en el ejemplo del factorial podemos identificar cada una de estas funciones:

    int factorial(int n) { int r; assert(n >= 0); if (n == 0) { r = 1; } else { r = n * factorial(n - 1); } return r; }

    Las funciones anteriores son:

    d(n) = n>=0 t(n) = n b1(n) = n==0 sb1(n) = 1 sp1(n) = n-1 c(n,r1) = n*r1

    Y en el problema del mximo comn divisor:

    int mcd(int a, int b) { int r; assert(a >= 0 && b >= 0 && !((a == 0) && (b == 0))); if (b == 0) { r = a; } else { r = mcd(b, a % b); } return r; }

    Las funciones del esquema son:

    d(a,b) = a>=0 && b>=0 && !((a==0) && (b==0)) t(a,b) = b b1(a,b) = b==0 sb1(a,b) = a sp1(a,b) = c(a,b,rp1) = rp1

    Para comprender algunas de las funciones anteriores podemos escribir la llamada recursiva (r = mcd(b, a%b);) en la forma:

    a1 = b;

    b1 = a%b;

    rp1 = mcd(a1, b1);

    r = rp1;

  • Anlisis y Diseo de Datos y Algoritmos

    15

    1.6 Esquemas Recursivos con Memoria.

    Recordemos el problema definido recursivamente del clculo de la serie de Fibonacci. Comienza con un 0, luego con un 1, y a partir de ah, cada nmero es la suma de los dos siguientes. Esto ltimo nos indica recursividad. Por tanto, la funcin de recursividad puede ser:

    () = { 0 = 01 = 1( 1) + ( 2) > 1

    Si nos fijamos en el conjunto de operaciones que componen el proceso iterativo, observamos que debe obtener resultados que ya han sido calculados previamente. Por lo que se puede optimizar el proceso almacenando en una estructura de datos (como un mapa o diccionario) aquellos resultados parciales que va obteniendo y recuperarlos en caso de tener que volverlos a calcular.

    Para evitar estas repeticiones diseamos un nuevo algoritmo recursivo que llamaremos Divide y Vencers con Memoria.

    De forma general es necesario una variable memo, de tipo HashMap, que en Java tiene los mtodos:

    Boolean memo.containsKey(x): Ha sido resuelto el problema x?

    R r = memo.get(x): Obtiene la solucin del problema ya resuelto x.

    void memo.put(x, r): Almacena la solucin obtenida r para el problema x.

    Los algoritmos recursivos con memoria siguen un esquema similar a este:

    R f(E x) {

    Map m = inicializar;

    return f1(x,m);

    }

    R f1(E x, Map m) {

    R r;

    R s;

    E y;

    if(contains(m,x)){

    r = get(m,x);

    } else if(b(x)){

    r = sb(x);

    put(m,x,r);

    }else{

    y = sp(x);

    s = f1(z,m);

    r = c(x,s);

    put(m,x,r);

    }

    return r;

    }

    Miguel Angel [email protected]

  • 16

    Veamos un ejemplo de la implementacin de la serie de Fibonacci con memoria:

    import java.util.HashMap; public class SerieDeFibonacci { private static HashMap memo = new HashMap(); public static Long fibonacci(Integer n) { Long r; memo.put(0, 0L); memo.put(1, 1L); if (memo.containsKey(n)) { r = memo.get(n); } else { r = fibonacci(n - 1) + fibonacci(n - 2); } memo.put(n, r); return r; } public static void main(String[] args) { Integer n = new Integer(args[0]); System.out.println(fibonacci(n)); } }

    1.7 Algoritmos iterativos.

    La idea es obtener algoritmos iterativos (esquemas secuenciales) a partir de los algoritmos recursivos.

    Los conceptos que necesitamos son:

    Estado: Conjunto de valores concretos para los parmetros que definen los problemas considerados.

    Estado inicial: Estado que representa el problema inicial.

    Estado final: Estado que representa al problema final.

    Transicin: Funcin que calcula un nuevo estado a partir de otro.

    Los algoritmos iterativos siguen un esquema similar a este:

    R f(T p) {

    E x = i(p);

    while(g(x)){

    x = s(x);

    }

    return r(x);

    }

    T: Tipo de problema original.

    E: Tipo del problema generalizado.

    R: Tipo de la solucin.

    i(p): Instanciacin.

    g(x): Verdadero si no es estado final.

    s(x): Siguiente problema.

    r(x): Clculo de la solucin.

  • Anlisis y Diseo de Datos y Algoritmos

    17

    1.8 Transformacin Recursin Iteracin.

    La recursividad lineal puede ser:

    Final: Cuando la forma de la funcin de combinacin es de la forma c(x, r) = r, es decir, no depende de

    las propiedades del problema.

    No final: En cualquier otro caso.

    Es importante en la recursividad lineal final que la solucin del problema sea la misma que la del caso base

    puesto que cada problema tiene la misma solucin que el sub-problema al que se reduce.

    Dada una definicin recursiva final:

    () = (())

    () = { () ()

    (()) ! ()

    Si el problema generalizado es x, el original y, y la funcin de instanciacin i(y), entonces el algoritmo iterativo resultante es de la forma:

    R f(T y){

    E x = i(y);

    while(!b(x)){

    x = sp(x);

    }

    return sb(x);

    }

    Pero, un algoritmo no final, se podra transformar en otro final para as poder transformarlo a iterativo?

    Una solucin puede ser mediante la generalizacin del problema, aadiendo parmetros acumuladores.

    Dada una definicin recursiva no final:

    () = { () ()

    (, (())) ! ()

    La definicin final quedara:

    () = (, )

    (, ) = { (, ) ()

    ((), (, )) ! ()

    La transformacin de recursividad mltiple a iterativo consiste en generalizar el problema incorporando los sub-problemas necesarios y sus soluciones calculadas. La iteracin empieza por los casos base y acaba en el problema original.

    Miguel Angel [email protected]

  • 18

    1.9 Correccin de los algoritmos recursivos.

    La correccin de los algoritmos recursivos depende de la especificacin del problema que estemos intentando

    resolver. En ellos debemos observar los datos de entrada y los resultados esperados, as como la relacin entre

    ellos.

    Podemos utilizar como mtodos para probar que un algoritmo recursivo es correcto la Induccin o bien Pruebas

    por contradiccin.

    Para el mtodo de induccin debemos estudiar:

    o Casos base: Verificar que lo que se implementa responde a lo especificado. o Paso inductivo: Dado que lo especificado se cumple para la k primeras instancias, probar que se cumple

    tambin para la instancia k+1.

    Con esto se consigue la correccin parcial, para la correccin total, los parmetros de las llamadas recursivas se deben acercar al caso base.

    Para la correccin parcial del caso del factorial se debe demostrar que factorial(n) realiza n!

    - Si n > 0, la prueba de la condicin n = 0 falla y x = n * factorial(n-1).

    - Por hiptesis inductiva, factorial(n-1) devuelve (n-1)! , por lo que factorial(n) devuelve n*(n-1)!, que en realidad es n!

    Para la correccin total n-1 < n y si n es positivo siempre nos acercaremos a n = 0.

    Truco: Pasos para transformar una funcin recursiva no final en otra s final.

    Funcin Recursiva No Final:

    () { 1 = 0

    ( 1) 0

    Funcin Recursiva Final:

    (, ) { = 0

    ( 1, ) 0

    Paso 1: Se mantienen los mismos casos bases y casos recursivos.

    Paso 2: Se copia la misma variacin de n de cada llamada recursiva (i.e. n-1)

    Paso 3: La misma operacin que se realiza sobre la llamada recursiva no final, se aplica sobre el acumulador.

    Paso 4: El caso base se convierte en el acumulador.

  • Anlisis y Diseo de Datos y Algoritmos

    19

    1.10 Ejercicios resueltos.

    1.10.1 Clculo de la edad de una persona.

    Dados el ao actual y el de nacimiento de una persona, determinar la edad que tiene.

    SOLUCIN:

    Algoritmo No Final:

    (, ) = { < 0 = 1 + ( 1, ) >

    Algoritmo Final:

    (, , ) = { < = ( 1, , + 1) >

    Iterativo:

    funcin entero edad (entero anoAct, entero anoNac) entero acu = 0 mientras (anoAct != anoNac) hacer acu = acu + 1 anoAct = anoAct - 1 fin-mientras retornar acu fin-funcin

    Miguel Angel [email protected]

  • 20

    #include #include int edad_N(int anoAct, int anoNac) { int res = 0; if (anoAct == anoNac) { res = 0; } else { res = 1 + edad_N(anoAct, anoNac + 1); } return res; } int edad_F(int anoAct, int anoNac, int acu) { int res = 0; if (anoAct == anoNac) { res = acu; } else { res = edad_F(anoAct, anoNac + 1, acu + 1); } return res; } int edad_I(int anoAct, int anoNac) { int res = 0; while (anoAct != anoNac) { res = res + 1; anoNac = anoNac + 1; } return res; } int metodo() { int opc; printf("\nTIPO DE ALGORITMO"); printf("\n 1 - Recursividad NO FINAL"); printf("\n 2 - Recursividad FINAL"); printf("\n 3 - Iterativo"); printf("\nOpc: "); scanf("%d", &opc); printf("\n"); return opc; } int main() { int anoAct; int anoNac; int r; printf("\nEDAD DE UNA PERSONA\n\n"); printf("Ano Actual : "); scanf("%d", &anoAct); printf("Ano Nacimiento: "); scanf("%d", &anoNac); switch (metodo()) { case 1: r = edad_N(anoAct, anoNac); break; case 2: r = edad_F(anoAct, anoNac, 0); break; case 3: r = edad_I(anoAct, anoNac); break; } printf("Esa persona tiene %d anios.\n", r); return 0; }

  • Anlisis y Diseo de Datos y Algoritmos

    21

    1.10.2 Mximo Comn Divisor (Algoritmo de Euclides).

    Calcular el mximo comn divisor de dos nmeros, mcd(a, b), utilizando el Algoritmo de Euclides:

    (, ) = { = (, ) < (, %) >

    SOLUCIN:

    Algoritmo No Final:

    La definicin recursiva indicada en el enunciado es Final, por tanto no existe No Final.

    Algoritmo Final:

    (, ) = { = (, ) < (, %) >

    Algoritmo Iterativo:

    funcin entero mcd (entero a, entero b) entero res entero tmp mientras (b != 0) hacer tmp = a a = b si (a < b) entonces b = tmp sino b = tmp % b fin-si fin-mientras res = a retornar res fin-funcin

    Miguel Angel [email protected]

  • 22

    #include int mcd_F(int a, int b) { int res; if (b == 0) { res = a; } else { if (a < b) { res = mcd_F(b, a); } else { res = mcd_F(b, a % b); } } return res; } int mcd_I(int a, int b) { int res; int tmp; while (b != 0) { tmp = a; a = b; if (a < b) { b = tmp; } else { b = tmp % b; } } res = a; return res; } int metodo() { int opc; printf("\nTIPO DE ALGORITMO"); printf("\n 1 - Recursividad NO FINAL (NO SOPORTADO)"); printf("\n 2 - Recursividad FINAL"); printf("\n 3 - Iterativo"); printf("\nOpc: "); scanf("%d", &opc); printf("\n"); return opc; } int main() { int a, b; int r; printf("\nALGORITMO DE EUCLIDES: MAXIMO COMUN DIVISOR (mcd)\n\n"); printf("\nEjemplo: mcd(273,2366) = 91\n\n"); printf("Introduzca el primer numero: "); scanf("%d", &a); printf("Introduzca el segundo numero: "); scanf("%d", &b); switch (metodo()) { case 1: break; case 2: r = mcd_F(a, b); break; case 3: r = mcd_I(a, b); break; } printf("El maximo comun divisor de %d y %d es %d \n", a, b, r); return 0; }

  • Anlisis y Diseo de Datos y Algoritmos

    23

    1.10.3 Factorial de un nmero.

    Calcular el factorial de un nmero n (n!) para nmeros enteros positivos.

    SOLUCIN:

    Algoritmo No Final:

    () = { < 01 = 0 ( 1) > 0

    Algoritmo Final:

    (, ) = { < 0 = 0( 1, ) > 0

    Algoritmo Iterativo:

    funcin entero factorial (entero n) entero acu = 1 mientras (n != 0) hacer acu = acu * n n = n - 1 fin-mientras retornar acu fin-funcin

    Miguel Angel [email protected]

  • 24

    #include int factorial_N(int n) { int res = 1; if (n == 0) { res = 1; } else { res = n * factorial_N(n - 1); } return res; } int factorial_F(int n, int acu) { int res = 1; if (n == 0) { res = acu; } else { res = res * factorial_F(n - 1, n * acu); } return res; } int factorial_I(int n) { int res = 1; while (n != 0) { res = res * n; n--; } return res; } int metodo() { int opc; printf("\nTIPO DE ALGORITMO"); printf("\n 1 - Recursividad NO FINAL"); printf("\n 2 - Recursividad FINAL"); printf("\n 3 - Iterativo"); printf("\nOpc: "); scanf("%d", &opc); printf("\n"); return opc; } int main() { int n; int r; printf("\nFACTORIAL DE N\n\n"); printf("Introduzca un valor para n: "); scanf("%d", &n); switch (metodo()) { case 1: r = factorial_N(n); break; case 2: r = factorial_F(n, 1); break; case 3: r = factorial_I(n); break; } printf("El factorial de %d, \(%d!) es %d \n", n, n, r); return 0; }

  • Anlisis y Diseo de Datos y Algoritmos

    25

    1.10.4 Potencia.

    Dada una base (a) y un exponente (n), calcular la potencia de an.

    SOLUCIN:

    Algoritmo No Final:

    (, ) = { 1 = 0 (, 1) > 0

    Algoritmo Final:

    (, , ) = { = 0(, 1, ) > 0

    Algoritmo Iterativo:

    funcin entero potencia (entero a, entero n) entero acu = 1 mientras (n > 0) hacer acu = acu * a n = n - 1 fin-mientras retornar acu fin-funcin

    Miguel Angel [email protected]

  • 26

    #include int potencia_N (int a, int n) { int res; if (n == 0) { res = 1; } else { res = a * potencia_N(a, n-1); } return res; } int potencia_F(int a, int n, int acu) { int res = 1; if (n==0) { res = acu; } else { res = potencia_F(a, n-1, a*acu); } return res; } int potencia_I(int a, int n) { int res = 1; while (n>0){ res = res * a; n--; } return res; } int metodo() { int opc; printf("\nTIPO DE ALGORITMO"); printf("\n 1 - Recursividad NO FINAL"); printf("\n 2 - Recursividad FINAL"); printf("\n 3 - Iterativo"); printf("\nOpc: "); scanf("%d", &opc); printf("\n"); return opc; } int main() { int a, n; int r; printf("\nPOTENCIA DE A^N\n\n"); printf("Introduzca la base: "); scanf("%d", &a); printf("Introduzca el exponente: "); scanf("%d", &n); switch (metodo()) { case 1: r = potencia_N(a, n); break; case 2: r = potencia_F(a, n, 1); break; case 3: r = potencia_I(a, n); break; } printf("El resultado para la entrada %d^%d es %d", a, n, r); return 0; }

  • Anlisis y Diseo de Datos y Algoritmos

    27

    1.10.5 Potencia segn la paridad del exponente.

    Dada una base (a) y un exponente (n), calcular la potencia de an teniendo en cuenta si el exponente es un nmero par o impar, segn se ilustra a continuacin:

    8 = (4)2 8

    9 = (4)2 9

    SOLUCIN:

    Algoritmo No Final:

    (, ) =

    {

    = 1

    ( (,

    2))

    2

    1 (%2) = 0

    ( (, 1

    2))

    2

    1 (%2) = 1

    Algoritmo Final:

    (, , , ) =

    {

    = 0

    (,

    2, 2, ) 0 (%2) = 0

    (, 1

    2, 2, ) 0 (%2) = 1

    : (, , , 1)

    Algoritmo Iterativo:

    funcin entero potencia (entero a, entero n) entero r = a entero acu = 1 mientras (n > 0) hacer si (n % 2 = 1) entonces acu = acu * r fin-si r = r * r n = n / 2 fin-mientras retornar acu fin-funcin

    Miguel Angel [email protected]

  • 28

    #include #include int potencia_N(int a, int n) { int res; if (n == 1) { res = a; } else { if (n % 2 == 0) { res = pow(potencia_N(a, n / 2), 2); } else { res = pow(potencia_N(a, (n - 1) / 2), 2) * a; } } return res; } int potencia_F(int a, int n, int r, int acu) { int res; if (n == 0) { res = acu; } else { if (n % 2 == 0) { res = potencia_F(a, n / 2, r * r, acu); } else { res = potencia_F(a, (n 1) / 2, r * r, acu * r); } } return res; } int potencia_I(int a, int n) { int r = a; int acu = 1; while (n > 0) { if (n % 2 == 1) { acu = acu * r; } r = r * r; n = n / 2; } return acu; } int metodo() { int opc; printf("\nTIPO DE ALGORITMO"); printf("\n 1 - Recursividad NO FINAL"); printf("\n 2 - Recursividad FINAL"); printf("\n 3 - Iterativo"); printf("\nOpc: "); scanf("%d", &opc); printf("\n"); return opc; } int main() { int a, n; int r; printf("\nPOTENCIA DE A^N SEGN PARIDAD DEL EXPONENTE\n\n"); printf("Introduzca la base: "); scanf("%d", &a); printf("Introduzca el exponente: "); scanf("%d", &n); switch (metodo()) { case 1: r = potencia_N(a, n); break; case 2: r = potencia_F(a, n, a, 1); break; case 3: r = potencia_I(a, n); break; } printf("El resultado para la entrada %d^%d es %d", a, n, r); return 0; }

  • Anlisis y Diseo de Datos y Algoritmos

    29

    1.10.6 Sumar los dgitos de un nmero.

    Dada un nmero entero positivo, n, calcular la suma aritmtica de sus dgitos.

    Ejemplo: Sea el nmero n = 2347 sumaDig(n) = 2 + 3 + 4 + 7 = 16

    SOLUCIN:

    Algoritmo No Final:

    () = { 0 = 0(\10) + %10 > 0

    Algoritmo Final:

    () = { = 0(\10, + (%10)) > 0

    Algoritmo Iterativo:

    funcin entero sumaDig (entero n) entero acu = 0 mientras (n != 0) hacer acu = acu + (n % 10) n = n \ 10 fin-mientras retornar acu fin-funcin

    Miguel Angel [email protected]

  • 30

    #include int sumaDig_N(int n) { int res; if (n == 0) { res = 0; } else { res = sumaDig_N(n / 10) + (n % 10); } return res; } int sumaDig_F(int n, int acu) { int res; if (n == 0) { res = acu; } else { res = sumaDig_F(n / 10, (n % 10) + acu); } return res; } int sumaDig_I(int n) { int res = 0; while (n != 0) { res = res + (n % 10); n = n / 10; } return res; } int metodo() { int opc; printf("\nTIPO DE ALGORITMO"); printf("\n 1 - Recursividad NO FINAL"); printf("\n 2 - Recursividad FINAL"); printf("\n 3 - Iterativo"); printf("\nOpc: "); scanf("%d", &opc); printf("\n"); return opc; } int main() { int n; int r; printf("\nSUMAR LOS DIGITOS DE UN NUMERO\n\n"); printf("Introduzca el numero: "); scanf("%d", &n); switch (metodo()) { case 1: r = sumaDig_N(n); break; case 2: r = sumaDig_F(n, 0); break; case 3: r = sumaDig_I(n); break; } printf("La suma de los digitos del numero %d es %d\n", n, r); return 0; }

    Nota: En lenguaje C, si la variable n es entera, n/10 devuelve la divisin entera, lo que anotamos como n\10.

  • Anlisis y Diseo de Datos y Algoritmos

    31

    1.10.7 Nmeros de la Serie de Fibonacci.

    Calcular los nmeros de la serie de Fibonacci, siendo su secuencia como sigue:

    () = { 0 1( 1) + ( 2) > 1

    SOLUCIN:

    Algoritmo No Final:

    () = { 1( 1) + ( 2) > 1

    Algoritmo Final:

    La definicin recursiva indicada no existe como Final.

    Algoritmo Iterativo:

    funcin entero fib (entero n) entero res entero n1, n0 entero i si (n

  • 32

    #include int fib_F(int n) { int res; if (n

  • Anlisis y Diseo de Datos y Algoritmos

    33

    1.10.8 Combinaciones sin repeticin.

    Calcule el valor de las combinaciones sin repeticin de n objetos tomados de k en k, esto es: ()

    SOLUCIN:

    Algoritmo No Final:

    (, ) = { 0 > 1 = = 0( 1, ) + ( 1, 1) . . .

    Algoritmo Final:

    La definicin recursiva indicada no existe como Final.

    Algoritmo Iterativo:

    funcin entero CombSR (entero n, entero k) entero acu = 1 mientras (k < n) hacer acu = acu + (k) + (k-1) n = n - 1 fin-mientras retornar acu fin-funcin

    Miguel Angel [email protected]

  • 34

    #include int combSR_F(int n, int k) { int res; if (k > n) { res = 0; } else { if ((k == 0) || (k == n)) { res = 1; } else { res = combSR_F(n - 1, k) + combSR_F(n - 1, k - 1); } } return res; } int combSR_I(int n, int k) { int acu = 1; while (k < n) { acu = acu + (k) + (k - 1); n--; } return acu; } int metodo() { int opc; printf("\nTIPO DE ALGORITMO"); printf("\n 1 - Recursividad NO FINAL"); printf("\n 2 - Recursividad FINAL (NO SOPORTADO)"); printf("\n 3 - Iterativo"); printf("\nOpc: "); scanf("%d", &opc); printf("\n"); return opc; } int main() { int n, k; int r; printf("\nCOMBINACIONES SIN REPETICION DE N ELEMENTOS TOMADOS DE K EN k.\n\n"); printf("\nEjemplo: Elementos=16, Grupos=3, resultado = 560\n\n"); printf("Introduzca los elementos: "); scanf("%d", &n); printf("Introduzca los grupos: "); scanf("%d", &k); switch (metodo()) { case 1: r = combSR_F(n, k); break; case 2: break; case 3: r = combSR_I(n, k); break; } printf("El resultado para la entrada Csr(%d ^ %d) es %d \n",n, k, r); return 0; }

  • Anlisis y Diseo de Datos y Algoritmos

    35

    1.10.9 Divisin por sucesin de restas.

    Obtener la divisin aplicando la operacin resta de manera recursiva. As, si se desea realizar A / B y obtener el cociente C y el resto R, el proceso consistir en restar a A la cantidad B hasta que el resultado de la resta sea menor que el propio B. El nmero de operaciones de restas realizadas es C y el resultado de la ltima resta es A.

    Nota: La funcin recursiva necesita conocer ms de un parmetro: necesita saber cul es el A actual, cul es el B actual y cul es el nmero de restas que se han realizado, C.

    SOLUCIN:

    Algoritmo No Final:

    (, , ) = { = ; 0 > 1 + (( ), , ) . . .

    Algoritmo Final:

    (, , , ) = { = ; > (( ), , , ( + 1)) . . .

    Algoritmo Iterativo:

    funcin entero division (entero a, entero b, entero s) entero res entero acu = 0 mientras (a > b) hacer a = (a b) acu = acu + 1 fin-mientras s = a res = acu retornar res fin-funcin

    Miguel Angel [email protected]

  • 36

    #include int division_N(int a, int b, int * s) { int res; if (b > a) { *s = a; res = 0; } else { res = 1 + division_N((a - b), b, s); } return res; } int division_F(int a, int b, int * s, int acu) { int res = 0; if (b > a) { *s = a; res = acu; } else { res = division_F((a - b), b, s, (acu + 1)); } return res; } int division_I(int a, int b, int * s) { int res; int acu = 0; while (a > b) { a = (a - b); acu++; } *s = a; res = acu; return res; } int metodo() { int opc; printf("\nTIPO DE ALGORITMO"); printf("\n 1 - Recursividad NO FINAL"); printf("\n 2 - Recursividad FINAL"); printf("\n 3 - Iterativo"); printf("\nOpc: "); scanf("%d", &opc); printf("\n"); return opc; } int main() { int a, b; int s = 0; int r; printf("\nDIVISION ENTERA POR RESTAS SUCESIVAS.\n\n"); printf("Introduzca el numerador: "); scanf("%d", &a); printf("Introduzca el denominador: "); scanf("%d", &b); switch (metodo()) { case 1: r = division_N(a, b, &s); break; case 2: r = division_F(a, b, &s, 0); break; case 3: r = division_I(a, b, &s); break; } printf("%d / %d = %d (resto = %d) \n", a, b, r, s); return 0; }

  • Anlisis y Diseo de Datos y Algoritmos

    37

    1.10.10 VECTOR. Sumar los elementos.

    Calcular la suma de todos los elementos de un vector conocido, as como tambin su tamao.

    SOLUCIN:

    Algoritmo No Final:

    () = { 0 < 0[] + ( 1) 0

    Algoritmo Final:

    (, ) = { < 0( 1, [] + ) 0

    Algoritmo Iterativo:

    funcin entero suma (entero n) entero acu = 0 mientras (n >= 0) hacer acu = acu + vector[n] n = n - 1 fin-mientras retornar acu fin-funcin

    Miguel Angel [email protected]

  • 38

    #include int vector[10] = { 2, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; const int TAM = 10; int suma_N(int n) { int res; if (n < 0) { res = 0; } else { res = vector[n] + suma_N(n - 1); } return res; } int suma_F(int n, int acu) { int res = 0; if (n < 0) { res = acu; } else { res = res + suma_F(n - 1, vector[n] + acu); } return res; } int suma_I(int n) { int res = 0; while (n >= 0) { res = res + vector[n]; n--; } return res; } int metodo() { int opc; printf("\nTIPO DE ALGORITMO"); printf("\n 1 - Recursividad NO FINAL"); printf("\n 2 - Recursividad FINAL"); printf("\n 3 - Iterativo"); printf("\nOpc: "); scanf("%d", &opc); printf("\n"); return opc; } int main() { int i; int r; printf("\nSUMA DE UN VECTOR DE 10 ELEMENTOS\n\n"); switch (metodo()) { case 1: r = suma_N(TAM - 1); break; case 2: r = suma_F(TAM - 1, 0); break; case 3: r = suma_I(TAM - 1); break; } printf("La suma del vector "); for (i=0; i

  • Anlisis y Diseo de Datos y Algoritmos

    39

    1.10.11 VECTOR. Comprobar si todos son pares.

    Determinar si todos los elementos de un vector, de tamao conocido, son nmeros pares.

    SOLUCIN:

    Algoritmo No Final:

    La definicin recursiva indicada no existe como No Final.

    Algoritmo Final:

    (, ) = { [0]%2 = 01 > 0 []%2 = 1(, 1) > 0 []%2 1

    Algoritmo Iterativo:

    funcin entero paridad (entero[] vector, entero tam) entero res = 0 // 0 = cierto, 1 = falso mientras (tam >= 0 y res = 0) hacer res = vector[tam-1] % 2 tam = tam - 1 fin-mientras retornar res fin-funcin

    Miguel Angel [email protected]

  • 40

    #include int vector[5] = { 2, 4, 6, 8, 1 }; const int TAM = 5; int paridad_N(int * dt, int n) { int res = 0; // true if (n == 0) { res = dt[0] % 2; } else { if (dt[n] % 2 == 1) { res = 1; // false } else { res = paridad_N(dt, n - 1); } } return res; } int paridad_I(int * dt, int n) { int res = 0; // true while (n >= 0 && res == 0) { res = dt[n - 1] % 2; n--; } return res; } int metodo() { int opc; printf("\nTIPO DE ALGORITMO"); printf("\n 1 - Recursividad NO FINAL (NO SOPORTADO)"); printf("\n 2 - Recursividad FINAL"); printf("\n 3 - Iterativo"); printf("\nOpc: "); scanf("%d", &opc); printf("\n"); return opc; } int main() { int i; int r; printf("\nPARIDAD DE UN VECTOR DE 5 ELEMENTOS\n\n"); switch (metodo()) { case 1: break; case 2: r = paridad_N(vector, TAM); break; case 3: r = paridad_I(vector, TAM); break; } printf("Son pares todos estos elementos: "); for (i=0; i

  • Anlisis y Diseo de Datos y Algoritmos

    41

    1.10.12 VECTOR. Determinar el valor ms grande.

    Determinar cul es el valor ms grande entre los elementos de un vector.

    SOLUCIN:

    Algoritmo No Final:

    (, ) = { [] = 0

    max ([ 1],(, 1)) > 0

    max(, ) = { > b . . .

    Algoritmo Final:

    (, , ) = { = 0(, 1,max(, [ 1]) ) > 0

    Algoritmo Iterativo:

    funcin entero mayorValor (entero[] vector, entero tam) entero res = vector[tam-1] mientras (tam >= 0) hacer res = max (res, vector[tam-1]) tam = tam - 1 fin-mientras retornar res fin-funcin

    Miguel Angel [email protected]

  • 42

    #include int vector[5] = { 2, 4, 36, 1, -8 }; const int TAM = 5; int max(int a, int b) { if (a < b) return b; else return a; } int mayorValor_N(int * dt, int n) { int res = 0; if (n == 0) { res = dt[n]; } else { res = max(dt[n], mayorValor_N(dt, n - 1)); } return res; } int mayorValor_F(int * dt, int n, int elto) { int res = 0; if (n == 0) { res = elto; } else { res = mayorValor_F(dt, n - 1, max(dt[n - 1], elto)); } return res; } int mayorValor_I(int * dt, int n) { int res = dt[n - 1]; while (n >= 0) { res = max(res, dt[n]); n--; } return res; } int metodo() { int opc; printf("\nTIPO DE ALGORITMO"); printf("\n 1 - Recursividad NO FINAL"); printf("\n 2 - Recursividad FINAL"); printf("\n 3 - Iterativo"); printf("\nOpc: "); scanf("%d", &opc); printf("\n"); return opc; } int main() { int i; int r; printf("\nOBTENER EL VALOR MAS GRANDE DE UN VECTOR\n\n"); switch (metodo()) { case 1: r = mayorValor_N(vector, TAM); break; case 2: r = mayorValor_F(vector, TAM, vector[0]); break; case 3: r = mayorValor_I(vector, TAM); break; } printf("Vector: "); for (i=0; i

  • Anlisis y Diseo de Datos y Algoritmos

    43

    1.10.13 VECTOR. Determinar la posicin donde se encuentra el valor ms pequeo.

    Determinar en qu posicin se encuentra el valor ms pequeo de un vector.

    SOLUCIN:

    Algoritmo No Final:

    (, ) = { 0 = 0min(, , (, 1)) > 0

    min(, , ) = { [] < []b . . .

    Algoritmo Final:

    (, , ) = { = 0(, 1,min(, , 1)) > 0

    Algoritmo Iterativo:

    funcin entero posMenor (entero[] vector, entero tam) entero res = 0 mientras (tam > 0) hacer res = min (vector, res, tam-1) tam = tam - 1 fin-mientras retornar res fin-funcin

    funcin entero min (entero[] vector, entero a, entero b) entero res si (vector[a] < vector[b]) entonces res = a sino res = b fin-si retornar res fin-funcin

    Miguel Angel [email protected]

  • 44

    #include int vector[5] = { 3, 8, 9, 1, 2 }; const int TAM = 5; int min(int * dt, int a, int b) { if (dt[a] < dt[b]) return a; else return b; } int posMenor_N(int * dt, int n) { int res = 0; if (n == 0) { res = 0; } else { res = min(dt, n, posMenor_N(dt, n - 1)); } return res; } int posMenor_F(int * dt, int n, int elto) { int res = 0; if (n == 0) { res = elto; } else { res = posMenor_F(dt, n - 1, min(dt, n, elto)); } return res; } int posMenor_I(int * dt, int n) { int res = 0; while (n > 0) { res = min(dt, res, n - 1); n--; } return res; } int metodo() { int opc; printf("\nTIPO DE ALGORITMO"); printf("\n 1 - Recursividad NO FINAL"); printf("\n 2 - Recursividad FINAL"); printf("\n 3 - Iterativo"); printf("\nOpc: "); scanf("%d", &opc); printf("\n"); return opc; } int main() { int i; int r; printf("\nOBTENER LA POSICION DEL MENOR VALOR DE UN VECTOR\n\n"); switch (metodo()) { case 1: r = posMenor_N(vector, TAM); break; case 2: r = posMenor_F(vector, TAM, vector[0]); break; case 3: r = posMenor_I(vector, TAM); break; } printf("\nVector: "); for (i=0; i

  • Anlisis y Diseo de Datos y Algoritmos

    45

    1.10.14 VECTOR. Producto Escalar.

    Calcular el producto escalar de dos vectores, de iguales tamaos conocidos, declarados globlamente.

    SOLUCIN:

    Algoritmo No Final:

    () = { 0 < 0(1[] 2[]) + [ 1] 0

    Algoritmo Final:

    (, ) = { < 0

    ( 1, (1[] 2[]) + ) 0

    Algoritmo Iterativo:

    funcin entero prodEsc (entero pos) entero acu = 0 mientras (pos >= 0) hacer acu = acu + (v1[n] * v2[n]) pos = pos - 1 fin-mientras retornar acu fin-funcin

    Miguel Angel [email protected]

  • 46

    #include int v1[3] = { 2, 5, 3 }; int v2[3] = { 3, 3, 4 }; const int TAM = 3; int prodEsc_N(int n) { int res = 0; if (n < 0) { res = 0; } else { res = (v1[n] * v2[n]) + prodEsc_N(n - 1); } return res; } int prodEsc_F(int n, int acu) { int res = 0; if (n < 0) { res = acu; } else { res = prodEsc_F(n - 1, (v1[n] * v2[n]) + acu); } return res; } int prodEsc_I(int n) { int res = 0; while (n >= 0) { res = res + v1[n] * v2[n]; n--; } return res; } int metodo() { int opc; printf("\nTIPO DE ALGORITMO"); printf("\n 1 - Recursividad NO FINAL"); printf("\n 2 - Recursividad FINAL"); printf("\n 3 - Iterativo"); printf("\nOpc: "); scanf("%d", &opc); printf("\n"); return opc; } int main() { int i; int r; printf("\nCALCULAR EL PRODUCTO ESCALAR DE 2 VECTORES\n\n"); switch (metodo()) { case 1: r = prodEsc_N(TAM-1); break; case 2: r = prodEsc_F(TAM-1, 0); break; case 3: r = prodEsc_I(TAM-1); break; } printf("\nVector v1: "); for (i=0; i

  • Anlisis y Diseo de Datos y Algoritmos

    47

    1.10.15 Resolver una funcin (1).

    Implemente recursivamente e iterativamente la siguiente funcin:

    () = { < 42 ( 1) + ( 2) 2 ( 4) . . .

    SOLUCIN:

    Algoritmo No Final:

    La definicin recursiva indicada ya es Final.

    Algoritmo Final:

    La definicin recursiva indicada no existe como Final.

    Algoritmo Iterativo:

    funcin entero func (entero n) entero res entero n0, n1, n2, n3 entero i si (n < 4) entonces res = n sino n3 = 3 n2 = 2 n1 = 1 n0 = 0 i = 4 mientras (i

  • 48

    #include int func_N(int n) { int res; if (n < 4) { res = n; } else { res = (2 * func_N(n - 1)) + func_N(n - 2) - (2 * func_N(n - 4)); } return res; } int func_I(int n) { int res = 0; int n0, n1, n2, n3; int i; if (n < 4) { res = n; } else { n3 = 3; n2 = 2; n1 = 1; n0 = 0; i = 4; while (i

  • Anlisis y Diseo de Datos y Algoritmos

    49

    1.10.16 Resolver una funcin (2).

    Implemente recursivamente e iterativamente la siguiente funcin.

    () = {

    2 = 01 = 11 = 22 ( 1) + 3 ( 2) ( 3) 3

    SOLUCIN:

    Algoritmo No Final:

    La definicin recursiva indicada ya es Final.

    Algoritmo Final:

    La definicin recursiva indicada no existe como Final.

    Algoritmo Iterativo:

    funcin entero func_I (entero n) entero res entero n0, n1, n2 entero i n2 = 1 n1 = 1 n0 = 2 si (n < 3) entonces segn (n) { caso 0 entonces res = n0 caso 1 entonces res = n1 caso 2 entonces res = n2 fin-segn sino i = 3 mientras (i

  • 50

    #include int func_N(int n) { int res; if (n == 0) { res = 2; } if (n == 1) { res = 1; } if (n == 2) { res = 1; } if (n >= 3) { res = 2 * func_N(n - 1) + 3 * func_N(n - 2) - func_N(n - 3); } return res; } int func_I(int n) { int res; int n0, n1, n2; int i; n0 = 2; n1 = 1; n2 = 1; if (n < 3) { if (n == 0) { res = n0; } if (n == 1) { res = n1; } if (n == 2) { res = n2; } } else { i = 3; while (i

  • Anlisis y Diseo de Datos y Algoritmos

    51

    1.10.17 CADENA. Obtener una cadena de caracteres a partir de un nmero.

    Dado un nmero n, de tipo entero positivo, se desea conviertir dicho nmero en una cadena de caracteres.

    SOLUCIN:

    Algoritmo No Final:

    La definicin recursiva indicada no existe como No Final.

    2() = { "" = 02(\10) + (%10) 0

    () = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}[]

    Algoritmo Final:

    2(, ) = { = 02(\10, + (%10)) 0

    Algoritmo Iterativo:

    funcin cadena int2txt (entero n) cadena txt = mientras (n != 0) hacer txt = (n%10) & txt n = n \ 10 fin-mientras retornar txt fin-funcin

    Miguel Angel [email protected]

  • 52

    Implementcin en Java:

    public class Int2txt { public static void main(String[] args) { int n = 2468; System.out.println("El nmero " + n + " es como cadena >" + int2txt_N(n, "") + "%s%s

  • Anlisis y Diseo de Datos y Algoritmos

    53

    1.10.18 CADENA. Invertir una cadena.

    Dada una cadena de caracteres cad, de tamao tam, obtener su cadena inversa.

    SOLUCIN:

    Algoritmo No Final:

    (, , ) = { [] = 0

    [] & (, , 1) > 0

    public class Ejercicio { public static void main(String[] args) { String texto = "Hola, estamos probando la inversin de una cadena."; System.out.println("Cadena normal : " + texto); System.out.println("Cadena invertida: " + inverso(texto, texto.length(), texto.length()-1)); } public static String inverso(String cad, int tam, int pos) { String res = ""; if (pos == 0) { res = "" + cad.charAt(pos); } else { res = "" + cad.charAt(pos) + inverso(cad, tam, pos - 1); } return res; } }

    Variante:

    (, , ) = { [], [] [], [] <

    Algoritmo Final:

    (, , , ) = {

    = 1[ 1] = [](, , 1, )

    } . . .

    #include char texto[] = "Hola, estamos probando la inversion de una cadena."; char acu[] = ""; const int TAM = 50; char * inverso(char * cad, int tam, int pos, char * acu) { if (pos == -1) { return acu; } else { acu[tam - pos - 1] = cad[pos]; return inverso(cad, tam, pos - 1, acu); } } int main() { printf("\nINVERTIR UNA CADENA\n\n"); printf("Cadena normal : %s\n", texto); printf("Cadena invertida: %s\n", inverso(texto, TAM, TAM-1, acu)); return 0; }

    Miguel Angel [email protected]

  • 54

    1.10.19 FIGURA. Contabilizar tringulos.

    Siguiendo el esquema de la figura, determine cuntos tringulos hay hasta alcanzar el nivel n.

    SOLUCIN:

    () = { 1 = 12 + ( 1) > 1

    () = { 1 = 1() + ( 1) > 1

    Reduzcamos la complejidad de la solucin planteada, que sale cuadrtica, redefiniendo la funcin tringulos(n) de la siguiente forma:

    () = 2 1

    luego tenemos:

    () = { 1 = 1(2 1) + ( 1) > 1

    de esta forma hemos pasado de:

    public int triangulos(int n) { return n == 1 ? 1 : (2 + triangulos(n - 1)); } public int sumador(int n) { return n == 1 ? 1 : (triangulos(n) + sumador(n - 1)); }

    a tener solamente:

    public int sumador(int n) { return n == 1 ? 1 : ((2 * n - 1) + sumador(n - 1)); }

    Algoritmo Final:

    (, ) = { = 1( 1, (2 1) + ) > 1

    public int sumador(int n, int acu) { return n == 1 ? 1 : sumador(n - 1, (2 * n - 1) + acu); }

    Algoritmo Iterativo:

    public int sumador(int n) { int i = 1; int acu = 0; while (i

  • Anlisis y Diseo de Datos y Algoritmos

    55

    2 Anlisis de la eficiencia de algoritmos.

    2.1 Introduccin.

    Cuando diseamos algoritmos es necesario demostrar en primer lugar que acaban y que hacen el cometido especificado, adems que resulte fcil de entender, codificar, depurar, verificar y de mantener, al mismo tiempo que usan eficientemente los recursos del ordenador y se ejecutan en el menor tiempo posible. Pero en segundo lugar es conveniente estimar el tiempo que tardarn en ejecutarse en funcin del tamao del problema a resolver. Deberemos por tanto analizar la complejidad de los algoritmos y estimar el tiempo de ejecucin.

    2.2 rdenes de complejidad.

    En general estamos interesados en el tiempo de ejecucin como una funcin del tamao del problema cuando el tamao es grande. Representaremos el tamao de un problema por n. En general n ser una funcin de los valores de las propiedades x del problema. Es decir n = f(x). Representaremos por T(n) la funcin que nos da el tiempo de ejecucin en funcin del tamao. En los estudios de complejidad de algoritmos asumimos que todas las funciones T(n) son montonas crecientes y normalmente slo estaremos interesados en los aspectos cualitativos de T(n), es decir en su comportamiento para valores grandes de n. Para ello clasificamos las funciones segn su comportamiento para grandes valores de n. Esta clasificacin agrupar las funciones T(n) en rdenes de complejidad. Cada orden de complejidad es un conjunto de funciones con comportamiento equivalente para grandes valores de n.

    Para concretar lo anterior introduciremos una relacin de orden total entre las funciones. Este orden define implcitamente una relacin de equivalencia y unas operaciones de mnimo y mximo asociadas. Representaremos este orden por el smbolo

  • 56

    El orden anterior

  • Anlisis y Diseo de Datos y Algoritmos

    57

    2.2.3 Otros rdenes de complejidad.

    Al comparar dos algoritmos de distinto orden hay que tener muy en cuenta las constantes multiplicativas. Junto con el orden de complejidad exacto, (g(n)), se usan otras notaciones O(g(n)) (cota superior), (g(n)) (cota inferior). Todos definen conjuntos de funciones:

    : () (()) lim

    ()

    ()<

    : () (()) lim

    ()

    ()> 0

    Para indicar que una funcin f(n) es de un orden de complejidad dado g(n) se indica indistintamente como O(f(n))=g(n) o bien f(n) O(g(n)). Igualmente con las otras notaciones , o bien .

    Algunas propiedades:

    ( () + ) = (())

    ( () + ) = (())

    ( () + ) = (())

    (()) = (()) (())

    (()) (())

    (()) (())

    () = (() + () (() + ())) = ()

    (() ()) = (()) (())

    (()) > 1 (() ()) > ()

    (()) = 1 (() ()) = ()

    (()) 1 (()

    ()) >

    (())

    (())

    (()) < 1 (()

    ()) < ()

    (()) = 1 (()

    ()) = ()

    2.3 Recurrencias Lineales.

    Para calcular la complejidad de algoritmos recursivos es necesario plantear ecuaciones de recurrencia que relacionan la complejidad del problema con la de los sub-problemas y la de la obtencin de los sub-problemas y la combinacin de las soluciones. Usualmente aparecen recurrencias lineales de la forma:

    0() + 1( 1) ++ ( ) = 11() + 2

    2() + + ()

    Donde ai y bi los son nmeros reales, con los bi todos distintos, y pi(n) polinomios de orden di en n. La ecuacin se denomina homognea si los bi , di son todos iguales a cero.

    La solucin de la ecuacin es:

    () =

    1

    =0

    =1

    Donde los cij se calcularan a partir de las condiciones iniciales, ri son las l races distintas, cada una de multiplicidad mi, de la denominada ecuacin caracterstica generalizada:

    (0 + 1

    1 ++ ) ( 1)1+1 ( 2)

    2+1 ( )+1 = 0

    Miguel Angel [email protected]

  • 58

    En el clculo de la complejidad slo nos interesa el orden de complejidad de la expresin anterior:

    (()) = (

    1

    =0

    =1

    ) = (1 )

    donde rh es la solucin de la ecuacin caracterstica generalizada con mayor valor.

    Ejemplo:

    El problema de Fibonacci es, como hemos visto en el tema anterior, f(n) = f(n-1) + f(n-2).

    El tiempo de ejecucin, T(n), verifica la ecuacin de recurrencia: T(n) = T(n-1) + T(n-2) + k.

    La ecuacin caracterstica ser: (x2 x 1) (x 1) = 0

    Sus races son: 1 =1+5

    2= 1618 y 2 =

    15

    2= 0618

    Y como |1| > 1, |2| < 1

    Entonces: (()) = (1) = ((

    1+5

    2)

    )

    Casos particulares:

    1er caso:

    Un caso particular de este tipo de ecuaciones que tiene mucha utilidad es:

    () = ( ) + ()

    Siendo g(n) un polinomio de grado d. La ecuacin caracterstica generalizada es:

    ( ) ( )+1 = 0

    Cuyas races son c, a1/b . El resto de las races de (xb - a) = 0 son complejas.

    () () { > 1} () () { = 1}

    () = {

    ( ) >

    (+1 ) =

    ( ) < () = {

    ( ) >

    (+1) =

    () <

    2 caso:

    Una variante del caso anterior es la recurrencia:

    () = (

    ) + ()

    Siendo como antes g(n) un polinomio de grado d. Observando que (log n)p (log(n-b))p podemos ver que la solucin tendr la forma h(n)(log n)p . Dnde h(n) verifica la ecuacin previa.

    () ( ) { > 0} () () { = 0}

    () {

    () >

    ( +1) =

    ( ) < () {

    () >

    ( ()) =

    () <

  • Anlisis y Diseo de Datos y Algoritmos

    59

    2.4 Recurrencias con Sumatorios.

    Nos podemos encontrar con la necesidad de calcular sumatorios de expresiones cuyas variables siguen progresiones aritmticas o geomtricas. Estando interesados en el orden de complejidad de los sumatorios cuando el lmite superior tiende a infinito.

    Los sumatorios ms usuales son del tipo:

    ()

    =

    = () + ( + 1) + + ()

    De una forma ms general consideraremos los sumatorios donde el ndice toma valores ms generales que en el caso anterior. Esto lo indicamos de la forma:

    ()

    =,=()

    = ((0)) + ((1)) + + (())

    En esta notacin el ndice, ahora x, toma los valores proporcionados por la funcin h(i) cuando i toma los valores 0, 1, 2, . Asumimos que h(i) es una funcin montona creciente que cumple adems h(0) = a, h(in) = n. Siendo in el valor que debe tomar i para que se cumpla la condicin anterior. Ejemplos concretos son cuando ha(i) = a + ri, hg(i) = a + ri . En el primer caso x toma los valores de una progresin aritmtica con diferencia r. En el segundo, los valores de una progresin geomtrica de razn r.

    Alternativamente podemos considerar la funcin g(i) montona decreciente que cumple g(0) = n, g(in) = a. Ejemplos concretos son cuando ga(i) = n - ri, gg(i) = nr - i . En el primer caso x toma los valores de una progresin aritmtica con diferencia -r. En el segundo, los valores de una progresin geomtrica de razn 1/r.

    ()

    ==()

    =

    = ((0)) + ((1)) + + (())

    Por tanto, los dos sumatorios siguientes son iguales por ser equivalentes las correspondientes funciones que generan los ndices:

    ()

    =

    =,=()

    = ()

    ==()

    =

    ()

    =

    =,=()

    = ()

    ==()

    =

    Todo lo anterior podemos resumirlo al considerar que un sumatorio cuyos ndices toman el valor de una secuencia aritmtica creciente tiene el mismo valor que otro cuyos valores siguen una secuencia aritmtica decreciente con diferencia cambiada de signo. Igualmente ocurre en el caso de la secuencia geomtrica. Esta idea puede generalizarse a otros patrones de generacin de ndices por lo que, de ahora en adelante, consideraremos principalmente patrones montonos crecientes en los sumatorios.

    Como estamos interesados en el orden de complejidad, y con los elementos anteriores podemos ver las relaciones que existen entre los sumatorios y las ecuaciones de recurrencia, podemos realizar por tanto diversas simplificaciones, obteniendo:

    () = ( 1) + () () =

    ==+

    () ()

    =

    =

    () = (

    ) + () () =

    ==

    () ()

    =

    =

    Miguel Angel [email protected]

  • 60

    De la misma forma, por la definicin de integral, tenemos las siguientes relaciones:

    ()

    =

    =,=()

    (())

    1

    =0

    (())1()

    0

    = (())()

    ()

    1()

    0

    = ()

    (1())

    En general, por quedar claro el contexto, simplificamos la notacin:

    Para progresiones aritmticas donde h(x) = a + rx, h(x) = r tenemos:

    ()

    =+

    ()

    (1())

    = 1

    ()

    Para progresiones aritmticas donde h(x) = arx, h(x) = aln rrx, h(h-1(x)) = xln r tenemos:

    ()

    =

    ()

    (1())

    = 1

    ln ()

    Casos particulares de sumatorios son:

    0, 0 ( (ln )) +1 (ln )

    =+

    0, 0, > 1 ( (ln )) { (ln )+1 = 0

    (ln )+1 > 0

    =

    Ejemplo 1:

    2

    =+2

    1

    22

    1

    2 2

    1

    6(3 3)

    1

    6(3) (3)

    Podemos conseguir el resultado anterior teniendo en cuenta que la solucin de la recurrencia siguiente tiene el mismo orden de complejidad:

    () = ( 2) + 2

    Ejemplo 2:

    1

    =3

    1

    ln 3

    1

    1

    ln 31

    1

    ln 3[ln ]

    1

    ln 3[ln ln ] (log )

    Podemos conseguir el resultado anterior teniendo en cuenta que la solucin de la recurrencia siguiente

    tiene el mismo orden de complejidad:

    () = (

    3) + 1

  • Anlisis y Diseo de Datos y Algoritmos

    61

    2.5 Complejidad de los algoritmos.

    2.5.1 Tamao de problemas y casos de ejecucin.

    En lo que sigue representaremos los problemas por p, p1, p2, , pr . Los problemas se agruparn en conjuntos de problemas. Un conjunto de problemas lo representaremos por P. Cada problema tendr unas propiedades x. Cada propiedad especfica la representaremos mediante un superndice: x = x1, , xk. Dentro de un conjunto de problemas P los valores de sus propiedades identifican al problema de manera nica.

    Asociado a un problema podemos asociar el concepto de tamao, como una nueva propiedad derivada del problema. El tamao de un problema es una medida de la cantidad de informacin necesaria para representarlo. Normalmente representaremos el tamao de un problema mediante n y lo calcularemos, mediante una funcin n = t(x) o bien n = t(p). Como hemos dicho, un problema dentro de un conjunto de problemas, puede representarse por un conjunto de propiedades x. Entonces el tamao es una nueva propiedad del problema que se calcula a partir de esas propiedades n = t(x). Por tanto, cada problema dentro de un conjunto de problemas, tendr un tamao.

    En los algoritmos recursivos podemos entender que cada llamada recursiva resuelve un problema distinto y el tamao de cada uno de los sub-problemas debe ser menor que el tamao del problema original. Por analoga, los algoritmos iterativos van transformando un problema en otro, tambin de tamao ms pequeo, hasta que se encuentra la solucin (caso base).

    Dado un conjunto de problemas y un algoritmo para resolverlos, el tiempo que tardar el algoritmo para resolver un problema dado P, depender del tamao del mismo. El tiempo que tarda el algoritmo en funcin del tamao del problema que resuelve lo representaremos por la funcin: T(n).

    Varias instancias de un mismo problema con el mismo tamao pueden tardar tiempos diferentes. Dentro de los problemas con un mismo tamao llamaremos caso peor a aquel problema que tarde ms tiempo en resolverse y lo representaremos por pp , y por Tp(n) el tiempo que tarda en funcin del tamao. Igualmente el problema que tarde menos tiempo en resolverse, de entre los que tienen el mismo tamao, lo llamaremos caso mejor y lo representaremos por pm , y por Tm(n) el tiempo que tarda en funcin del tamao. No debe confundirse caso mejor con que su tamao sea pequeo, ya que se trata de conceptos diferentes. Por ltimo, si los problemas con tamao n tienen una distribucin de probabilidad f(p), entonces llamaremos caso medio y lo representaremos por Tmd(n) a la media de los tiempos que tarda cada uno de esos problemas, es decir,

    () = ()()

    | ()=

    En cada uno de los casos anteriores hablaremos del clculo del caso peor, mejor o medio para estimar el tiempo de ejecucin de un algoritmo dado.

    Miguel Angel [email protected]

  • 62

    2.5.2 Complejidad de los algoritmos iterativos.

    Un algoritmo iterativo se compone de secuencia de bloques. Cada bloque es un bloque bsico, un boque if o un bloque while. Un bloque bsico es una secuencia de instrucciones sin ningn salto de control (es decir sin if, ni while).

    Veamos la forma de determinar los tiempos Tp, Tm, Tmd para los bloques bsicos, la estructura if y la estructura while. Hablaremos en general de T para referirnos indistintamente al caso peor, mejor o medio y usaremos superndices cuando queramos hacerlo explcito.

    Para un bloque bsico la forma de estimar el tiempo de ejecucin es sumando el tiempo que tarda cada una de las instrucciones elementales que lo componen. Si hacemos la hiptesis simplificadora que cada instruccin elemental tarda el mismo tiempo entonces el tiempo que tarda un bloque bsico en ejecutarse es proporcional al nmero de operaciones elementales. Es decir, para un bloque bsico:

    Tp(n) = Tm(n) = Tmd(n) = c k

    Donde k es el nmero de operaciones elementales del bloque bsico y c el tiempo de ejecucin de una sentencia elemental. Por tanto, el tiempo de ejecucin de un bloque bsico no depende del tamao del problema.

    Secuencia de bloques bsicos

    S1;

    S2;

    Sk;

    () = 1() + 2() + + ()

    Estructura if

    if(g)

    {

    S1;

    }

    else

    {

    S2;

    }

    =

    +(1 , 2

    )

    =

    +(1, 2

    )

    =

    + 11 + 22

    Siendo fi (i = 1, 2) la frecuencia de ejecucin del bloque i.

    Donde g es la expresin lgica de la guarda.

    Donde s1 y s2 son dos secuencias de bloques bsicos.

    Estructura while

    while(g)

    {

    S;

    }

    () = +( + ())

    Siendo I el conjunto de valores que va tomando el tamao en las sucesivas iteraciones.

    Donde g es la expresin lgica de la guarda.

  • Anlisis y Diseo de Datos y Algoritmos

    63

    2.5.3 Mtodo de la instruccin crtica para algoritmos iterativos.

    Este mtodo simplifica en algunas ocasiones el clculo de la complejidad de los algoritmos iterativos. Para aplicarlo se trata de buscar la instruccin crtica, que es aquella que se ejecuta el mximo nmero de veces.

    Este mtodo dice:

    La complejidad de un algoritmo iterativo es igual a la complejidad del nmero de veces que se ejecute la instruccin crtica.

    (T(n)) = (N(n))

    donde N(n) es el nmero de veces que se ejecuta la instruccin crtica en funcin del tamao del problema n.

    Este enunciado es fcil de comprobar para algoritmos de la forma:

    r;

    while(g){

    s;

    }

    (()) = ( + +( + )

    ) = (||) = (())

    El razonamiento se puede generalizar al caso de los bucles anidados y de los bucles consecutivos. Este nmero es fcil de obtener en los siguientes casos:

    Algoritmos iterativos sin bucles anidados aunque puedan tener varios estructuras while consecutivos: la instruccin crtica es una del cuerpo de uno de los bucles. Justamente el que tenga ms iteraciones. El nmero de iteraciones de un bucle se obtiene a partir del cardinal del conjunto de valores que toma el ndice.

    Varios bucles anidados: la instruccin crtica es una del cuerpo del bucle interior y entonces tenemos:

    () =

    1 1

    2 2

    1

    Algoritmos iterativos con varios bucles while anidados pero donde las variables de ndice de cada bucle no dependen en sus lmites unas de otras: slo depende del tamao del problema n, entonces:

    () = 1()2() ()

    Donde Ni(n) es el nmero de veces que se ejecuta el bucle anidado i.

    En el caso general para calcular N(n) para una instruccin concreta aparecen sumatorios cuyo orden de complejidad habr que calcular con la tcnicas citadas antes.

    En efecto la instruccin crtica es la guarda.

    El cardinal del conjunto es el nmero de veces que

    se ejecuta el cuerpo: la guarda.

    Miguel Angel [email protected]

  • 64

    2.5.4 Complejidad de los algoritmos recursivos sin memoria.

    Los problemas que se resuelven con tcnicas recursivas sin uso de memoria dan lugar a un tipo de recurrencias estudiadas anteriormente. Como ya hemos visto, un problema que se resuelve un algoritmo recursivo del tipo Divide y Vencers, adopta la forma:

    () = { 0

    (, (1), (2), , ())

    Donde x representa el problema de tamao n, xi los subproblemas de tamao ni con ni < n, donde c representa la funcin que combina las soluciones de los problemas y la solucin del caso base de los que puede haber ms de uno. El tiempo de ejecucin, T(n), para este tipo de problemas verifica la recurrencia:

    () = () + () + (1) + (2) + + ()

    Donde Tp, Tc son, respectivamente, los tiempos para calcular los sub-problemas y para combinar las soluciones. Suponiendo que un problema de tamao n se divide en a sub-problemas, todos del mismo tamao, n/b, y que el coste de dividir el problema en sub-problemas ms el coste de combinar las soluciones es g(n), entonces la ecuacin de recurrencia ser para los casos recursivos (n > n0):

    () = (

    ) + (), > 0

    Si el tamao de los sub-problemas es diferente, aparecen recurrencias del tipo

    () = () +

    =1

    (

    ) , > 0

    Que se resuelven por la generalizacin del llamado Master Theorem.

    Cuando el tamao de los sub-problemas se escoge aleatoriamente y queremos calcular casos medios, aparecen otras recurrencias ms complejas, como:

    () = 1 +1

    (() + ( 1))

    1

    =0

    Estas hay que resolverlas numricamente.

  • Anlisis y Diseo de Datos y Algoritmos

    65

    2.5.5 Complejidad de los algoritmos recursivos con memoria.

    Si se usa memoria, el clculo del tiempo es diferente al caso de no usar memoria. Sea, como antes, n el tamao de x, y sea g(n) el tiempo necesario para calcular los sub-problemas ms el tiempo para combinar las soluciones en un problema de tamao n, cada problema dentro de un conjunto de problemas, puede ser identificado de forma nica, por sus propiedades individuales. Sean los problemas x, x1, x2, , xk y sea ni = t(xi) el tamao del problema i. Para resolver un problema dado debemos resolver un subconjunto de problemas. Sea Ix el conjunto formado por los problemas que hay que resolver para calcular la solucin de x, entonces el tiempo de ejecucin verifica:

    () = (())

    Esto es debido a que, supuesto calculados los sub-problemas, el tiempo para calcular un problema de ndice x es g(T(x)), es decir, el tiempo necesario para calcular los sub-problemas y componer la solucin. Luego el tiempo necesario para calcular un problema es la suma de lo que se requiere para cada uno de los sub-problemas, que es necesario resolver, que forman el problema.

    Un caso particular, muy usado, se presenta cuando g(n) = k . Entonces la frmula anterior queda:

    () = (()) =

    1 = |1| = (|1|)

    Es decir, en este caso muy general, el tiempo de ejecucin es proporcional al nmero de problemas que es necesario resolver.

    En general un problema puede ser representado por x = (x1, x2, , xk) por lo que para calcular |Ix| tenemos que calcular |(x1, x2, , xk)| donde las propiedades se mueven en el conjunto de valores que identifican los sub-problemas que hay que resolver para calcular la solucin de x.

    Un caso un poco ms general es cuando podemos calcular la cantidad de sub-problemas de un tamao dado n en funcin de n solamente. Sea esta funcin sp(n), entonces

    () = (() ())

    y si g(n) = k, entonces

    () = (() ()) =

    ()

    Donde ahora In es el conjunto de enteros que incluye los diferentes tamaos de los sub-problemas necesarios para resolver un problema de tamao n.

    Ejemplos:

    Problema de Fibonacci: cada problema se representa por su propiedad n, g(n) = k. Comprobamos que en este caso I = {0,1,2,n}. Luego la complejidad del problema de Fibonacci resuelto con Memoria es: (n).

    Problema de la Potencia Entera: cada problema se representa por su propiedad Exponente, n=Exponente, y g(n) = k. Luego la complejidad del problema de la Potencia Entera, de tamao n, resuelto sin Memoria,

    verifica que = {,

    2,

    4, ,1} , || = log2( + 1) , es: (log n).

    Estas ideas nos permiten decidir cundo usar Divide y Vencers con o sin Memoria. Slo usaremos la Memoria cuando la complejidad del problema se reduzca (caso del problema de Fibonacci). Si la complejidad es la misma (caso del problema de la Potencia Entera) no es conveniente usar Memoria. Las razones para que la complejidad sea distinta (usando memoria y sin usarla) es la aparicin de sub-problemas repetidos al resolver un problema. Esto ocurre en el problema de Fibonacci y no ocurre en el problema de la Potencia Entera. Por lo tanto, un criterio directo para decidir si usar memoria o no, es verificar si aparecen sub-problemas repetidos o no.

    Miguel Angel [email protected]