Vous êtes sur la page 1sur 86

42 - Estructuras dinámicas en C: Listas tipo Cola

Una lista se comporta como una cola si las inserciones las hacemos al final y las
extracciones las hacemos por el frente de la lista. También se las llama listas FIFO
(First In First Out - primero en entrar primero en salir)
Confeccionaremos un programa que permita administrar una lista tipo cola.
Desarrollaremos las funciones de insertar, extraer, vacia, imprimir y liberar.

Programa: programa184.c

#include<stdio.h>
#include<stdlib.h>

struct nodo {
int info;
struct nodo *sig;
};

struct nodo *raiz=NULL;


struct nodo *fondo=NULL;

void insertar(int x)
{
struct nodo *nuevo;
nuevo=malloc(sizeof(struct nodo));
nuevo->info=x;
nuevo->sig=NULL;
if (vacia())
{
raiz = nuevo;
fondo = nuevo;
}
else
{
fondo->sig = nuevo;
fondo = nuevo;
}
}

int extraer()
{
if (!vacia())
{
int informacion = raiz->info;
struct nodo *bor = raiz;
if (raiz == fondo)
{
raiz = NULL;
fondo = NULL;
}
else
{
raiz = raiz->sig;
}
free(bor);
return informacion;
}
else
return -1;
}

void imprimir()
{
struct nodo *reco = raiz;
printf("Listado de todos los elementos de la
cola.\n");
while (reco != NULL)
{
printf("%i - ", reco->info);
reco = reco->sig;
}
printf("\n");
}

void liberar()
{
struct nodo *reco = raiz;
struct nodo *bor;
while (reco != NULL)
{
bor = reco;
reco = reco->sig;
free(bor);
}
}

void main()
{
insertar(5);
insertar(10);
insertar(50);
imprimir();
printf("Extraemos uno de la cola: %i\n",
extraer());
imprimir();
liberar();
getch();
return 0;
}
La declaración del nodo es igual a la Pila. Luego definimos dos punteros externos
llamados raiz y fondo:
struct nodo {
int info;
struct nodo *sig;
};

struct nodo *raiz=NULL;


struct nodo *fondo=NULL;
raíz apunta al principio de la lista y fondo al final de la lista. Utilizar dos punteros
tiene como ventaja que cada vez que tengamos que insertar un nodo al final de la
lista no tengamos que recorrerla. Por supuesto que es perfectamente válido
implementar una cola con un único puntero externo a la lista.
El método vacía retorna 1 si la lista no tiene nodos y 0 en caso contrario:
int vacia()
{
if (raiz == NULL)
return 1;
else
return 0;
}
En la inserción luego de crear el nodo tenemos dos posibilidades: que la cola esté
vacía, en cuyo caso los dos punteros externos a la lista deben apuntar al nodo
creado, o que haya nodos en la lista.
void insertar(int x)
{
struct nodo *nuevo;
nuevo=malloc(sizeof(struct nodo));
nuevo->info=x;
nuevo->sig=NULL;
if (vacia())
{
raiz = nuevo;
fondo = nuevo;
}
else
{
fondo->sig = nuevo;
fondo = nuevo;
}
}
Recordemos que definimos un puntero llamado nuevo, luego creamos el nodo con
la función malloc y cargamos los dos atributos, el de información con lo que llega
en el parámetro y el puntero con NULL ya que se insertará al final de la lista, es
decir no hay otro después de este.
Si la lista está vacía:

En caso de no estar vacía:

Debemos enlazar el puntero sig del último nodo con el nodo recién creado:
fondo->sig=nuevo;
Y por último el puntero externo fondo debe apuntar al nodo apuntado por nuevo:
fondo=nuevo;

Con esto ya tenemos correctamente enlazados los nodos en la lista tipo cola.
Recordar que el puntero nuevo desaparece cuando se sale del método insertar,
pero el nodo creado no se pierde porque queda enlazado en la lista.
El funcionamiento del método extraer es similar al de la pila:
int extraer()
{
if (!vacia())
{
int informacion = raiz->info;
struct nodo *bor = raiz;
if (raiz == fondo)
{
raiz = NULL;
fondo = NULL;
}
else
{
raiz = raiz->sig;
}
free(bor);
return informacion;
}
else
return -1;
}
Si la lista no está vacía guardamos en una variable local la información del primer
nodo (el operador lógico ! invierte el valor devuelto por la función vacía, si retorna
un 1 luego el operador ! lo convierte a cero y viceversa y retorna un 1):
int informacion = raiz->info;
Definimos un puntero y lo inicializamos con el primero de la lista:
struct nodo *bor = raiz;
Para saber si hay un solo nodo verificamos si los dos punteros raiz y fondo
apuntan a la misma dirección de memoria:
if (raiz == fondo)
{

Luego hacemos:
raiz = NULL;
fondo = NULL;

En caso de haber 2 o más nodos debemos avanzar el puntero raiz al siguiente


nodo:

raiz = raiz->sig;

Ya tenemos la lista correctamente enlazada (raiz apunta al primer nodo y fondo


continúa apuntando al último nodo)
Finalmente eliminamos el nodo y retornamos la información:
free(bor);
return informacion;
Si la lista tipo cola está vacía retornamos un -1 (que representa que la cola está
vacía, no debemos insertar este valor -1 en la lista)
La función liberar es necesaria llamarla cuando no necesite nuestro programa
trabajar más con la lista y tiene por objetivo liberar la ram de todos los nodos
actuales que tiene la lista tipo cola:
void liberar()
{
struct nodo *reco = raiz;
struct nodo *bor;
while (reco != NULL)
{
bor = reco;
reco = reco->sig;
free(bor);
}
}
En la función main llamamos a las distintas funciones que hemos codificado en un
orden lógico:
void main()
{
insertar(5);
insertar(10);
insertar(50);
imprimir();
printf("Extraemos uno de la cola: %i\n", extraer());
imprimir();
liberar();
getch();
return 0;
}

Retornar
COLAS

1. INTRODUCCIÓN.
Una Cola es otro tipo especial de lista en el cual los elementos se insertan por un
extremo (el posterior) y se suprimen por el otro (el anterior o frente). Las colas se
conocen tambien como listas FIFO (primero en entrar,primero en salir). Las
operaciones para las colas son análogas a las de las pilas. Las diferencias
sustanciales consisten en que las inserciones se hacen al final de la lista, y no al
principio, y en que la terminología tradicional para colas y listas no es la misma.
Las primitivas que vamos a considerar para las colas son las siguientes.

2. OPERACIONES PRIMITIVAS DE LAS COLAS.


Dentro del tipo abstracto de cola podemos proponer las siguientes primitivas:

 CREAR()
 DESTRUIR(C)
 FRENTE(C)
 PONER_EN_COLA(x,C)
 QUITAR_DE_COLA(C)
 VACIA(C)
ESPECIFICACIÓN SEMANTICA Y SINTACTICA.

 cola crear ()
Argumentos: Ninguno.
Efecto: Devuelve una cola vacia preparada para ser usada.

 void destruir(cola C)
Argumentos: Una cola C.
Efecto: Destruye el objeto C liberando los recursos que mantiene que
empleaba.Para volver a usarlo habrá que crearlo de nuevo.

 tElemento frente (cola C)


Argumentos: Recibe una cola C no vacía.
Efecto: Devuelve el valor del primer elemento de la cloa C. Se puede
escribir en función de las operaciones primitivas de las listas
como: ELEMENTO(PRIMERO(C),C).

 void poner_en_cola (tElemento x, cola C)


Argumentos:

x: Elemento que queremos insertar en la cola.


C: Cola en la que insertamos el elemento x.

Efecto: Inserta el elemento x al final de la cola C. En función de las


operaciones de las listas seria: INSERTA(x,FIN(C),C).

 void quitar_de_cola (cola C)


Argumentos: Una cola C que debe ser no vacía.
Efecto: Suprime el primer elemento de la cola C. En función de las
operaciones de listas seria: BORRA(PRIMERO(C),C).

 int vacia (cola C)


Argumentos: Una cola C.
Efecto: Devuelve si la cola C es una cola vacía.

EQUIVALENCIA CON LAS LISTAS


3. IMPLEMENTACIÓN DE LAS COLAS.
IMPLEMENTACIÓN DE COLAS BASADA EN CELDAS ENLAZADAS.

Igual que en el caso de las pilas, cualquier implementación de listas es válida


para las colas. No obstante, para aumentar la eficiencia
de PONER_EN_COLA es posible aprovechar el hecho de que las inserciones se
efectúan sólo en el extremo posterior de forma que en lugar de recorrer la lista de
principio a fin cada vez que desea hacer una inserción se puede mantener un
apuntador al último elemento. Como en las listas de cualquier clase, tambien se
mantiene un puntero al frente de la lista. En las colas ese puntero es útil para
ejecutar mandatos del tipo FRENTE o QUITA_DE_COLA. Utilizaremos al
igual que para las listas, una celda cabecera con el puntero frontal apuntándola
con lo que nos permitirá un manejo más cómodo. Gráficamente, la estructura de
la cola es tal y como muestra la figura:
Una cola es pues un puntero a una estructura compuesta por dos punteros, uno al
extremo anterior de la cola y otro al extremo posterior. La primera celda es una
celda cabecera cuyo campo elemento se ignora.

La definición de tipos es la siguiente:


typedef struct Celda{
tElemento elemento;
struct Celda *siguiente;
} celda;

typedef struct {
celda *ant,*post;
} tcola;

typedef tcola *cola;

FUNCIÓN DE ABSTRACCIÓN.

Dado el objeto del tipo rep c, *c = (ant, post), el objeto abstracto que representa
es:

<c->ant->siguiente->elemento, c->ant->siguiente->siguiente->elemento, ..., c-


>ant->siguiente-> (n) ->siguiente->elemento>, tal que c->siguiente->siguiente-
> (n) ->siguiente = c->post.

INVARIANTE DE LA REPRESENTACIÓN.

Dado un objeto del tipo rep c, *c = (ant, post), debe cumplir:

a. c tiene valores obtenidos de llamadas (tcola **) malloc(sizeof(tcola));


b. Los campos siguiente de los nodos, c->ant y c->post tienen direcciones
válidas, obtenidas de llamadas a(celda) malloc(sizeof(celda)). Sólo
es NULL el últimode los campos siguiente.
Con estas definiciones, la implementación de las primitivas es la siguiente:
cola CREAR ()
{
cola C;

C = (tcola *) malloc(sizeof(tcola));
if (C == NULL)
error("Memoria insuficiente.");
C->ant = C->post = (celda *)malloc(sizeof(celda));
if (C->ant == NULL)
error("Memoria insuficiente.");
C->ant->siguiente = NULL;
return C;
}

void DESTRUIR (cola C)


{
while (!VACIA(C))
QUITAR_DE_COLA(C);
free(C->ant);
free(C);
}

int VACIA (cola C)


{
return(C->ant == C->post);
}

tElemento FRENTE (cola C)


{
if (VACIA(C)) {
error("Error: Cola Vacia.");
}
return(C->ant->siguiente->elemento);
}

void PONER_EN_COLA (tElemento x,cola C)


{
C->post->siguiente = (celda *) malloc(sizeof(celda));
if (C->post->siguiente == NULL)
error("Memoria insuficiente.");
C->post = C->post->siguiente;
C->post->elemento = x;
C->post->siguiente = NULL;
}

void QUITAR_DE_COLA (cola C)


{
celda *aux;

if (VACIA(C))
error("Cola vacia.");
aux = C->ant;
C->ant = C->ant->siguiente;
free(aux);
}

Este procedimiento QUITAR_DE_COLA suprime el primer elemento de C


desconectando el encabezado antiguo de la cola,de forma que el primer elemento
de la cola se convierte en la nueva cabecera.

En la figura siguiente puede verse esquematicamente el resultado de hacer


consecutivamente las siguientes operaciones:

 C=CREAR(C);
 PONER_EN_COLA(x,C);PONER_EN_COLA(y,C);
 QUITAR_DE_COLA(C);
 DESTRUIR(C);
Se puede observar que en el primer caso, la memoria que se obtiene del sistema
es la de la estructura de tipo celda que hace de cabecera y la memoria para ubicar
los dos punteros anterior y posterior. En los dos últimos casos, la línea punteada
indica la memoria que es liberada.

IMPLEMENTACIÓN DE LAS COLAS USANDO MATRICES CIRCULARES.

La implementación matrical de las listas no es muy eficiente para las colas,


puesto que si bien con el uso de un apuntador al último elemento es posible
ejecutar PONER_EN_COLA en un tiempo constante, QUITAR_DE_COLA,
que suprime le primer elemento, requiere que la cola completa ascienda una
posición en la matriz con lo que tiene un orden de eficiencia lineal proporcional
al tamaño de la cola. Para evitarlo se puede adoptar un criterio diferente.
Imaginemos a la matriz como un circulo en el que la primera posición sigue a la
última, en la forma en la que se ve en la figura siguiente. La cola se encuentra en
alguna parte de ese círculo ocupando posiciones consecutivas. Para insertar un
elemento en la cola se mueve el apuntador post una posición en el sentido de las
agujas del reloj y se escribe el elemento en esa posición. Para suprimir un
elemento simplemente se mueve ant una posición en el sentido de las agujas del
reloj. De esta forma, la cola se mueve en ese sentido conforme se insertan y
suprimen elementos. Obsérvese que utilizando este modelo los
procedimientos PONER_EN_COLA y QUITAR_DE_COLAse pueden
implementar de manera que su ejecución se realice en tiempo constante.

Existe un probelma que aparece en la representación de la figura anterior y en


cualquier variación menor de esta estrategia (p.e. que post apunte a la última
posición en el sentido de las agujas del reloj). El problema es que no hay forma
de distinguir una cola vacia de una que llene el círculo completo salvo que
mantengamos un bit que sea verdad si y solo si la cola está vacia. Si no deseamos
mantener este bit debemos prevenir que la cola llene alguna vez la matriz. Para
ver por qué puede pasar esto, supongamos que la cola de la figura anterior tuviera
MAX_LONG elementos. Entonces, post apuntaría a la posición anterior en el
sentido de las agujas del reloj de ant. ¿Qué pasaria si la cola estuviese vacia?.
Para ver como se representa una cola vacia, consideramos primero una cola de un
elemento. Entonces post y ant apuntarian a la misma posición. Si extraemos un
elemento, ant se mueve una posición en el sentido de las agujas del reloj,
formando una cola vacia. Por tanto una cola vacia tiene post a una posición de
ant en el sentido de las agujas del reloj, que es exactamente la misma posición
relativa que cuando la cola tenia MAX_LONG elementos. Por tanto vemos que
aún cuando la matriz tenga MAX_LONG casillas, no podemos hacer crecer la
cola más allá de MAX_LONG-1 casillas, a menos que introduzcamos un
mecanismo para distinguir si la cola está vacía o llena.

Ahora escribimos las primitivas de las colas usando esta representación para una
cola:
typedef struct {
tElemento *elementos;
int Lmax;
int ant,post;
} tipocola;

typedef tipocola *cola;

cola CREAR (int tamanoMax)


{
cola C;

C = (cola) malloc(sizeof(tipocola));
if (C == NULL)
error("No hay memoria.");
C->Lmax = tamanoMax+1;
C->ant = 0;
C->post = C->Lmax-1;
C->elementos = (tElemento *) calloc((tamanoMax+1), sizeof(tElemento));
if (C->elementos == NULL)
error("No hay memoria.");
return C;
}
void DESTRUIR (cola *C)
{
free(*C->elementos);
free(*C);
*C == NULL;
}

int VACIA (cola C)


{
return((C->post+1)%(C->Lmax) == C->ant)
}

tElemento FRENTE (cola C)


{
if (VACIA(C))
error("Cola vacia.");
return(C->elementos[C->ant]);
}

void PONER_EN_COLA (tElemento x,cola C)


{
if ((C->post+2) % (C->Lmax) == C->ant)
error("Cola llena.");
C->post = (C->post+1) % (C->Lmax);
C->elementos[C->post] = x;
}
void QUITAR_DE_COLA (cola C)
{
if (VACIA(C))
error("Cola vacia.");
C->ant = (C->ant+1) % (C->Lmax);
}

En esta implementación podemos observar que se reserva una posicón más que la
especificada en el parametro de la función CREAR. La razón de hacerlo es que
no se podrán ocupar todos los elementos de la matriz ya que debemos distinguir
la cola llena de la cola vacía. Estas dos situaciones por lo tanto vienen
representadas tal y como se muestra en la figura siguiente.
Se puede observar en el caso de la cola llena en la figura como la posición
siguiente a post no es usada y por lo tanto es necesario crear una matriz de un
tamaño N+1 para tener una capacidad para almacenar N elementos en cola.

Tutor de Estructuras de Datos Interactivo


Exposito Lopez Daniel, Abraham García Soto, Martin Gomez Antonio Jose
Director de proyecto: Joaquín Fernández Valdivia
5º Licenciatura Informatica
ETSII 99/00 (Universidad de Granada).

Pilas y colas
Pilas

Una pila (stack) es un objeto similar a una pila de platos, donde se puede agregar
y sacar datos sólo por el extremo superior. En computación esta estructura es
utilizada ampliamente, aunque muchas veces los usuarios ni siquiera se percaten.
Están tan arraigadas como concepto en computación, que una de las pocas cosas
que los procesadores (CPU) saben hacer, aparte de operaciones aritméticas, es usar
pilas.
La Pila se utiliza con las siguientes funciones:

 poner(p,x): agrega el elemento x a la pila p. También es conocido


como push
 int sacar(p): quita el elemento que está en la cima de la pila p (acá
suponemos una pila de números enteros). Hay gente que se refiere a este
método como pop y pull.
 vacía(p): retorna 1 si la pila p no tiene ningún elemento, 0 sino.

Las pilas son estructuras LIFO (last in, first out), el último que entra es el primero
en salir.

Barbarismos: una pila NO ES la batería que usa el reloj del computador, como
alguna vez dijo un alumno de pocas luces. Tampoco es cierto lo que una vez salió
en un Condorito: la invención del auto a pila (a pila de "jetones" que lo empuja)

Propuesto: Implemente una pila y sus funciones usando arreglos y otra usando
listas enlazadas.

Colas

Cuando uno quiere ir a comer un sandwich a la cafetería se pone en una fila. Esta
estructura es una estructura FIFO (first in, first out), el primero en entrar es el
primero en salir. En computación se llama "colas" a las filas (en la práctica
también, quien no ha escuchado "¡a la cola patudo!").

La Cola se utiliza, en forma análoga a una Pila, con las siguientes funciones:

 encolar(c,x): agrega el elemento x al final de la cola c.


 int decolar(c): saca el elemento que se encuentra al principio de la cola c
(acá también estamos suponiendo que la cola es de números enteros).
 int cabeza(c): devuelve cual es el elemento que esta al principio de la cola
c, sin sacarlo.
 vacia(c): devuelve 1 si la cola no tiene elementos, 0 sino.

Propuesto: Implemente una cola y sus funciones usando arreglos y otra usando
listas enlazadas.

Algunos ejemplos

Ejemplo 1: invertir números


Este es un programa muy simple que escribe en orden inverso una sucesión de
números leídos desde el teclado:

1. Pila p;
2. int numero;
3. printf ("ingresa números positivos, termina con –1\n");
4. scanf(“%d”,&numero);
5. while (numero!=-1)
6. {
7. poner(p,numero);
8. scanf(“%d”,&numero);
9. }
10. while (!vacia(p)) print (“%d ”,sacar(p));

Línea(s) Ejecución

1 se crea una pila auxiliar vacía. Suponemos que se guardan


enteros en la pila

4 se leen números mientras estos sean distintos de –1

7 el número se agrega a la pila por arriba

mientras haya números en la pila, se sacan (por arriba) y


10 se escriben

Ejemplo 2: una triquiñuela

Este método recibe una Pila (suponemos que se guardan enteros en ella) y la
devuelve invertida, usando una cola auxiliar.

1. void invertir(Pila p)
2. {
3. Cola c;
4. while (!vacia(p))
5. {
6. int n = sacar(p);
7. encolar(c,n);
8. }
9. while (!vacia(c))
10. poner(p,decolar(c));
11. }
Línea(s) Ejecución

10 La función invertir recibe una Pila p, de la clase


PilaString. Si bien la referencia NO puede cambiar, el
contenido del objeto puede ser modificado dentro de la
función y el cambio se verá en todo el programa.

13 se crea una cola auxiliar vacía.

16-17 se saca el elemento de la pila y se pone en la cola. Esto


mismo se podría haber escrito así: encolar(c,sacar(p));

19-20 mientras haya elementos en la cola, se van sacando y


colocando en la pila

Ejemplo 3: otra más

Análogo al ejemplo anterior, pero usando dos pilas en vez de una cola.

1. void invertir(Pila p)
2. {
3. Pila aux1, aux2;
4. while (!vacia(p))
5. poner(aux1,sacar(p));
6. while (!vacia(aux1))
7. poner(aux2,sacar(aux1));
8. while (!vacia(aux2))
9. poner(p,sacar(aux2));
10. }

Línea(s) Ejecución

22-23 Se vacia la pila p en la pila aux1

24-25 Se vacia la pila aux1 en la pila aux2

26-27 Se vacia la pila aux2 en la pila p


Ejemplo 4: un ejemplo “real” (Homenaje a Kurt Schwarze, coautor de estos
apuntes)

En un establo se guarda un rebaño de hipopótamos. Se desea ordenar los


hipopótamos según su peso, usando dos establos adicionales. Los establos
son tan angostos que los hipopótamos deben estar formados en fila. Cada
establo tiene un abertura que es la entrada y salida al mismo tiempo.

1. while (!vacia(establo1))
2. {
3. Hipo h=sacar(establo1);
4. poner(establo2,h);
5. while (!vacia(establo1))
6. {
7. Hipo h2=sacar(establo1);
8. h=sacar(establo2);
9. if (peso(h) < peso(h2))
10. {
11. poner(establo2,h);
12. poner(establo3,h2);
13. }
14. else
15. {
16. poner(establo3,h);
17. poner(establo2,h2);
18. }
19. }
20. while (!vacia(establo3))
21. poner(establo1,sacar(establo3));
22. }

Línea(s) Ejecución

34-35 se pasa un hipo del establo1 al establo2 (donde estan todos


ordenados)

38-48 se saca un hipo del establo 1 y se compara con el hipo mas


gordo del establo 2. El mas flaco se deja en el establo 2 y
el otro se tira al establo 3

36 esto se hace mientras haya hipos en el establo 1


51-52 se mueven todos los hipos desdel establo3 hacia el establo1

36 esto se repite mientras haya hipos en el establo 1

Ejemplo 5: La boletería del cine

Para ir a ver la película "Titanic II, la ira de Rose" hay dos filas de personas.
Un acomodador es el encargado de dejar entrar a la gente. Sus instrucciones
son simples: "deja entrar siempre a la persona que es mayor, si tienen la
misma edad, a gente de la fila 1 tiene preferencia". El siguiente código
muestra el comportamiento del acomodador. Suponemos que las colas
guardan referencias a Personas:

1. while (!vacia(cola1) || !vacia(cola2))


2. {
3. Persona p;
4. if (vacia(c1))
5. p=decolar(p);
6. else
7. if (vacia(c2))
8. p=decolar(c1);
9. else
10. {
11. Persona p1=cabeza(c1);
12. Persona p2=cabeza(c2);
13. if (p1.edad <= p2.edad) p=decolar(c1);
14. else
15. p=decolar(c2);
16. }
17. println ("Se atiende a %s de %d años\n",p.nombre,p.edad);
18. }

Línea(s) Ejecución

54 mientras una de las dos colas tenga gente...

57-61 si una de las colas está vacía, se deja entrar a l a gente


de la otra cola

64-65 vemos quienes están parados al inicio de cada cola


66 si es menor el de la cola 1, pasa él

67-68 si no, el otro

NUNCA saques elementos de una pila o una cola sin verificar antes que
tengan elementos.

Problemas propuestos

Problema 1: Escriba una función que utilizando una o más pilas, entregue el largo
(cantidad de elementos) de una cola. La función debe dejar la cola en el estado
original.

Problema 2: Una cola puede representarse utilizando dos pilas. Escriba las
funciones encolar, decolar y vacia utilizando las funciones de Pilas si el tipo de
dato Cola está definido como:

typedef struct c

Pila p1;

Pila p2;

} Cola;
Pilas y Colas C++
Publicado el junio 19, 2012 por Victor Hugo Choc Cac

Pilas y Colas escrito en C++, este es un solución donde se implementa en la forma


de manejar los datos de entrada, esto fue ejecutado en VisualStudio y también en
cualquier compilador standar, si tienes alguna duda no dudes en postearlo.

/*

Programación
Choc Cac, Victor Hugo.

*/

#include <cstdlib>
#include <iostream>

using namespace std;


const int NUMEROMAXIMO = 6;
int VALORMAXIMO = NUMEROMAXIMO-1;

//=======================================================
==
//==============Declaracion de la cola=====================
struct SCola{
int elemento;
struct SCola *siguiente;
};

typedef struct SCola TCola;

typedef struct{
TCola *Adelante;
TCola *ElFin;
}Cola;

void CrearCola(Cola *cola);


void InsertarCola (Cola *cola, int x);
int RemoverCola(Cola *cola);
int LimpiarCola(Cola cola);
//==========Fin de declaracones cola=======================
//=======================================================
==
//=======================================================
==
//=======Inicio de declariones Pila========================
typedef struct
{
int tope;
int item[NUMEROMAXIMO];
}pila;

int Lleno(pila*);
int VacioPila(pila*);
void push(pila*, int);
void pop(pila*, int*);

//==========fin declaraciones de pila======================


//==========fin declaraciones de pila======================
//=======================================================
==

int _tmain(int argc, _TCHAR* argv[])


{
//=======================================================
==
//========declaracines de pilas============================
pila p;
int dato;
p.tope=0;
//=========fin de Pila=====================================
//=======================================================
==

//=======================================================
==
//=========declaraciones de cola===========================
Cola cola;
CrearCola(&cola);
int Cx;
//=========fin de cola=====================================
//=======================================================
==

cout << “\n\t\tPrimeros en Entrar Ultimos en Salir”;


cout << “\n\t\t\t\t===PILA===\n”;
for(int i=0;i<VALORMAXIMO;i++)
{
cout << “\nEscribe Numero: “;
cin >> dato;
push(&p,dato);
cout << “insertado…”;
}

cout << “\n”;


for(int a=0;a<VALORMAXIMO;a++)
{
pop(&p,&dato);
Cx = dato;
InsertarCola(&cola, Cx);//insertamos en la cola
cout << “\nInserto en cola y se elimino en Pila = ” << dato;
}

/*
Una vez que se inserto
en Cola imprimimos
e eleminamos la cola.
*/
cout << “\n\n\t\tPrimeros en Entrar Primeros en Salir”;
cout << “\n\t\t\t\t===COLA===\n\n”;

for(int xy=0;xy<VALORMAXIMO;xy++)
{
cout << “Imprimiendo y elimando Cola: ” << RemoverCola(&cola) << “\n”;
}
//=============fin de impresion de cola
system(“pause”);
return 0;
}

//=======================================================
==
//==========Inicio funciones pila==========================
void push(pila *p,int dato)
{
if(!Lleno(p))
{
(p->tope)++;
p->item[p->tope]=dato; //elemento[1]=dato
}
else
{
cout << “\nDesbordamiento de bufer”;
}
}
void pop(pila *p,int *dato)
{
if(!VacioPila(p))
{
*dato=p->item[p->tope];
(p->tope)–;
}
else
{
cout << “\nDesbordamiento de bufer”;
}
}

int Lleno(pila *p)


{
return(p->tope==VALORMAXIMO);
}

int VacioPila(pila *p)


{
return(p->tope==0);
}
//==========fin funciones de pila==========================
//=======================================================
==

//=======================================================
==
//==========Inicio funciones de cola=======================
void CrearCola(Cola *cola)
{
cola->Adelante=cola->ElFin=NULL;
}

void InsertarCola (Cola *cola, int x)


{
TCola *nuevo;
nuevo=(TCola*)malloc(sizeof(TCola));
nuevo->elemento=x;
nuevo->siguiente=NULL;
if(LimpiarCola(*cola))
{
cola->Adelante=nuevo;
}
else
cola->ElFin->siguiente=nuevo;
cola->ElFin=nuevo;
}

int RemoverCola (Cola *cola)


{
int temp=NULL;
if(!LimpiarCola(*cola))
{
TCola *nuevo;
nuevo=cola->Adelante;
temp=cola->Adelante->elemento;
cola->Adelante=cola->Adelante->siguiente;
free(nuevo);
}
else
cout << “cola vacia: “;
return (temp);
}

int LimpiarCola(Cola cola)


{
return (cola.Adelante==NULL);
}
//==========Fin funciones de cola==========================
//======================================================

.------------------------
alguien me puede ayudar con la funcion buscar?
se los agradeceria muchoo!

#include"stdio.h"
#include"iostream.h"
#include"conio.h"
#define localizar (struct nodo*)malloc(sizeof(struct nodo))

struct nodo
{
int info;
struct nodo *sig;
struct nodo *ant;
};
struct nodo *cab, *p,*aux;
void iniciar()
{
cab=new nodo;
cab->sig=cab;
}
void sumar()
{
struct nodo*nuevo;
nuevo = new nodo;
cout<<"digite un numero: ";
cin>>nuevo->info;
nuevo->sig=cab->sig;
cab->sig=nuevo;
cab=nuevo;
}

void retirar(nodo *cab){

nodo *p ;
nodo *aux ;

if(cab!=NULL){
aux=cab->info
if(cab==NULL){
cab=NULL;

}
else{
cab=cab->sig;
return aux;
}
else
free(p);
}
}

void imprimir()
{
struct nodo*r;
if(cab==cab->sig)
{
cout<<"sin elemetos";
getch();
}
else
{
r=cab->sig;
do
{
r=r->sig;
cout<<r->info<<" ";
}while(r!=cab);
}
getch();
}

void contar(struct nodo *cab)


{
struct nodo *aux;
int z=0;
int n=0;
if(cab->sig==NULL)
cout<<"cola Vacia";

else
{
aux=cab->sig;
while(aux!=cab)
{z++;
aux=aux->sig;

}
cout<<"\nEl numero de elementos encontrados es : "<<z<<"\n";
}
getch();
}

void buscar(struct nodo *cab)


{
struct nodo*r;
int valor, band=0;
int i=1, a=0;
cout<<"digite el numero a buscar";
cin>>r->info;

if(cab==NULL)
{
cout<<"sin elemetos";
getch();
}
else
{
r=cab->sig;
do
{
r=r->sig;
cout<<r->info<<" numero "<<i<<endl;
i++;
}while(r!=cab);
}
getch();
}

int main()
{
int dato;
iniciar();
do
{
system("cls");
cout<<"1. adicionar \n";
cout<<"2. quitar \n";
cout<<"3. imprimir \n";
cout<<"4. contar nodos \n";
cout<<"5. buscar \n";
cout<<"6. salir \n";
cout<<"Digite dato \n";
cin>>dato;
switch(dato)
{
case 1:{ sumar();
break;
}
case 2:{retirar(cab);
break;
}
case 3:{imprimir();
break;
}
case 4:{
contar(cab);
getch();
break;
}

case 5:{
buscar(cab);
break;
}

}
} while(dato!=6);
return 0;
}

uando tenemos una llamada a una función que es lo último que hacemos en otra
función, decimos que esta llamada es una tail-call y entonces en este caso, se puede
aplicar la tail call optimization y ahorrarnos un frame en la pila de llamadas.

Ahorrarse un frame en la pila de llamadas puede no parecer mucho, pero es muy


interesante si somos capaces de crear funciones recursivas, donde la llamada
recursiva sea lo último que hagamos. En este caso, se puede aplicar TCO y en lugar
de tener un frame de pila por cada llamada recursiva, tendremos uno solo en total.
Es el mundo ideal: recursividad sin temer por el desbordamiento. A este tipo de
recursividad la conocemos como tail recursion (o recursividad de “cola”). Veamos un
ejemplo de ella.

Vamos a partir de una función recursiva clásica. En este caso el factorial:


function fact(n) {

if (n == 0)

return 1 ;

else

return n * fact(n-1) ;

Esta función recursiva es básicamente la misma que la del artículo anterior, dedicado
a la recursividad. Es un ejemplo de recursividad clásica.

Veamos ahora como quedaría el mismo ejemplo usando tail recursion:

function fact(n) {

return tail_fact(n,1) ;

function tail_fact(n,a) {

if (n == 0)

return a ;

else

return tail_fact(n-1,n*a) ;

La diferencia entre la función fact recursiva original y la tail_fact es que en la


primera no podemos aplicar TCO por qué realmente llamar a fact desde fact no
es la última cosa que hacemos. Puede parecer que sí, leyendo el código, pero no es
cierto. Observa que antes de devolver, multiplicamos el valor por n. Por lo tanto
debemos tener este valor de n accesible y este valor está en
el framecorrespondiente, y en cada frame el valor de n es distinto, por lo que
necesitamos mantener todos esos valores de n porque debemos recuperarlos al ir
“deshaciendo” la recursividad.

Por otro lado tail_fact lo último que hace antes de devolver es... llamar
a tail_fact. No multiplica el valor de tail_fact por una variable local (como
si hace fact), por lo tanto el estadio intermedio de todos los saltos recursivos no lo
necesitamos para nada.

Aplicando TCO, incluso si llamases a fact(10000) terminarías teniendo


tres framesen la pila: el global, el de la función fact y el de la función tail_fact.
En cambio con la versión recursiva tradicional tendrías 10001 frames en la pila, y
muy probablemente un desbordamiento.

Por supuesto para usar TCO es necesario que nuestro código use tail recursion pero
también que el compilador o motor del lenguaje lo soporte. P. ej. Scheme obliga a
que todas las implementaciones soporten TCO y lo mismo ocurre en… ¡ECMAScript
2015!

¡Espero que te haya resultado interesante!

--

Procedimientos y estructuras
recursivas
Table of Contents
 Bibliografía
 Diseño de funciones recursivas
o Recursión
o Confía en la recursión
 longitud
 sumatorio
 elemento?
 nth
o Recursión mutua
 Procesos recursivos e iterativos
o El coste espacial de la recursión
 longitud
 fibonacci
o Procesos recursivos e iterativos
o Procesos iterativos
o Fibonacci iterativo
o Memoization
o Triángulo de pascal
 Estructuras de datos recursivas
o Expresiones-s
 Definición
 Estructura jerárquica
 Funciones sobre expresiones-s como estructuras jerárquicas
o Árboles binarios
 Barrera de abstracción
 Barrera de abstracción de árbol binario
 Implementación de los árboles binarios
 Funciones de mayor nivel
 to-list-bt
 (member-bt? x bt)
 (insert-bt x bt)
 (insert-list-bt lista bt)
 (list-to-bt lista)
o Árboles genéricos
 Barrera de abstracción e implementación de árboles
genéricos
 Funciones sobre árboles genéricos
 (tree-to-list tree)
 (cuadrado-tree tree)
 map-tree

Bibliografía
El tema está basado en los siguientes materiales. Os recomendamos que los estudieis y,
si os interesa y os queda tiempo, que exploréis también en los enlaces que hemos dejado
en los apuntes para ampliar información.
 Abelson & Sussman
o Capítulo 1.2 Procedures and the Processes They Generate (apartados
1.2.1 al 1.2.5)
o Capítulo 2.2: Hierarchical Data
 Capítulo 6.6 Scott: Recursion

Diseño de funciones recursivas


Recursión
Ya hemos visto algunos ejemplos de funciones recursivas. Una función es recursiva
cuando se llama a si misma. Una vez que uno se acostumbra a su uso, se comprueba
que la recursión es una forma mucho más natural que la iteración de expresar un gran
número de funciones y procedimientos.

Recordemos el ejemplo típico de función recursiva, el factorial:

(dfeine (factorial x)
(if (= x 0)
1
(x * (factorial (- x 1)))))

La formulación matemática de la recursión es sencilla de entender, pero su


implementación en un lenguaje de programación no lo es tanto. El primer lenguaje de
programación que permitió el uso de expresiones recursivas fue el LISP. En el momento
de su creación existía ya el Fortran, que no permitía que una función se llamase a si
misma.

Confía en la recursión
Ya sabemos cómo es un algoritmo recursivo. Hemos visto varios ejemplos utilizando
números, identificadores o listas. Pero… ¿cómo diseñar un algoritmo recursivo? Para
resolver un problema de forma recursiva debes:

1. Descomponer el problema principal en alguna versión más simple, que puedas


resolver llamando a la propia función recursiva
2. Confiar en la recursión para resolver esta versión más simple del problema y que
va a devolver su solución
3. Obtener la solución al problema completo a partir de la solución de la versión más
simple

La frase confía en la recursión quiere decir que cuando estés analizando el


funcionamiento de un programa recursivo y veas una llamada recursiva debes confiar en
que esta llamada va a devolver el resultado que se prentende.
Es útil escribir y pensar en los procedimientos recursivos de forma declarativa, teniendo
en cuenta lo que hacen y no cómo lo hacen. Es útil pensar en la formulación recursiva del
problema de una forma matemática, analítica o gráfica antes de ponerse a programar. Es
muy útil también probar con algunos ejemplos concretos.

¿Cuándo para el algoritmo? Cuando el problema es lo más simple posible y ya no se


puede simplificar más: este es el caso base. Debemos entonces devolver un valor
concreto, la recursión ya ha terminado.

Vamos a utilizar estos consejos para construir algunos procedimientos recursivos.

longitud

Queremos diseñar la función (longitud lista) que devuelva la longitud de una lista.

Un ejemplo de un diseño recursivo de la función podría ser el siguiente:

 Supongamos que tenemos la lista (a b c d).


 ¿Cómo podemos llamar a la propia función longitud de forma recursiva con una
lista más pequeña?
 Podríamos quitar el primer elemento de la lista llamando a cdr y calcular la
longitud de la lista restante, confiando en la llamada recursiva:

 (longitud (cdr '(a b c d))) ->


 (longitud '(b c d)) -> 3

 Hemos obtenido 3. Como habíamos quitado el primer elemento de la lista, la


longitud de la lista original se obtien sumando 1.
 Este ejemplo nos lleva a la formulación declarativa de la recursión:

 longitud (lista) = 1 + longitud (resto (lista))

 Falta el caso base. Normalmente es la parte más sencilla. En este caso es trivial:
la longitud de la lista vacía es 0.

Y sólo queda convertir este diseño en un programa de Scheme:

(define (longitud lista)


(if (null? lista)
0
(+ 1 (longitud (cdr lista)))))

sumatorio
Queremos implementar la función (sumatorio n m) que devuelve la suma de los
números desde n hasta m, ambos incluidos.

Igual que en el ejemplo anterior, comenzamos con un caso sencillo y vemos cómo se
puede formular recursivamente.

sumatorio(8,20) = 8+9+...+20 = 8 + (9+10+...+20) = 8+sumatorio(9,20)

También se podría expresar otra recursión:

sumatorio(8,20) = 8+9+...+20 = (8+9+...+19)+20 = sumatorio(8,19)+20

Podemos considerar como caso base el caso en el que n/=/m y devolver n, o el caso en el
que n/>/m y devolver 0.

El paso a Scheme es directo.

Versión 1:

(define (sumatorio n m)
(if (= n m)
0
(+ n (sumatorio (+ n 1) m))))

Versión 2:

(define (sumatorio n m)
(if (= n m)
0
(+ m (sumatorio n (- m 1)))))

elemento?

Otro ejemplo. La función (elemento? x lista) que comprueba si el elemento x está


incluido en la lista.

El diseño recursivo consiste en quitar el primer elemento de la lista y preguntar si es x o


si x está en el resto de la lista. La descripción declarativa es la siguiente:

(define (elemento? x lista)


(if (null? lista)
#f
(or (equal? x (car lista))
(elemento? x (cdr lista)))))

nth

Vamos a ver cómo encontrar el elemento n-ésimo de una lista de forma recursiva. Este va
a ser un ejemplo algo distinto a los anteriores, ya que aquí no tendremos que construir
ninguna solución a partir de la llamada recursiva, sino que debermos buscar esta solución
haciendo cada vez más sencillo el problema.

Vamos a llamar a la función (nth n lista), donde n es la posición del elemento que
queremos devolver (comenzando por 1) y lista es la lista que recorremos.

Podemos empezar por preguntarnos qué sabemos hacer con las listas:

 Devolver el primer elemento de una lista utilizando la función car


 Devolver el resto de la lista sin el primer elemento utilizando la función cdr

O sea, que si nos preguntaran por el primer elemento de la lista:

(nth 1 '(a b c))

para devolverlo habría que llamar a car:

(car '(a b c)) -> a

¿Cómo podríamos usar la recursión para devolver un elemento que no está en primer
lugar? Esto es, ¿qué hacer cuando n es distinto de 1? Habrá que simplificar el problema y
pasarle a la llamada recursiva la lista sin el primer elemento y habiendo restado 1 a n:

(define (nth n lista)


(if (= n 1)
(car lista)
(nth (- n 1) (cdr lista)))

¿Qué pasaría si se llama a la función con el número n siendo mayor que el número de
elementos de la lista? Tendríamos un error, ya que la se intentaría obtener el primer
elemento de una lista vacía. Para solucionarlo, basta con contemplar este caso como otra
posible condición de terminación de la recursión:
(define (nth n lista)
(cond
((null? lista) lista)
((= n 1) (car lista))
(else (nth (- n 1) (cdr lista)))))

Recursión mutua
A veces es útil usar un patrón de recursión mutua, en el que dos funciones se llaman una
a la otra. El patrón recuerda la famosa ilustración Drawing Hands de M.C. Escher:

Un primer ejemplo de este comportamiento es la definición recursiva mutua


de par e impar:

 x es par si x-1 es impar


 x es impar si x-1 es par
 0 es par

La implementación en Scheme es directa:

(define (par? x)
(if (= 0 x)
#t
(impar? (- x 1))))

(define (impar? x)
(if (= 0 x)
#f
(par? (- x 1))))

Un ejemplo algo más complejo es la curva de Hilbert. La curva de Hilbert es una curva
fractal que tiene la propiedad de rellenar completamente el espacio. Su dibujo tiene una
formulación recursiva.

Vemos que la curva H3 se puede construir a partir de la curva H2.

Utilizamos la librería graphics/turtles que permite usar la conocida tortuga y comandos de


Logo en Scheme (draw y turn).

 La función (h-der i w) dibuja una curva de Hilbert de orden i con una longitud
de trazo wa la derecha de la tortuga.
 La función (h-izq i w) dibuja una curva de Hilbert de orden i con una longitud
de trazo wa la izquierda de la tortuga.

Para dibujar una curva de Hilbert de orden i a la derecha de la tortuga:

1. Gira la tortuga -90


2. Dibuja una curva de orden i-1 a la izquierda
3. Avanza w dibujando
4. Gira 90
5. Dibuja una curva de orden i-1 a la derecha
6. Avanza w dibujando
7. Dibuja una curva de orden i-1 a la derecha
8. Gira 90
9. Avanza w dibujando
10. Dibuja una curva de orden i-1 a la izquierda
11. Gira -90
El algoritmo para dibujar a la izquierda es simétrico.

Algoritmo en Scheme:

(require graphics/turtles)
(turtles #t)
(define (h-der i w)
(if (> i 0)
(begin
(turn -90)
(h-izq (- i 1) w)
(draw w)
(turn 90)
(h-der (- i 1) w)
(draw w)
(h-der (- i 1) w)
(turn 90)
(draw w)
(h-izq (- i 1) w)
(turn -90))))
(define (h-izq i w)
(if (> i 0)
(begin
(turn 90)
(h-der (- i 1) w)
(draw w)
(turn -90)
(h-izq (- i 1) w)
(draw w)
(h-izq (- i 1) w)
(turn -90)
(draw w)
(h-der (- i 1) w)
(turn 90))))

Podemos probarlo con distintos parámetros de grado de curva y longitud de trazo:

(clear)
(h-izq 3 20)
(h-izq 6 5)

El resultado de esta última llamada es:


Procesos recursivos e iterativos
Hemos visto que la recursión es una herramienta muy elegant y con un alto nivel de
abstracción. Pero hay que tener cuidado al utilizarla, porque a veces una solución
recursiva pura es muy elegante pero tiene un coste demasiado elevado.

Veremos en este apartado que en estos casos es posible modificar la recursión y construir
una solución que crea un proceso iterativo utilizando lo que se denomina recursión por la
cola.

El coste espacial de la recursión


Vamos examinar en primer lugar el coste espacial de la recursión. ¿Cuál es el coste
espacial de una llamada recursiva? ¿Qué información debo guardar para realizar la
recursión? La respuesta es que el coste viene dado por el número de llamadas a
funciones que quedan a la espera de que termine la recursión. Veamos algunos ejemplos.

longitud

Comenzamos repasando la función longitud que hemos visto anteriormente.


Supongamos la siguiente invocación de esta función:

(longitud '(a b c d)) -> 4

La traza de llamadas recursivas que se han generado con la llamada anterior es la


siguiente:
(longitud '(a b c d))
(+ 1 (longitud '(b c d)))
(+ 1 (+ 1 (longitud '(c d))))
(+ 1 (+ 1 (+ 1 (longitud '(d)))))
(+ 1 (+ 1 (+ 1 (+ 1 (longitud '())))))
(+ 1 (+ 1 (+ 1 (+ 1 0))))
(+ 1 (+ 1 (+ 1 1)))
(+ 1 (+ 1 2))
(+ 1 3)
4

Cada llamada a la recursión deja en espera una llamada a la función + con los
parámetros 1 y el propio resultado de la recursión. Como en cualquier llamada a una
función, este estado local se almacena en una /pila de llamada/ de la que será recuperado
cuando la llamada termine. En el caso de la recursión la pila va creciendo hasta que la
recursión termina. Si por un error la recursión no termina y se entra en un bucle infinito en
algún momento se produce un stack overflow, un desbordamiento de la memoria
reservada por el sistema operativo para el programa que está en funcionamiento.

Estas llamadas en espera son las que determinan el coste espacial de la recursión. En
este caso el coste espacial depende linealmente de la longitud de la palabra. Se trata de
un coste O(n), siendo n la longitud de la palabra.

fibonacci

El coste espacial y el temporal depende del número de llamadas a la recursión. En el caso


en que se realicen dos llamadas a la recursión tendríamos un coste exponencial.

Veámoslo con una recursión que implementan de forma elegante, pero poco eficiente, la
serie de Fibonacci.

Recordemos que la secuencia de números de Fibonacci se define con la siguiente


expresión:

fib (n) = fib (n-1) + fib (n-2)


fib (1) = fib (0) = 1

De esta forma, la secuencia de números de Fibonacci es: 1, 1, 2, 3, 5, 8, 13, 21, …

La función recursiva que resulta de aplicar directamente esta ecuación es la siguiente:

(define (fib n)
(cond ((= n 0) 0)
((= n 1) 1)
(else (+ (fib (- n 1))
(fib (- n 2))))))
La diferencia con las funciones recursivas vistas hasta ahora es que en el cuerpo de la
función se realizan dos llamadas recursivas. Esto genera un proceso recursivo en forma
de árbol, como se comprueba en la siguiente figura, extraída del Abelson & Sussman:

Cada llamada a la recursión produce otras dos llamadas, con lo que el número de
llamadas finales es 2n, siendo n el número que se pasa a la función. El coste espacial y
temporal es por tanto de O(2n). Podemos comprobar con el intérprete que es imposible
evaluar la función para números a partir de 30.

¿Es posible diseñar la función recursiva de otra forma, para evitar este coste? Vamos a
verlo.

Procesos recursivos e iterativos


En la asignatura hecho la distinción entre programas y los procesos que éstos generan.
Esta distinción se hace muy importante en el caso de los programas recursivos.

Hemos visto en el apartado anterior que el proceso generado por una función recursiva
tiene a veces un alto coste espacial y temporal debido a las llamadas que permenacen en
espera en la pila de la recursión.

Es posible escribir programas recursivos que generen procesos que no dejan llamadas en
espera. A estos procesos los llamamos procesos iterativos, en contraposición con
los procesos recursivosque generan los programas vistos hasta ahora.

La siguiente versión de la función factorial está escrita de forma que el proceso generado
es iterativo y no deja llamadas en espera.
(define (factorial-iter n)
(fact-iter-aux n n))

(define (fact-iter-aux product n)


(if (= n 1)
product
(fact-iter-aux (* product (- n 1)) (- n 1))))

La función (fact-iter-aux product n) es la que implementa la nueva versión de


factorial. El parámetro adicional product que se ha añadido guarda en cada llamada el
cálculo parcial del factorial. Cuando la recursión llega a su fin ya se tiene el resultado
calculado y se devuelve directamente, sin necesidad de evaluar ninguna función en
espera.

Lo podemos comprobar en la secuencia de llamadas que se produce para


evaluar (factorial-iter 4):

(factorial-iter 4)
(factorial-iter-aux 4 4)
(factorial-iter-aux 12 3)
(factorial-iter-aux 24 2)
(factorial-iter-aux 24 1)
24

Veamos otro ejemplo más, la versión iterativa de la función longitud que calcula la
longitud de una lista.

(define (longitud-iter lista)


(longitud-iter-aux lista 0))

(define (longitud-iter-aux lista result)


(if (null? lista)
result
(longitud-iter-aux (cdr lista) (+ result 1))))

La función longitud-iter ahora recibe un argumento adicional, denominado result.


Igual que en la función fact-aux anterior en él se almacena el valor parcial del cálculo
de la recursión. En este caso de la longitud de la palabra.

En cada llamada recursiva se incrementa en 1 el resultado parcial y se elimina un


elemento de la lista. De esta forma, cuando la lista esté vacía, el resultado habrá
acumulado su longitud (siempre que inicialmente se haya pasado 0 como valor).

La función longitud se redefine para que llame a longitud-iter con los valores
iniciales correctos.
Como podemos comprobar, tampoco se deja ninguna llamada en espera en la pila de
llamada. Cuando alcanzamos el caso base de la recursión tenemos la respuesta al
problema completo.

(longitud 'abcdef)
(longitud-iter 'abcdef 0)
(longitud-iter 'bcdef 1)
(longitud-iter 'cdef 2)
(longitud-iter 'def 3)
(longitud-iter 'ef 4)
(longitud-iter 'f 5)
(longitud-iter "" 6)
6

Procesos iterativos
Las dos funciones anteriores son ejemplos de funciones que generan procesos iterativos.

En general, las versiones iterativas son menos elegantes y más difíciles de entender que
las recursivas, pero son más eficientes.

En otros lenguajes de programación existen bucles (for, while) para definir procesos
iterativos. En un lenguaje funcional puro no existen bucles; el proceso iterativo se
implementa como hemos visto. Se utiliza uno o más parámetros auxiliares y se modifica
su valor en cada llamada a la recursión.

Esta recursión que no deja llamadas en espera también recibe el nombre de tail
recursión o recursión por la cola. El código del intérprete que evalua la recursión por la
cola puede tratar estas llamadas de una forma especial, sabiendo que no hay que
devolver el resultado para que vuelva a ser procesado. Se devuelve el último valor a la
llamada original y se puede eliminar la pila de la llamada.

Fibonacci iterativo
La versión recursiva de la función que devuelve el número de Fibonacci tenía un coste
exponencial, debido a las dos llamadas recursivas.

(define (fib n)
(cond ((= n 0) 0)
((= n 1) 1)
(else (+ (fib (- n 1))
(fib (- n 2))))))

Se puede implementar una versión eficiente de la misma función utilizando el mecanismo


de recursión por la cola.
(define (fib-iter a b count)
(if (= count 0)
b
(fib-iter (+ a b) a (- count 1))))

(define (fib n)
(fib-iter 1 0 n))

La función (fib-iter a b count) define un proceso iterativo que va calculando cada


número sucesivo de fibonacci utilizando los parámetros a y b. El parámetro count cuenta
el número de iteraciones.

Esta recursión tiene un coste lineal O(n) y permite calcular sin problemas valores de
fibonacci imposibles de obtener con la recursión anterior.

Memoization
La memoization es una técnica de optimización que permite mantener la elegancia de los
procesos recursivos sin incurrir en el coste de las llamadas repetidas a la recursión.

Si miramos la traza de (fibonaci 4) podemos ver que se está llamando de forma


repetida a la recursión con los mismos valores.

Por ejemplo, para calcular (fib 200) hay que llamar a (fib 199) y (fib 198). A su
vez, para calcular (fib 199) se llama a (fib 198) y (fib 197). ¿Por qué llamar dos
veces a (fib 198)? Estamos en programación funcional y el resultado de la llamada
siempre va a ser el mismo. ¿No podríamos guardar el valor devuelto por (fib 198) la
primera vez y utilizarlo la segunda vez? De esta forma no volveríamos a lanzar la
recursión para este valor (ni para ningún otro).

Esto es lo que hace la memoization, en una llamada recursiva guarda el valor devuelto
por cada una de las llamadas en alguna estructura de datos adecuada (una tabla hash por
ejemplo) y lo reutiliza en el resto de llamadas.

La forma de implementar la memoization en Scheme utiliza continuaciones y la forma


especial no funcional set!, características del lenguaje que todavía no hemos estudiado.
Una explicación muy buena de cómo hacerlo se puede encontrar en Community Scheme
Wiki.

Triángulo de pascal
El último ejemplo de función recursiva que vamos a transformar en iterativa es el triángulo
de Pascal.

Cada elemento del triángulo de Pascal es la suma de los dos números que hay sobre él:
1
1 1
1 2 1
1 13 3
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1
...

La función Pascal(fila, columna) se define matemáticamente de la siguiente forma:

Pascal(fila, 0) = 1 para cualquier valor de fila


Pascal(fila, columna) = 1 si fila = columna
Pascal(fila, columna) = Pascal(fila-1,columna-1) + Pascal(fila-1, columna) en otro caso

La función Scheme que calcula de forma recursiva el número de Pascal que se encuentra
en una determinada fila y columna es la siguiente:

(define (pascal row col)


(cond ((= col 0) 1)
((= col row) 1)
(else (+ (pascal (- row 1) (- col 1))
(pascal (- row 1) col) ))))

Al igual que con la función fibonacci esta solución es muy poco eficiente. Para calcular un
valor de la recursión se hacen dos llamadas recursivas, lo que genera un proceso en
forma de árbol con un coste exponencial.

Una forma de solucionar el problema con una versión iterativa consiste en generar cada
una de las filas del triángulo a partir de la anterior. La fila se puede representar con una
lista.

La función (pascal-sig-fila fila) se encarga de construir la fila siguiente del


triángulo de Pascal a partir de la anterior. Por ejemplo:

(pascal-sig-fila '(1 3 3 1)) -> (1 4 6 4 1)

Su implementación se basa en la función (pascal-sig-fila-central fila) que


calcula de forma recursiva los elementos interiores de la fila. Esta función no es iterativa.

(define (pascal-sig-fila fila)


(append '(1)
(pascal-sig-fila-central fila)
'(1)))

(define (pascal-sig-fila-central fila)


(if (= 1 (length fila))
'()
(append (list (+ (car fila) (car (cdr fila))))
(pascal-sig-fila-central (cdr fila)))))

La función (pascal-iter-aux fila n) es la que realiza las n iteraciones


utilizando fila como parámetro auxiliar en el que se van construyendo las sucesivas
filas. Termina cuando la longitud de la fila calculada coincide con n.

Por último, la función (pascal-iter fila col) llama a la anterior para producir la fila
número fila, y devuelve el elemento número col:

(define (pascal-iter fila col)


(if (= 0 col)
1
(list-ref (pascal-iter-aux '(1 1) fila) col)))

(define (pascal-iter-aux fila n)


(if (= n (- (length fila) 1))
fila
(pascal-iter-aux (pascal-sig-fila fila) n)))

La función pascal-sig-fila se encarga de construir la fila siguiente del triángulo de


Pascal a partir de la anterior. Por ejemplo:

La función pascal-iter va llamando de forma iterativa a pascal-sig-fila hasta que


llegamos a la fila n que deseamos. La función pascal recoge esa fila y devuelve el
elemento col deseado.

Puedes probar los siguientes valores para comprobar la diferencia de rendimiento entre
las dos versiones:

(pascal 28 10) -> 13123110


(pascal-iter 28 10) -> 13123110

Estructuras de datos recursivas


En este apartado vamos a estudiar el diseño y la implementación en Scheme de tres
estructuras de datos que tienen definiciones recursivas:

 Expresiones-s
 Árboles binarios
 Árboles genéricos

Expresiones-s
Las expresiones-s (expresión simbólica) son la base del diseño del lenguaje de
programación LISP. Fueron introducidas en 1958 por John McCarthy, en el
artículo Recursive Functions of Symbolic Expressions and Their Computation by Machine,
Part I, en el que define el lenguaje de programación LISP.

Definición

En su formulación original de McCarty, una expresión-s es una expresión matemática que


se define con la siguiente formulación recursiva:

 Un dato atómico es una expresión-s


 Si e1 y e2 son expresiones-s, la expresión (e1 . e2) también es una expresión-s

Por ejemplo, las siguientes expresiones son expresiones-s de McCarthy:

(A . B)
(A . (B . C))
((A . B) . (C . D))
(A. ((A . B) . (C . D)))

Y las siguientes expresiones no son expresiones-S de McCarthy:

(A . B . C)
(A)
(() . B)

En la misma formulación, McCarthy define una lista como un tipo especial de expresión-s
que termina con el átomo NIL:

(m1, m2, …, mn) -> (m1 . (m2 . ( … (mn . NIL) … )))

Esta formulación matemática fue el origen del concepto de pareja de LISP utilizando las
funciones cons, car y cdr (también definidas en el mismo artículo).

En la actualidad, el concepto de expresión-s se ha extendido, eliminando la notación del


punto y refiriéndose a expresiones con símbolos (identificadores) entre un número
balanceado de paréntesis.

Los siguientes son ejemplos de expresiones-s extendidas:


(= 4 (+ 2 2))
(if (= x y) (* x y) (+ (/ x y) 45))
(define (factorial x) (if (= x 0) 1 (* x (factorial (- x 1)))))

Esta es la interpretación que vamos a utilizar a partir de ahora cuando hablemos


de expresión-sen general.

En Scheme las expresiones-s se implementan con listas. Cuando el intérprete, en su


bucle read-eval-print recibe una expresión-s, la convierte en una lista e intenta evaluarla.

Cualquier expresión de Scheme correcta es una expresión-s, pero al revés no. Por
ejemplo, las siguientes expresionesn-s son expresiones correctas de Scheme:

(define (factorial x) (if (= x 0) 1 (* x (factorial (- x 1)))))


(define lista (quote (1 (2 (3)))))

Y la siguiente expresión-s no lo es:

(1 (2 (3)))

La consideración de una expresión-s como una lista nos lleva a su última definición
recursiva:

 Una expresión-s es una lista cuyos elementos pueden ser datos atómicos o
expresiones-s
 Una expresión-s vacía es una lista vacía

Con esta definición, considerando las expresiones-s como listas, vemos que se pueden
aplicar las funciones car y cdr para recorrerlas:

 La función car devolverá un dato atómico u otra expresión-s


 La función cdr devolverá la expresión-s resultante de eliminar el primer elemento
de la lista. Nunca devolverá un dato atómico.

Estructura jerárquica

Las expresiones-s representan una estructura de niveles, en la que la lista inicial define el
nivel 1, y las siguientes expresiones anidadas van aumentando de nivel.

Los datos atómicos constituyen las hojas de la estructura.


 Otro ejemplo (ejercicio en clase):

 (let ((x 12)


 (y 5))
 (+ x y)))

Las funciones que podemos utilizar para recorrer una expresion-s son:

 list y quote: construcción


 cons: añadir elemento a la cabeza
 car: devuelve el primer dato (puede ser una hoja o una expresión-S)
 cdr: devuelve el resto (siempre una expresión-S)
 null?: comprueba si es lista vacía

Para remarcar el carácter jerárquico de las expresiones-s definimos la función hoja? que
comprueba si el elemento es una hoja o una expresión-s (lista)

(define (hoja? x)
(not (pair? x)))

Funciones sobre expresiones-s como estructuras jerárquicas


 (cuenta-hojas-exp exp): cuenta las hojas
 (niveles-exp exp): cuenta los niveles
 (pertenece-exp x exp): busca una hoja
 (cuadrado-exp exp): eleva todas las hojas al cuadrado
 (map-exp f exp): similar a map

La función (cuenta-hojas-exp exp) cuenta las hojas que hay en una expresión-s

(define (cuenta-hojas-exp exp)


(cond
((null? lista) 0)
((hoja? (car exp)) (+ 1 (cuenta-hojas-exp (cdr exp))))
(else
(+ (cuenta-hojas-exp (car exp))
(cuenta-hojas-exp (cdr exp))))))

La función (niveles-exp exp) devuelve el número de niveles de una expresión-s.

(define (niveles-exp exp)


(cond
((null? exp) 0)
((hoja? exp) 0)
(else (max (+ 1 (niveles-exp (car exp)))
(niveles-exp (cdr exp))))))

La función (pertenece-exp? x exp) comprueba si el átomo x aparece en la


expresión-s

(define (pertenece-exp? x exp)


(cond
((null? exp) #f)
((hoja? exp) (equal? x exp))
(else (or (pertenece-exp? x (car exp))
(pertenece-exp? x (cdr exp))))))

La función (cuadrado-exp exp) devuelve una expresión-s formada por números con la
expresión-s con la misma estructura y los números elevados al cuadrado.

(define (cuadrado-exp exp)


(cond ((null? exp) '())
((hoja? exp) (cuadrado exp))
(else (cons (cuadrado-exp (car exp))
(cuadrado-exp (cdr exp)) )) ))

Por último (map-exp f exp) devuelve una expresión-s formada con la misma
estructura que la original con el resultado de aplicar a cada uno de sus átomos la
función f

(define (map-exp f exp)


(cond ((null? exp) '())
((hoja? exp) (f exp))
(else (cons (map-exp f (car exp))
(map-exp f (cdr exp)) )) ))

Árboles binarios
Un árbol binario es una estructura que contiene tres elementos: un dato, un árbol binario
izquierdo y un árbol binario derecho.

Esta definición es recursiva y necesita un caso base. Es el siguiente: un árbol binario


puede ser también un árbol-vacio, un símbolo especial vacio-bt que simboliza un árbol
que no tiene datos ni hijos.
En la asignatura no nos preocupa cuáles son los datos del árbol, ni su ordenación; sólo su
estructura. Veremos un conjunto de funciones que definen la barrera de abstracción para
trabajar con árboles binarios y después las utilizaremos para construir funciones de mayor
nivel.

Barrera de abstracción

Ya hemos comentado que una de las características de los lenguajes de programación es


que permiten construir abstracciones.

Llamamos barrera de abstracción al conjunto de funciones que permiten trabajar con una
abstracción y aislan su implementación. Estas funciones definen la especificación de la
abstracción: qué nos permite hacer la abstracción.

En el caso de las estructuras de datos, las funciones de la barrera de abstracción


permiten crear nuevas estructuras y trabajar con ellas.
Cuando estamos trabajando con una estructura de datos o con cualquier otra abstracción
no debemos saltar la barrera de abstracción. Las funciones superiores siempre deben
usar las funciones proporcionadas por la barrera de abstracción. De esta forma no les
afectan posibles cambios en la implementación de la barrera.

En informática se utilizan barreras de abstracción continuamente. Otros nombres


alternativos que recibe este concepto son: capa de una aplicación, interfaz, API
(Application Programming Interface).

Por ejemplo, las distintas capas de un sistema operativo, un driver de un dispositivo, un


API accesible por web de una aplicación como Twitter o Google Maps o todo un nuevo
lenguaje para hacer un (como OpenGL para crear gráficos 3D).

Muchos lenguajes de programación tienen elementos más elaborados para definir


abstracciones (clases, métodos, paquetes, modificadores de visibilidad, etc.). En el caso
de Scheme, la única forma de definir una barrera de abstracción es definiendo una lista de
funciones.

Barrera de abstracción de árbol binario

Representamos la barrera de abstracción dibujando dos capas separadas por una lista de
funciones. La capa inferior representa el código que implementa la barrera de
abstracción. La capa superior representa el código que usa la barrera de abstracción.

En el caso de los árboles binarios, definimos la siguiente barrera:


Las funciones de la barrera son las siguientes:

 (make-bt dato izq-bt der-bt): crea un árbol binario con un dato, un árbol
binario izquierdo y un árbol binario derecho
 vacio-bt: constante que define el árbol binario vacío
 (dato-bt bt): devuelve el dato de un árbol binario
 (izq-bt bt): devuelve el árbol binario izquierdo
 (der-bt bt): devuelve el árbol binario derecho
 (hoja-bt? bt): comprueba si el árbol es una hoja (no tiene hijos)
 (vacio-bt? bt): comprueba si el árbol es vacío
Usamos el convenio de que todas las funciones de la barrera terminan con el mismo
sufijo. En esta caso -bt de binary tree. También usamos el convenio de llamar make- a la
función que construye la estructura de datos.

Una vez que hemos definido las funciones (y suponiendo que alguien las haya
implementado) podemos usarlas para trabajar con la abstracción. Por ejemplo vamos a
construir un árbol binario y a obtener algunos de sus elementos.

*
/ \
+ 8
/ \
5 3
(define t1 (make-bt 5 vacio-bt vacio-bt))
(define t2 (make-bt 3 vacio-bt vacio-bt))
(define t3 (make-bt '+ t1 t2))
(define t4 (make-bt 8 vacio-bt vacio-bt))
(define t5 (make-bt '* t3 t4))
(dato-bt t5) -> *
(dato-bt (izq-bt t5)) -> +
(dato-bt (izq-bt (izq-bt t5))) -> 5

Implementación de los árboles binarios

Necesitamos una estructura con la que podamos guardar 3 elementos: el dato de la raiz,
el sub-árbol izquierdo y el sub-árbol derecho.

La forma natural en Scheme es utilizar una lista que contiene a esos tres elementos. Y el
árbol binario vacio se puede representar entonces como una lista vacía.
Vemos que con esta definición un árbol binario se puede expresar como una expresión-s.
En el caso del ejemplo visto en la figura la expresión-s que define el árbol binario es:

(10 (5 (3 () ()) ()) (23 (12 () ()) (28 () ())))

La implementación de la barrera de abstracción en Scheme es la siguiente:

(define (make-bt dato izq der)


(list dato izq der))
(define vacio-bt '())

(define (vacio-bt? btree) (null? btree))


(define (dato-bt btree) (car btree))
(define (izq-bt btree) (car (cdr btree)))
(define (der-bt btree) (car (cdr (cdr btree))))

(define (hoja-bt? btree)


(and (vacio-bt? (izq-bt btree))
(vacio-bt? (der-bt btree))))

Funciones de mayor nivel

Terminamos construyendo algunas operaciones sobre árboles binarios, definidas sobre la


barrera de abstracción anterior.

 (to-list-bt bt): devuelve una lista formada por los elementos del bt
 (member-bt? x bt): busca el elemento x en un árbol binario ordenado
 (insert-bt x bt): "inserta" (realmente, no modifica el árbol que se pasa como
parámetro, sino que crea otro) un dato en un árbol binario ordenado
 (insert-list-bt lista bt): "inserta" una lista en un árbol binario ordenado
 (list-to-bt list): construye un árbol binario ordenado a partir de una lista

to-list-bt

La función (bt-to-list bt) construye una lista con los elementos contenidos en el
árbol binario bt.

(define (to-list-bt btree)


(if (vacio-bt? btree)
'()
(append (to-list-bt (izq-bt btree))
(list (dato-bt btree))
(to-list-bt (der-bt btree)))))

La recursión de esta función se basa en obtener el dato de la raíz, obtener de forma


recursiva la lista de elementos de la rama izquierda y la lista de elementos de la rama
derecha. La función appendconcatena todos los elementos. Dependiendo del orden en
que los concatenemos aparecerán los elementos en pre-order, in-order o post-order.

(member-bt? x bt)

La función (member-bt? x bt) comprueba si un número x pertenece a un árbol binario


ordenado bt. En el árbol binario ordenado todos los datos del hijo izquierdo son menores
que el dato de la raíz y los del hijo derecho son mayores.

La búsqueda se implementa con la típica búsqueda dicotómica:

 Si el dato de la raíz del árbol es menor que x, hacemos una llamada recursiva
para comprobar si el dato está el subárbol derecho
 Si el dato es mayor que x, buscamos x en el subárbol izquierdo
 Si el dato de la raiz es igual que x devolvemos #t.
El coste de la búsqueda es log(n).

(define (member-bt? x bt)


(cond
((vacio-bt? bt) #f)
((= x (dato-bt bt)) #t)
((< x (dato-bt bt))
(member-bt? x (izq-bt bt)))
(else
(member-bt? x (der-bt bt)))))

(insert-bt x bt)

La función insert-bt devuelve un nuevo árbol binario nuevo a partir de bt en el que se


ha colocado el dato x en su posición correcta.

La función se basa en identificar en qué subárbol va el dato que estamos insertando. Si el


dato es mayor que la raíz hay que ponerlo en el hijo derecho, por lo quec onstruimos un
nuevo árbol con la misma raíz e hijo izquierdo que el actual y como hijo derecho ponemos
el resultado de la llamada recursiva de insertar un dato en el árbol derecho (¡confía en la
recursión!). Si el dato es menor hacemos lo contrario.

(define (insert-bt x bt)


(cond
((vacio-bt? bt) (make-bt x vacio-bt vacio-bt))
((< x (dato-bt bt))
(make-bt (dato-bt bt)
(insert-bt x (izq-bt bt))
(der-bt bt)))
((> x (dato-bt bt))
(make-bt (dato-bt bt)
(izq-bt bt)
(insert-bt x (der-bt bt))))
(else bt)))

(insert-list-bt lista bt)

La función insert-list-bt se basa en la función anterior para insertar todos los


elementos de una lista en un árbol binario. Al igual que en el caso anterior, hay que hacer
notar que no existe tal inserción, sino que se construye un árbol nuevo que se devuelve
como resultado.

(define (insert-list-bt lista bt)


(if (null? lista)
bt
(insert-list-bt (cdr lista)
(insert-bt (car lista) bt))))
(list-to-bt lista)

Por último, la función list-to-bt construye una árbol binario ordenado a partir de una
lista de números utilizando la función anterior.

(define (list-to-bt lista)


(insert-list-bt lista vacio-bt))

Árboles genéricos
A diferencia de los árboles binarios, los árboles genéricos pueden tener un número de
hijos variables. Se usan en un gran número de dominios en los que es necesario
representar estructuras jerárquicas: procesadores de lenguaje, documentos XML,
documentos HTML, etc.

La definición recursiva del tipo de datos árbol:

 Un árbol genérico está formado por un dato y una lista de árboles hijos (también
árboles)

La lista de árboles hijos puede ser vacía. De esta forma, a diferencia de los árboles
binarios, en los árboles genéricos no usamos el concepto de árbol-vacio. Un árbol o nodo
hoja es un árbol cuya lista de hijos es una lista vacía.

La siguiente figura representa un árbol genérico.


Y su representación utilizando la definición anterior:
Barrera de abstracción e implementación de árboles genéricos

La barrera de abstracción de los árboles genéricos se construye únicamente con las


siguientes funciones:

 (make-tree dato lista-hijos): construye un árbol a partir de un dato y una


lista de hijos formada por árboles. La lista puede ser vacía.
 (dato-tree tree): devuelve el dato de la raíz del árbol
 (hijos-tree tree): devuelve una lista de árboles hijos

Vemos que utilizamos el sufijo -tree para todas las funciones.

Una estructura muy relevante es la lista de hijos, una lista de árboles. En las siguientes
funciones vamos a utilizar esta estructura en bastantes ocasiones. Vamos por ello a darle
un nombre y denominarla bosque. Decimos, por tanto, que la función hijos-tree es
un bosque formado por los hijos del árbol.

¿Cómo implementamos los árboles? En Scheme es muy fácil, utilizando una lista. El
primer elemento de la lista es el dato del árbol y el resto de la lista (otra lista) es la lista de
hijos.

Las implementación de las funciones es la siguiente:

(define (make-tree dato lista-hijos) (cons dato lista-hijos))


(define (dato-tree tree) (car tree))
(define (hijos-tree tree) (cdr tree))

El árbol anterior se puede construir con las siguientes instrucciones:

(define t1 (make-tree 5 '()))


(define t2 (make-tree 2 '()))
(define t3 (make-tree 3 '()))
(define t4 (make-tree 10 '()))
(define t5 (make-tree 12 '()))
(define t6 (make-tree '* (list t2 t3)))
(define t7 (make-tree '- (list t5)))
(define t8 (make-tree '+ (list t1 t6 t4)))
(define tree (make-tree '* (list t8 t7)))

Si expresamos el árbol en forma de lista podemos comprobar que es una expresión-s:

(+ (5) (* (2) (3)) (10))


De hecho, podríamos construirlo también utilizando directamente su formulación como
expresión-s:

(define tree '(+ (5) (* (2) (3)) (10)))

Funciones sobre árboles genéricos

Vamos a construir las siguientes funciones que trabajan sobre árboles genéricos:

 (tree-to-list tree): devuelve una lista con los datos del árbol original
 (square-tree tree): eleva al cuadrado todos los datos de un árbol
manteniendo la estructura del árbol original
 (map-tree f tree): devuelve un árbol con la estructura del árbol original
aplicando la función f a sud datos.

Vamos a ver que todas ellas comparten un mismo patrón de recursión mutua en la que la
función se divide en dos partes: una función sobre árboles y otra sobre bosques (listas de
árboles) que se aplica a los hijos.

(tree-to-list tree)

Comencemos con la función (tree-to-list tree) devuelve una lista con todos los
elementos del árbol.

(define (to-list-tree tree)


(cons (dato-tree tree)
(to-list-bosque (hijos-tree tree)))))

(define (to-list-bosque bosque)


(if (null? bosque)
'()
(append (to-list-tree (car bosque))
(to-list-bosque (cdr bosque)))))

La recursión de esta función es muy interesante, ya que se basa en una recursión mútua.

La función (to-list-tree tree) obtiene la lista de hijos del nodo actual y llama a la
función (to-list-bosque) con ella. Esta función devuelve una lista plana con todos los
datos del bosque que le pasamos como parámetro. Sólo falta entonces añadir el dato del
árbol inicial.

A su vez, la función to-list-bosque llama a la función to-list-tree para obtener la


lista con los datos del primer árbol, se llama a si misma para obtener el bosque con el
resto de árboles de la lista y concatena ambas listas llamando a la función append.
(cuadrado-tree tree)

La función cuadrado-tree devuelve el árbol resultante de aplicar la


función cuadrado a los números del árbol que se pasa como parámetro.

Es importante notar, al igual que hacíamos cuando hablábamos de árboles binarios, que
el árbol resultante es una copia del árbol que se pasa como parámetro y que no se
pueden modificar directamente los datos del árbol original porque nos encontramos en el
paradigma de programación funcional.

(define (cuadrado-tree tree)


(make-tree (cuadrado (dato-tree tree))
(cuadrado-bosque (hijos-tree tree))))

(define (cuadrado-bosque bosque)


(if (null? bosque)
'()
(cons (cuadrado-tree (car bosque))
(cuadrado-bosque (cdr bosque)))))

Ambas funciones tienen un patrón muy similar de recursión mutua al usado en la función
anterior tree-to-list.

Otra versión muy interesante y compacta es la siguiente:

(define (cuadrado-tree tree)


(make-tree (cuadrado (dato-tree tree))
(map cuadrado-tree (hijos-tree tree))))

La recursión se basa en aplicar la propia función a cada uno de los hijos. Para ello se
obtiene la lista de hijos con la función hijos-tree y se usa la función map para aplicar la
propia función a todos los elementos de esa lista (que son árboles). La lista de árboles
resultante se utiliza para construir el árbol que se devuelve añadiendo como dato el
cuadrado del dato original.

map-tree

Por último, la función map-tree es la generalización de la función cuadrado-


tree anterior y permite aplicar una función cualquiera de un argumento a todos los datos
del árbol inicial, devolviendo el árbol resultante.

(define (map-tree f tree)


(make-tree (f (dato-tree tree))
(map-bosque f (hijos-tree tree))))

(define (map-bosque f bosque)


(if (null? bosque)
'()
(cons (map-tree f (car bosque))
(map-forest f (cdr bosque)))))

Lenguajes y Paradigmas de Programación


Curso 2010-2011
Departamento de Ciencia de la Computación e Inteligencia Artificial
Universidad de Alicante

Sitio web realizado con org-mode y el estilo CSS del proyecto Worg

Validate XHTML 1.0


INTRODUCCIÓN
El área de la programación es muy amplia y con muchos detalles. Los programadores necesitan ser capaces
de resolver todos los problemas que se les presente a través del computador aun cuando en el lenguaje que
utilizan no haya una manera directa de resolver los problemas. En el lenguaje de programación C, así como
en otros lenguajes de programación, se puede aplicar una técnica que se le dio el nombre de recursividad por
su funcionalidad. Esta técnica es utilizada en la programación estructurada para resolver problemas que
tengan que ver con el factorial de un número, o juegos de lógica. Las asignaciones de memoria pueden ser
dinámicas o estáticas y hay diferencias entre estas dos y se pueden aplicar las dos en
un programa cualquiera.
1.-Recursividad:
La recursividad es una técnica de programación importante. Se utiliza para realizar una llamada a
una función desde la misma función. 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 párrafo muestra una función, expresada con palabras, que calcula 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 y la iteración (ejecución en bucle) están muy relacionadas, cualquier acción que pueda
realizarse con la recursividad puede realizarse con iteración y viceversa. Normalmente, un cálculo
determinado se prestará a una técnica u otra, sólo necesita elegir el enfoque más natural o con el que se
sienta más cómodo.
Claramente, esta técnica puede constituir un modo de meterse en problemas. Es fácil crear una función
recursiva que no llegue a devolver nunca un resultado definitivo y no pueda llegar a un punto de finalización.
Este tipo de recursividad hace que el sistema ejecute lo que se conoce como bucle "infinito".
Para entender mejor lo que en realidad es el concepto de recursión veamos un poco lo referente a
la secuencia de Fibonacci.
Principalmente habría que aclarar que es un ejemplo menos familiar que el del factorial, que consiste en la
secuencia de enteros.
0,1,1,2,3,5,8,13,21,34,...,
Cada elemento en esta secuencia es la suma de los precedentes (por ejemplo 0 + 1 = 0, 1 + 1 = 2, 1 + 2 = 3,
2 + 3 = 5, ...) sean fib(0) = 0, fib (1) = 1 y así sucesivamente, entonces puede definirse la secuencia de
Fibonacci mediante la definición recursiva (define un objeto en términos de un caso mas simple de si
mismo):
fib (n) = n if n = = 0 or n = = 1
fib (n) = fib (n - 2) + fib (n - 1) if n >= 2
Por ejemplo, para calcular fib (6), puede aplicarse la definición de manera recursiva para obtener:
Fib (6) = fib (4) + fib (5) = fib (2) + fib (3) + fib (5) = fib (0) + fib (1) + fib (3) + fib (5) = 0 + 1 fib (3) + fib (5)
1. + fib (1) + fib (2) + fib(5) =

1. + 1 + fib(0) + fib (1) + fib (5) =


2. + 0 + 1 + fib(5) = 3 + fib (3) + fib (4) =

3 + 1 + fib (0) + fib (1) + fib (4) =


3. + fib (1) + fib (2) + fib (4) =
4. + 0 + 1 + fib (2) + fib (3) = 5 + fib (0) + fib (1) + fib (3) =
5. + 0 + 1 + fib (1) + fib (2) = 6 + 1 + fib (0) + fib (1) =
6. +0+1=8

Obsérvese que la definición recursiva de los números de Fibonacci difiere de las definiciones recursivas de la
función factorial y de la multiplicación . La definición recursiva de fib se refiere dos veces a sí misma . Por
ejemplo, fib (6) = fib (4) + fib (5), de tal manera que al calcular fib (6), fib tiene que aplicarse de manera
recursiva dos veces. Sin embargo calcular fib (5) también implica calcular fib (4), así que al aplicar la definición
hay mucha redundancia de cálculo. En ejemplo anterior, fib(3) se calcula tres veces por separado. Sería
mucho mas eficiente "recordar" el valor de fib(3) la primera vez que se calcula y volver a usarlo cada vez que
se necesite. Es mucho mas eficiente un método iterativo como el que sigue parar calcular fib (n).
If (n < = 1)
return (n);
lofib = 0 ;
hifib = 1 ;
for (i = 2; i < = n; i ++)
{
x = lofib ;
lofib = hifib ;
hifib = x + lofib ;
} /* fin del for*/
return (hifib) ;
Compárese el numero de adiciones (sin incluir los incrementos de la variable índice, i) que se ejecutan para
calcular fib (6) mediante este algoritmo al usar la definición recursiva. En el caso de la función factorial, tienen
que ejecutarse el mismo numero de multiplicaciones para calcular n! Mediante ambos métodos: recursivo e
iterativo. Lo mismo ocurre con el numero de sumas en los dos métodos al calcular la multiplicación. Sin
embargo, en el caso de los números de Fibonacci, el método recursivo es mucho mas costoso que el iterativo.
2.- Propiedades de las definiciones o algoritmos recursivos:
Un requisito importante para que sea correcto un algoritmo recursivo es que no genere una secuencia infinita
de llamadas así mismo. Claro que cualquier algoritmo que genere tal secuencia no termina nunca. Una
función recursiva f debe definirse en términos que no impliquen a f al menos en un argumento o grupo de
argumentos. Debe existir una "salida" de la secuencia de llamadas recursivas.
Si en esta salida no puede calcularse ninguna función recursiva. Cualquier caso de definición recursiva o
invocación de un algoritmo recursivo tiene que reducirse a la larga a alguna manipulación de uno o casos mas
simples no recursivos.
3.- Cadenas recursivas:
Una función recursiva no necesita llamarse a sí misma de manera directa. En su lugar, puede hacerlo de
manera indirecta como en el siguiente ejemplo:
a (formal parameters) b (formal parameters)
{{
..
b (arguments); a (arguments);
..
} /*fin de a*/ } /*fin de b*/
En este ejemplo la función a llama a b, la cual puede a su vez llamar a a, que puede llamar de nuevo a b. Así,
ambas funciones a y b, son recursivas, dado que se llamas a sí mismo de manera indirecta. Sin embargo, el
que lo sean no es obvio a partir del examen del cuerpo de una de las rutinas en forma individual. La rutina a,
parece llamar a otra rutina b y es imposible determinar que se puede llamar así misma de manera indirecta al
examinar sólo a a.
Pueden incluirse mas de dos rutinas en una cadena recursiva. Así, una rutina a puede llamar a b, que llama
a c, ..., que llama a z, que llama a a. Cada rutina de la cadena puede potencialmente llamarse a sí misma y,
por lo tanto es recursiva. Por supuesto, el programador debe asegurarse de que un programa de este tipo no
genere una secuencia infinita de llamadas recursivas.
4.- Definición recursiva de expresiones algebraicas:
Como ejemplo de cadena recursiva consideremos el siguiente grupo de definiciones:
a. una expresión es un término seguido por un signo mas seguido por un término, o un término solo
b. un término es un factor seguido por un asterisco seguido por un factor, o un factor solo.
c. Un factor es una letra o una expresión encerrada entre paréntesis.

Antes de ver algunos ejemplos, obsérvese que ninguno de los tres elementos anteriores está definido en
forma directa en sus propios términos. Sin embargo, cada uno de ellos se define de manera indirecta. Una
expresión se define por medio de un término, un término por medio de un factor y un factor por medio de una
expresión. De manera similar, se define un factor por medio de una expresión, que se define por medio de un
término que a su vez se define por medio de un factor. Así, el conjunto completo de definiciones forma una
cadena recursiva.
La forma mas simple de un factor es una letra. Así A, B, C, Q, Z y M son factores. También son términos,
dado que un término puede ser un factor solo. También son expresiones dado que una expresión puede ser
un término solo. Como A es una expresión, (A) es un factor y, por lo tanto, un término y una expresión. A + B
es un ejemplo de una expresión que no es ni un término ni un factor, sin embargo (A + B) es las tres cosas. A
* B es un término y, en consecuencia, una expresión, pero no es un factor. A * B + C es una expresión, pero
no es un factor.
Cada uno de los ejemplos anteriores es una expresión valida. Esto puede mostrarse al aplicar la definición de
una expresión de cada uno. Considérese, sin embargo la cadena A + * B. No es ni una expresión, ni un
término, ni un factor. Sería instructivo para el lector intentar aplicar la definición de expresión, término y factor
para ver que ninguna de ellas describe a la cadena A + * B. De manera similar, (A + B*) C y A + B + C son
expresiones nulas de acuerdo con las definiciones precedentes.
A continuación se codificará un programa que lea e imprima una cadena de caracteres y luego imprima
"valida" si la expresión lo es y "no valida" de no serlo. Se usan tres funciones para reconocer expresiones,
términos y factores, respectivamente. Primero, sin embrago se presenta una función auxiliar getsymb que
opera con tres parámetros: str, length y ppos. Str contiene la entrada de la cadena de cadena de
caracteres; length representa el número de caracteres en str. Ppos apunta a un puntero pos cuyo valor es la
posición str de la que obtuvimos un carácter la ultima vez. Si pos < length, getsymb regresa el
carácter cadena str [pos] e incrementa pos en 1. Si pos > = length, getsymb regresa un espacio en blanco.
getsymb (str, length, ppos)
char str[];
int length, *ppos;
{
char C;
if (*ppos < length)
c = str [*ppos];
else
c=‘‘;
(*ppos) ++;
return ( c );
} /* fin de getsymb*/
La función que reconoce una expresión se llama expr. Regresa TRUE (o1) (VERDADERO) si una expresión
valida comienza en la posición pos de str y FALSE (o0) FALSO en caso contrario. También vuelve a
colocar pos en la posición que sigue en la expresión de mayor longitud que puede encontrar. Suponemos
también una función readstr que lee una cadena de caracteres, poniendo la cadena en str y su largo
en length.
Una vez descritas las funciones expr y readst, puede escribirse la rutina principal como sigue.
La biblioteca estándar ctype.h incluye una función isalpha que es llamada por una de las funciones siguientes.
# include <stdio.h>
# include <ctype.h>
# define TRUE 1
# define FALSE =
# define MAXSTRINGSIZE 100
main ()
{
char str [MAXSTRINGSIZE];
int length, pos;
readstr (str, &length);
pos = 0;
if (expr (str, length, &pos) = = TRUE && por >= length)
printf ("%s", "valida");
else
printf ("%s", "no valida");
/* la condición puede fallar por una de dos razones (o ambas). Si expr(str, length, &pos) = = FALSE entonces
no hay una expresión valida al inicio de pos. Si pos < length puede que se encuentre una expresión valida,
comenzando en pos, pero no ocupa la cadena completa */
} /*fin del main*/
Las funciones factor y term se parecen mucho a expr excepto en que son responsables del reconocimiento de
factores y término, respectivamente. También reinicializan pos en la posición que sigue al factor o término de
mayor longitud que se encuentra en la cadena str.
Los códigos para estas rutinas se apegan bastantes a las definiciones dadas antes. Cada una intenta
satisfacer uno de los criterios para la entidad que se reconoce. Si se satisface uno de esos criterios el
resultado es TRUE (VERDADERO). Si no satisface ninguno, el resultado es FALSE (FALSO).
expr (str. length, ppos)
char str [];
int length, *ppos;
{
/* buscando un término */
if (term( str, length, ppos) = = FLASE)
return (FLASE);
/* se ha encontrado un término; revisar el siguiente símbolo */
if (getsymb(str, length, ppos) ! = ‘+’) {
/* se encontró la mayor expresión (un solo término). Reposicionar pos para que señale la última posición de la
expresión */
(*ppos) - - ;
return (TRUE);
} /* fin del if */
/* en este punto, se a encontrado un termino y su signo mas. Se deberá buscar otro termino */
return (term(str, length, ppos));
} /*fin de expr */
La rutina term que reconoce términos, es muy similar y será presentada sin comentarios.
term (str, length, ppos)
char str[];
int length, *ppos;
{
if (factor(str, length, ppos) = = FALSE)
return (FALSE);
if (getsymb (str, length, ppos) ! = ‘+’) {
(*ppos) -- ;
return (TRUE) ;
} /* fin del if */
return (factor(str, length, ppos));
} /* fin de term */
La función factor reconoce factores y debería ser ahora bastante sencilla. Usa el programa común de
biblioteca isalpha (esta función se encuentra en la biblioteca ctype.h), que regresa al destino de cero si su
carácter de parámetro es una letra y cero (o FALSO) en caso contrario.
factor (str, length, ppos)
char str[];
int length, *ppos;
{
int c;
if ((c = getsymb (str, length, ppos)) ! = ‘)’ )
return (isalpha(c));
return (expr(str, length, ppos) && getsymb (str, length, ppos) == ‘)’ );
} /* fin de factor */
Las tres rutinas son recursivas, dado que cada una puede llamar a sí misma da manera indirecta. Pos
ejemplo, si se sigue la acción del programa para la cadena de entrada " (a * b + c * d) + (e * (f) + g) " se
encontrará que cada una de las tres rutinas expr, term y factor se llama a sí misma.
5.- Programación Recursiva:
Es mucho mas difícil desarrollar una solución recursiva en C para resolver un problema especifico cuando no
se tiene un algoritmo. No es solo el programa sino las definiciones originales y los algoritmos los que deben
desarrollarse. En general, cuando encaramos la tarea de escribir un programa para resolver un problema no
hay razón para buscar una solución recursiva. La mayoría de los problemas pueden resolverse de una
manera directa usando métodos no recursivos. Sin embargo, otros pueden resolverse de una manera mas
lógica y elegante mediante la recursión.
Volviendo a examinar la función factorial. El factor es, probablemente, un ejemplo fundamental de un
problema que no debe resolverse de manera recursiva, dado que su solución iterativa es directa y simple. Sin
embargo, examinaremos los elementos que permiten dar una solución recursiva. Antes que nada, puede
reconocerse un gran número de casos distintos que se deben resolver. Es decir, quiere escribirse un
programa para calcular 0!, 1!, 2! Y así sucesivamente. Puede identificarse un caso "trivial" para el cual la
solución no recursiva pueda obtenerse en forma directa. Es el caso de 0!, que se define como 1. El siguiente
paso es encontrar un método para resolver un caso "complejo" en términos de uno mas "simple", lo cual
permite la reducción de un problema complejo a uno mas simple. La transformación del caso complejo al
simple resultaría al final en el caso trivial. Esto significaría que el caso complejo se define, en lo fundamental,
en términos del mas simple.
Examinaremos que significa lo anterior cuando se aplica la función factorial. 4! Es un caso mas complejo que
3!. La transformación que se aplica al numero a para obtener 3 es sencillamente restar 1. Si restamos 1 de 4
de manera sucesiva llegamos a 0, que es el caso trivial. Así, si se puede definir 4! en términos de 3! y, en
general, n! en términos de (n – 1)!, se podrá calcular 4! mediante la definición de n! en términos de (n – 1)! al
trabajar, primero hasta llegar a 0! y luego al regresar a 4!. En el caso de la función factorial se tiene una
definición de ese tipo, dado que:
n! = n * (n – 1)!
Asi, 4! = 4 * 3! = 4 * 3 * 2! = 4 * 3 * 2 * 1! = 4 * 3 * 2 * 1 * 0! = 4 * 3 * 2] * ] = 24
Estos son los ingredientes esenciales de una rutina recursiva: poder definir un caso "complejo" en términos de
uno más "simple" y tener un caso "trivial" (no recursivo) que pueda resolverse de manera directa. Al hacerlo,
puede desarrollarse una solución si se supone que se ha resuelto el caso más simple. La versión C de la
función factorial supone que esta definido (n –1)! y usa esa cantidad al calcular n!.
Otra forma de aplicar estas ideas a otros ejemplos antes explicados. En la definición de a * b, es trivial el caso
de b = 1, pues a * b es igual a a. En general, a + b puede definirse en términos de a * (b – 1) mediante la
definición a * b = a * (b – 1) + a. De nuevo, el caso complejo se transforma en un caso mas simple al restar 1,
lo que lleva, al final, al caso trivial de b = 1. Aquí la recursión se basa únicamente en el segundo parámetro, b.
Con respecto al ejemplo de la función de Fibonacci, se definieron dos casos triviales: fib(0) = 0 y fib(1) = 1. Un
caso complejo fib(n) se reduce entonces a dos más simples: fib(n –1) y fib(n –2). Esto se debe a la definición
de fib(n) como fib(n –1) + fib(n – 2), donde se requiere de dos casos triviales definidos de manera directa.
Fib(1) no puede definirse como fib(0) + fib(-1) porque la función de Fibonacci no está definida para números
negativos.
6.- Asignación estática y dinámica de memoria:
Hasta este momento solamente hemos realizado asignaciones estáticas del programa, y más concretamente
estas asignaciones estáticas no eran otras que las declaraciones de variables en nuestro programa. Cuando
declaramos una variable se reserva la memoria suficiente para contener la información que debe almacenar.
Esta memoria permanece asignada a la variable hasta que termine la ejecución del programa (función main).
Realmente las variables locales de las funciones se crean cuando éstas son llamadas pero nosotros no
tenemos control sobre esa memoria, el compilador genera el código para esta operación automáticamente.
En este sentido las variables locales están asociadas a asignaciones de memoria dinámicas, puesto que se
crean y destruyen durante la ejecución del programa.
Así entendemos por asignaciones de memoria dinámica, aquellas que son creadas por nuestro programa
mientras se están ejecutando y que por tanto, cuya gestión debe ser realizada por el programador.
El lenguaje C dispone, como ya indicamos con anterioridad, de una serie de librerías de funciones estándar.
El fichero de cabeceras stdlib.h contiene las declaraciones de dos funciones que nos permiten reservar
memoria, así como otra función que nos permite liberarla.
Las dos funciones que nos permiten reservar memoria son:
 malloc (cantidad_de_memoria);
 calloc (número_de_elementos, tamaño_de_cada_elemento);

Estas dos funciones reservan la memoria especificada y nos devuelven un puntero a la zona en cuestión. Si
no se ha podido reservar el tamaño de la memoria especificado devuelve un puntero con el valor 0 o NULL. El
tipo del puntero es, en principio void, es decir, un puntero a cualquier cosa. Por tanto, a la hora de ejecutar
estás funciones es aconsejable realizar una operación cast (de conversión de tipo) de cara a la utilización de
la aritmética de punteros a la que aludíamos anteriormente. Los compiladores modernos suelen realizar esta
conversión automáticamente.
Antes de indicar como deben utilizarse las susodichas funciones tenemos que comentar el operador sizeof.
Este operadores imprescindible a la hora de realizar programas portables, es decir, programas que puedan
ejecutarse en cualquier máquina que disponga de un compilador de C.
El operador sizeof (tipo_de_dato), nos devuelve el tamaño que ocupa en memoria un cierto tipo de dato, de
esta manera, podemos escribir programas independientes del tamaño de los datos y de la longitud de palabra
de la máquina. En resumen si no utilizamos este operador en conjunción con las conversiones de tipo cast
probablemente nuestro programa sólo funciones en el ordenador sobre el que lo hemos programado.
Por ejemplo, el los sistemas PC, la memoria está orientada a bytes y un entero ocupa 2 posiciones de
memoria, sin embargo puede que en otro sistema la máquina esté orientada a palabras (conjuntos de 2 bytes,
aunque en general una máquina orientada a palabras también puede acceder a bytes) y por tanto el tamaño
de un entero sería de 1 posición de memoria, suponiendo que ambas máquinas definan la misma precisión
para este tipo.
Con todo lo mencionado anteriormente veamos un ejemplo de un programa que reserva dinámicamente
memoria para algún dato.
#include <stdlib.h
#include <stdio.h>
main()
{
int *p_int;
float *mat;
p_int = (int *) malloc(sizeof(int));
mat = (float *)calloc(20,sizeof(float));
if ((p_int==NULL)||(mat==NULL))
{
printf ("\nNo hay memoria");
exit(1);
}
/* Aquí irían las operaciones sobre los datos */
/* Aquí iría el código que libera la memoria */
}
Este programa declara dos variables que son punteros a un entero y a un float. A estos punteros se le asigna
una zona de memoria, para el primero se reserva memoria para almacenar una variable entera y en el
segundo se crea una matriz de veinte elementos cada uno de ellos un float. Obsérvese el uso de los
operadores cast para modificar el tipo del puntero devuelto por malloc y calloc, así como la utilización del
operador sizeof.
Como se puede observar no resulta rentable la declaración de una variable simple (un entero, por ejemplo,
como en el programa anterior) dinámicamente, en primer lugar por que aunque la variable sólo se utilice en
una pequeña parte del programa, compensa tener menos memoria (2 bytes para un entero) que incluir todo el
código de llamada a malloc y comprobación de que la asignación fue correcta (esto seguro que ocupa más de
dos bytes).
En segundo lugar tenemos que trabajar con un puntero con lo cual el programa ya aparece un poco más
engorroso puesto que para las lecturas y asignaciones de las variables tenemos que utilizar el operador *.
Para termina un breve comentario sobre las funciones anteriormente descritas. Básicamente da lo mismo
utilizar malloc y calloc para reservar memoria es equivalente:
mat = (float *)calloc (20,sizeof(float));
mat = (float *)malloc (20*sizeof(float));
La diferencia fundamental es que, a la hora de definir matrices dinámicas calloc es mucho más claro y
además inicializa todos los elementos de la matriz a cero. Nótese también que puesto que las matrices se
referencian como un puntero la asignación dinámica de una matriz nos permite acceder a sus elementos con
instrucciones de la forma:
NOTA: En realidad existen algunas diferencias al trabajar sobre máquinas con alineamiento de palabras.
mat[0] = 5;
mat[2] = mat[1]*mat[6]/67;
Con lo cual el comentario sobre lo engorroso que resultaba trabajar con un puntero a una variable simple, en
el caso de las matrices dinámicas no existe diferencia alguna con una declaración normal de matrices.
La función que nos permite liberar la memoria asignada con malloc y calloc es free(puntero), donde puntero es
el puntero devuelto por malloc o calloc.
En nuestro ejemplo anterior, podemos ahora escribir el código etiquetado como: /* Ahora iría el código que
libera la memoria */
free (p_int);
free(mat);
Hay que tener cuidado a la hora de liberar la memoria. Tenemos que liberar todos los bloque que hemos
asignado, con lo cual siempre debemos tener almacenados los punteros al principio de la zona que
reservamos. Si mientras actuamos sobre los datos modificamos el valor del puntero al inicio de la zona
reservada, la función free probablemente no podrá liberar el bloque de memoria.
7.- Ejemplos:
7.1.- Las Torres de Hanoi:
A continuación se verá cómo pueden usarse técnicas recursivas para lograr una solución lógica y elegante de
un problema que no se especifica en términos recursivos. EL problema es el de "las torres de Hanoi", cuyo
planteamiento inicial se muestra en la figura a continuación...
Hay tres postes: A, B y C. En el poste A se ponen cinco discos de diámetro diferente de tal manera que un
disco de diámetro mayor siempre queda debajo de uno de diámetro menor. El objetivo es mover los discos al
poste C usando B como auxiliar. Sólo puede moverse el disco superior de cualquier poste a otro poste, y un
disco mayor jamás puede quedar sobre uno menor. Considérese la posibilidad de encontrar una solución. En
efecto, ni siquiera es claro que exista una.
Ahora se verá si se puede desarrollar una solución. En lugar de concentrar la atención en una solución para
cinco discos, considérese el caso general de n discos. Supóngase que se tiene una solución para n – 1 discos
y que en términos de ésta, se pueda plantear la solución para n – 1 discos. El problema se resolvería
entonces. Esto sucede porque en el caso trivial de un disco (al restar 1 de n de manera sucesiva se producirá,
al final, 1) la solución es simple: sólo hay que el único disco del poste A a C. Así se habrá desarrollado una
solución recursiva si se plantea una solución para n discos en términos de n – 1. Considérese la posibilidad de
encontrar tal relación. Para el caso de cinco discos en particular, supóngase que se conoce la forma de mover
cuatro de ellos del poste A al otro, de acuerdo con las reglas. ¿Cómo puede completarse entonces el
trabajo de mover el quinto disco? Cabe recordar que hay 3 postes disponibles.
Supóngase que se supo cómo mover cuatro discos del poste A al C. Entonces, se pondrá mover éstos
exactamente igual hacia B usando el C como auxiliar. Esto da como resultado la situación los cuatro primeros
discos en el poste B, el mayor en A y en C ninguno. Entonces podrá moverse el disco mayor de A a C y por
último aplicarse de nuevo la solución recursiva para cuatro discos para moverlo de B a C, usando el poste A
como auxilia. Por lo tanto, se puede establecer una solución recursiva de las torres de Hanoi como sigue:
Para mover n discos de A a C usando B como auxiliar:
1. Si n = = 1, mover el disco único de A a C y parar.
2. Mover el disco superior de A a B n – 1 veces, usando C como auxiliar.
3. Mover el disco restante de A a C.
4. Mover los disco n – 1 de B a C usando A como auxiliar

Con toda seguridad este algoritmo producirá una solución completa por cualquier valor de n. Si n = = , el paso
1 será la solución correcta. Si n = = 2, se sabe entonces que hay una solución para n – 1 = = 1, de manera tal
que los pasos 2 y 4 se ejecutaran en forma correcta. De manera análoga, cuando n = = 3 ya se habrá
producido una solución para n – 1 = = 2, por lo que los pasos 2 y 4 pueden ser ejecutados. De esta forma se
puede mostrar que la solución funciona para n = = 1, 2, 3, 4, 5,... hasta el valor para el que se desee encontrar
una solución. Adviértase que la solución se desarrollo mediante la identificación de un caso trivial (n = = 1) y
una solución para el caso general y complejo (n) en términos de un caso mas simple (n – 1).
Ya se demostró que las transformaciones sucesivas de una simulación no recursivas de una rutina recursiva
pueden conducir a un programa mas simple para resolver un problema. Ahora se simulara la recursión del
problema y se intentara simplificar la simulación no recursiva.
towers (n, frompeg, topeg, auxpeg)
int n;
char auxpeg, frompeg, topeg;
{
/* si es solo un disco, mover y regresar */
if (n = = 1) {
printf (" /n%s%c%s%c%", "mover disco 1 del poste",frompeg, "al poste", topeg);
return; } /* fin del if*/
/* mover los n – 1 discos de arriba de A a B, usando como auxiliar */
towers (n – 1, frompeg, auxpeg, tpoeg);
/* move remaining disk from A to C */
printf ("/n%s%d%s%c%s%c%", "mover disco", n, "del poste" frompeg, "al poste", topeg);
/* mover n – 1 discos de B hacia C empleando a A como auxiliar */
towers (n – 1, auxpeg, topeg, frompeg);} /* fin de towers */
En esta función, hay cuatro parámetros, cada uno de los cuales esta sujeto a cambios en cada llamada
recursiva. En consecuencia, el área de datos debe contener elementos que representen a los cuatro. No hay
variables locales. Hay un solo valor temporal que se necesita para guardar el valor de n – 1, pero esta se
puede representar por un valor temporal similar en el programa de simulación y no tiene que estar apilada.
Hay tres puntos posibles a los que regresa la función en varias llamadas: el programa de llamada y los dos
puntos que siguen a las llamadas recursivas. Por lo tanto, se necesitan cuatro etiquetas.
start:
Label1:
Label2:
Label3:
La dirección de regreso se codifica como un entero (1, 2 o 3) dentro de cada área de datos.
Considérese la siguiente simulación no recursiva de towers:
struct dataarea {
int nparam;
char fromparam;
char toparam;
char auxparam;
short int retaddr;
};
struct stack {
int top struct dataarea item [MAXSTACK]; };
simtowers (m, frompeg, topeg, auxpeg)
int n;
char auspeg, frompeg, topeg; {
struct stack s;
struct dataarea currarea;
char temp;
short int i;
s.top = -1;
currarea.nparam = 0;
currarea.fromparam = ‘ ‘ ;
currarea.toparam = ‘ ‘ ;
currarea. auxparam = ‘ ‘ ;
currarea.retaddr = 0;
/* colocar en la pila un área de datos simulados */
push (&s, &currarea);
/* asignar parámetros y direcciones de regreso del área de datos actual a sus valores apropiados */
currarea.nparam = n;
currarea,fromparam = frompeg;
currarea,toparam = topeg;
currarea.auxparam = auxpeg;
currarea.retaddr = 1;
start: /* Este es el inicio de la rutina simulada */
if (currarea.nparam = = 1) {
printf (" /n%s%c%s%c", "mover disco 1 del poste", currarea.fromparam, "al poste", currarea.toparam) ;
i = currarea.retaddr;
pop (&s, &currarea);
switch (i) {
case 1: goto label1;
case 2: goto label2;
case 3: goto label3; } /* fin del switch */
} /* fin del if */
/* Esta es la primera llamada recursiva */
push (&s, &currarea);
-- currarea.nparam;
temp = currarea.auxparam;
currarea.auxparam = currarea.toparam;
currarea.toparam = temp;
currarea.retaddr = 2;
got start;
label2: /* se regresa a este punto desde la primera llamada recursiva */
printf ("/n%s%d%s%c%s%c", "mover disco", currarea.nparam, "del poste", currarea.fromparam, "al poste",
currarea.toparam);
/* Esta es la segunda llamada recursiva */
push (&s, &currarea);
--currarea.nparam;
temp = currarea.fromparam;
currarea.fromparam = currarea.auxparam;
currarea.auxparam = temp;
currarea.rtaddr = 3;
got start;
label3: /* se regresa a este punto desde la segunda llamada recursiva */
i = currarea.retaddr;
pop (&s, &currarea);
swicth (i) {
case 1: goto label1;
case 2: goto label2;
case 3: goto label3; } /* fin del switch */
label1: return;
} /* fin de simtowers */
Ahora se simplificará el programa. En primer lugar, debe observarse que se usan tres etiquetas para indicar
direcciones de regreso; una para cada una de las dos llamadas recursivas y otra para el regreso al programa
principal. Sin embargo, el regreso al programa principal puede señalarse por un subdesborde en la pila, de la
misma for que en la segunda versión simfact. Esto deja dos etiquetas de regreso. Si pudiera eliminarse otra
mas, sería innecesario guardar en la pila la dirección de regreso, ya que solo restaría un punto al que se
podría transferir el control si se eliminan los elementos de la pila con éxito. Ahora dirijamos nuestra atención a
la segunda llamada recursiva y a la instrucción:
towers (n – 1, auxpeg, topeg, frompeg);
Las acciones que ocurren en la simulación de esta llamada son las siguientes:
1. Se coloca el área de datos vigente a 1 dentro de la pila.
2. En la nueva área de datos vigente a 2, se asignan los valores respectivos n – 1, auxpeg, topeg, y frompeg
a los parámetros.
3. En el área de datos vigente a 2, se fija la etiqueta de retorno a la dirección de la instrucción que sigue de
inmediato a la llamada.
4. Se salta hacia el principio de la rutina simulada.
5. Después de completar la rutina simulada, ésta queda lista parar regresar. Las siguientes acciones se
llevan a efecto:
6. Se salva la etiqueta de regreso, /, de área de datos vigentes a 2.
7. Se eliminan de la pila y se fija el área de datos vigente como el área de datos eliminada de la pila, a 1.
8. Se transfiere el control a /.

Sin embrago, / es la etiqueta del final del bloque del programa ya que la segunda llamada a towers aparece en
la última instrucción de la función. Por lo tanto, el siguiente paso es volver a eliminar elementos de la pila y
regresar. No se volverá a hacer uso de la información del área de datos vigente a 1, ya que ésta es destruida
en la eliminación de los elementos en la pila tan pronto como se vuelve a almacenar. Puesto que no hay razón
para volver a usar esta área de datos, tampoco hay razón para salvarla en la pila durante la simulación de la
llamada. Los datos se deben salvar en la pila sólo si se van a usar otra vez. En consecuencia, la seguida
llamada a towers puede simularse en forma simple mediante:
1. El cambio de los parámetros en el área de datos vigente a sus valores respectivos.
2. El "salto" al principio de la rutina simulada.

Cuando la rutina simulada regresa puede hacerlo en forma directa a la rutina que llamó a la versión vigente.
No hay razón para ejecutar un regreso a la versión vigente, sólo para regresar de inmediato a la versión
previa. Por lo tanto, se elimina la necesidad de guardar en la pila la dirección de regreso al simular la llamada
externa (ya que se puede señalar mediante subdesborde y simular la segunda llamada recursiva, ya que no
hay necesidad de salvar y volver a almacenar al área de datos de la rutina que llamada en este momento). La
única dirección de regreso que resta es la que sigue a la primera llamada recursiva.
Ya que sólo queda una dirección de regreso posible, no tiene caso guardarla en la pila para que se vuelva a
insertar y eliminar con el resto de los datos. Siempre se eliminan elementos de la pila con éxito, hay una solo
dirección hacia la que se puede ejecutar un "salto" (la instrucción que sigue a la primera llamada). Como los
nuevos valores de las variables del área de datos vigente se obtendrán a partir de los datos antiguos de área
de datos vigente, es necesario declarar una variable adicional, temp, de manera que los valores sean
intercambiables.
7.2.- El Problema de las Ocho Reinas:
El problema de las ocho reinas y en general de las N reinas, se basa en colocar 8 reinas en un tablero de 8´
8 (o N en un tablero de NxN, si el problema se generaliza), de forma que en no puede haber dos piezas en la
misma línea horizontal, vertical o diagonal, ver Figura 1.
Para ver el gráfico seleccione la opción "Descargar" del menú superior
Figura 1
Posible solución para el problema de las ocho reinas
Este programa has sido muy estudiado por los matemáticos. Se trata de un problema NP-Completo que no
tiene solución para N=2 y N=3. Para N=4 tiene una única solución. Para N=8 hay más de
80 soluciones dependiendo de las restricciones que se impongan.
Una forma de abordar el problema se lleva a cabo mediante la construcción de un predicado en Prolog del tipo
siguiente: reinas (N, Solución), donde N representa las dimensiones del tablero y el número de reinas y
Solución es una lista que contiene la permutación de la lista de números que resuelven el problema. Los
índices de dicha lista representan la columna en la que se encuentra una reina y el número que almacena la
posición o índice representa la fila donde la reina está colocada. Así, para el ejemplo mostrado en la Figura 1,
tenemos que R=[2,4,1,3].
Este problema es resuelto, de modo clásico, por el método de prueba y error, luego se adapta perfectamente
a un algoritmo de backtracking. Básicamente, el problema se reduce a colocar una reina, e intentar repetir
el proceso teniendo en cuenta la reina colocada. Si logramos poner todas las reinas el problema se ha
resuelto, en caso contrario, deshacemos lo que llevamos hasta ahora y probamos con otra combinación. Por
tanto, hemos de generar un conjunto de permutaciones y seleccionar aquella que cumpla los
requisitos impuestos por el juego.
Veamos el código que resuelve el problema:
rango(N, N, [N]).
rango(N, M, [N|Cola]):-N<M, Aux is N+1, rango(Aux, M, Cola).
dame(X,[X|Xs],Xs).
dame(X,[Y|Ys],[Y|Zs]):-dame(X,Ys,Zs).
permuta([],[]).
permuta(L,[Z|Zs]):-dame(Z,L,Ys), permuta(Ys,Zs).
atacada(X,Y):-atacada(X,1,Y).
atacada(X,N,[Y|Ys]):-X is Y+N; X is Y-N.
atacada(X,N,[Y|Ys]):-N1 is N+1, atacada(X,N1,Ys).
correcta([]).
correcta([X|Y]):-correcta(Y), not atacada(X,Y).
reina(N, Solucion):-rango(1,N,L1), permuta(L1,Solucion), correcta(Solucion).
Es muy importante comprender como funciona cada uno de los predicados para entender el funcionamiento
del algoritmo general.
Prolog permite implementar los programas casi directamente a partir de las especificaciones realizadas a
partir de un análisis y diseño de la solución desde un alto nivel de abstracción. Además el procedimiento de
backtracking está implícito en el propio motor de inferencia, luego este paradigma se adapta perfectamente a
nuestro problema.
Si analizamos y diseñamos nuestro problema tenemos que la forma de resolverlo se resume en los pasos
siguientes:
Para N, obtenemos una lista de números comprendidos entre 1 y N: [1,2,3,4,...,N].
Obtenemos una permutación del conjunto de números de la lista.
Comprobamos que la permutación es correcta.
Si la permutación no es correcta, lo que debemos hacer es volver al paso 2 para generar una permutación
nueva.
Comencemos a analizar la solución implementada. El problema se resuelve con el predicado reina(N,
Solución): rango(1,N,L1), permuta(L1,Solucion), correcta Solución).Como vemos es, sencillamente, una copia
de las especificaciones realizadas más arriba. Se genera el rango entre 1 y N, se obtiene una permutación y
se comprueba si la permutación es, o no, correcta. En el caso de que cualquier predicado del consecuente
falle, la propia máquina Prolog se encarga de realizar el proceso de backtracking. Con lo cual ya tenemos
cubiertos los cuatro pasos fundamentales del algoritmo. Para tener más claras las ideas, observemos el árbol
de ejecución general del objetivo reina(4,Solucion) en la Figura 2.
Figura 2
Árbol de ejecución para el objetivo reina(4,Solucion)
He aquí otro ejemplo sobre el problema de las ocho reinas. Primero se mostrara un pseudocodigo sobre el
mismo y luego su respectiva codificación en el lenguaje C.
<PRINCIPIO ensayar> (i: entero)
inicializar el conjunto de posiciones de la reina i-ésima
+-REPETIR hacer la selección siguiente
| +-SI segura ENTONCES
| | poner reina
| | +-SI i < 8 ENTONCES
| | | LLAMAR ensayar (i + 1)
| | | +-SI no acertado ENTONCES
| | | | quitar reina
| | | +-FINSI
| | +-FINSI
| +-FINSI
+-HASTA acertada O no hay más posiciones
<FIN>
Observaciones sobre el código:
1) Estudiar la función ensayar() a partir de este pseudocódigo.
2) Vectores utilizados:
int posiciones_en_columna[8]; RANGO: 1..8
BOOLEAN reina_en_fila[8]; RANGO: 1..8
BOOLEAN reina_en_diagonal_normal[15]; RANGO: -7..7
BOOLEAN reina_en_diagonal_inversa[15]; RANGO: 2..16
En C, el primer elemento de cada vector tiene índice 0, esto
es fácil solucionarlo con las siguientes macros:
#define c(i) posiciones_en_columna[(i)-1]
#define f(i) reina_en_fila[(i)-1]
#define dn(i) reina_en_diagonal_normal[(i)+7]
#define di(i) reina_en_diagonal_inversa[(i)-2]
Significado de los vectores:
c(i) : la posición de la reina en la columna i
f(j) : indicativo de que no hay reina en la fila j-ésima
dn(k): indicativo de que no hay reina en la diagonal normal
(\) k-ésima
di(k): indicativo de que no hay reina en la diagonal
invertida (/) k-ésima
Dado que se sabe, por las reglas del ajedrez, que una reina actúa sobre todas las piezas situadas en la
misma columna, fila o diagonal del tablero se deduce que cada columna puede contener una y sólo una reina,
y que la elección de la situación de la reina i-ésima puede restringirse a los cuadros de la columna i. Por tanto,
el parámetro i se convierte en el índice de columna, y por ello el proceso de selección de posiciones queda
limitado a los ocho
posibles valores del índice de fila j.
A partir de estos datos, la línea poner reina del pseudocódigo es:
c (i) = j; f (j) = di (i + j) = dn (i - j) = FALSE;
y la línea quitar reina del pseudocódigo:
f (j) = di (i + j) = dn (i - j) = TRUE;
y la condición segura del pseudocódigo:
f (i) && di (i + j) && dn (i - j)
/* Ficheros a incluir: */
#include <stdio.h> /* printf () */
#include <conio.h> /* getch () */
/* Macros: */
#define BOOLEAN int
#define TRUE 1
#define FALSE 0
/* Variables globales: */
BOOLEAN acertado;
int posiciones_en_columna[8];
BOOLEAN reina_en_fila[8];
BOOLEAN reina_en_diagonal_normal[15];
BOOLEAN reina_en_diagonal_inversa[15];
#define c(i) posiciones_en_columna[(i)-1]
/* rango de índice: 1..8 */
#define f(i) reina_en_fila[(i)-1]
/* rango de índice: 1..8 */
#define dn(i) reina_en_diagonal_normal[(i)+7]
/* rango de índice: -7..7 */
#define di(i) reina_en_diagonal_inversa[(i)-2]
/* rango de índice: 2..16 */
/* Prototipos de las funciones: */
void proceso (void);
void ensayar (int i);
/* Definiciones de las funciones: */
void main (void)
{
printf ("\n\nPROBLEMA DE LAS OCHO REINAS:\n ");
proceso ();
printf ("\n\nPulsa cualquier tecla para finalizar. ");
getch ();
}
void proceso (void)
{
register int i,j;
for (i = 1; i <= 8; i++)
f (i) = TRUE;
for (i = 2; i <= 16; i++)
di (i) = TRUE;
for (i = -7; i <= 7; i++)
dn (i) = TRUE;
ensayar (1);
if (acertado)
for (printf ("\n\nLA SOLUCION ES:\n\n"), i = 1; i <= 8; i++)
{
for (j = 1; j <= 8; j++)
printf ("%2d", c (j) == i ? 1 : 0);
printf ("\n");
}
else
printf ("\n\nNO HAY SOLUCION.\n");
}
void ensayar (int i)
{
int j = 0;
do
{
j++;
acertado = FALSE;
if (f (j) && di (i + j) && dn (i - j))
{
c (i) = j;
f (j) = di (i + j) = dn (i - j) = FALSE;
if (i < 8)
{
ensayar (i + 1);
if (! acertado)
f (j) = di (i + j) = dn (i - j) = TRUE;
}
else
acertado = TRUE;
}
} while (! acertado && j != 8);
favorito

1
Tengo que crear una función que elimine de una lista todos los elementos con dato numérico x.
Mi Código:

struct nodolista {
int elem;
nodolista *sig;
};

typedef nodolista *lista;

void remover(int x, lista l) {


if (l != NULL) {
remover(x, l->sig);
if (l->elem == x)
delete l;
}
}
Pero no funciona correctamente, ya que cuando borra un elemento, el anterior a ese no se
conecta con su siguiente. Esto lo sé hacer con modo iterativo, pero no de manera recursiva.
c++

compartirmejorar esta pregunta


editada el 7 abr. a las 2:57

Mariano
19.1k124678
formulada el 6 abr. a las 22:27

Edu
1119
añade un comentario
1 respuesta
activasmás antiguas votos

voto a favor1votar en contra aceptada

Haz la llamada recursiva que devuelva el siguiente, de este modo a la vuelta de la llamada
recursiva lo puedes asignar al actual si no lo has eliminado. Es decir, al ir llamando al siguiente:

 Si es null devuelves null para asignarlo al anterior.


 Si es el que tienes que eliminar, asignas el siguiente a éste a un temporal (ya que lo vas a
eliminar), lo eliminas, y haces la llamada recursiva con el siguiente.
 Si no lo tienes que eliminar asignas como el siguiente lo que devuelva la llamada recursiva
del siguiente a éste.
O dicho de una forma más resumida:

 Si no eliminas el siguiente -> lo asignas.


 Si eliminas el siguiente -> asignas el siguiente que no vayas a eliminar (o null si ya no
hay mas).
Te dejo el código de la función recursiva:

lista remover(int x, lista l) {

if (l == NULL) {
return l; // Devolvemos NULL
} else {
if (l->elem == x) {
lista tmp = l->sig; // Asignamos el siguiente a un temporal.
delete l; // Eliminamos el actual.
return remover(x, tmp); // Devolvemos lo que devuelva la llamada recursiva con el siguiente
}
else {
l->sig = remover(x, l->sig); // Asignamos lo que devuelva la llamada recursiva con el siguiente.
return l; // Devolvemos el actual;
}
}
}
Espero haberme explicado, sino no dudes en comentar la respuesta e intendo explicarme mejor.

Como comenta @Xam Puedes usar nullptr en vez de null a partir de C++11.
compartirmejorar esta respuesta
editada el 7 abr. a las 0:16
respondida el 7 abr. a las 0:10
WyrncaelLeer
más: http://www.monografias.com/trabajos14/recursividad/recursividad.shtml#ixzz5FWom7YZG

Vous aimerez peut-être aussi