Capítulo 15 – Estructuras de Datos en C

March 23, 2018 | Author: Junior Ama Vera | Category: Queue (Abstract Data Type), Computer Program, Compiler, Programming Language, Data Type
Share Embed Donate


Short Description

Download Capítulo 15 – Estructuras de Datos en C...

Description

CAPÍTULO 16 ESTRUCTURAS DE DATOS EN C Los tipos de datos provistos por los lenguajes de programación de alto nivel son herramientas provistas por el lenguaje y que le permiten al programador representar eventos del problema que está resolviendo. Por ejemplo en un programa que calcule el crecimiento de una inversión, podemos tener una variable entera para modelar el número de meses transcurridos desde que se realizó la inversión. También podemos usar una variable flotante para modelar el capital acumulado de dicha inversión. Los tipos de datos mencionados constituyen una implementación restringida de las entidades matemáticas que son los números enteros y reales. Son restringidas por que sólo nos permiten representar un subrango de los números enteros o reales. La definición de un tipo de dato no sólo está formada por el rango de valores que pueden representarse con ese tipo sino también por las operaciones que pueden realizarse con datos de ese tipo. Por ejemplo el tipo entero define las operaciones de negativo, suma, resta, multiplicación, división entera y módulo, entre otras. Muchos de los problemas planteados para ser resueltos por la computadora, requieren que se manejen colecciones de datos relacionados, y para ello la mayoría de los lenguajes de alto nivel nos permiten construir estructuras de datos a partir de datos simples u otras estructuras previamente definidas. En el lenguaje C estos tipos estructurados son los arreglos, las estructuras y las uniones. En estos tipos estructurados, aunque el programador puede establecer el tamaño y forma del tipo de dato, las operaciones que pueden hacerse con los datos de ese tipo ya están definidas. En este capítulo, se estudiarán otras estructuras de datos no predefinidas por el lenguaje C pero que tienen gran aplicación en las ciencias computacionales. Se definirán sus operaciones y la forma de implementarlas en el lenguaje C. Las estructuras de datos que se verán en este capítulo son las pilas, colas, listas ligadas y árboles binarios. Existen otras estructuras y variantes de las que se tratarán en este capítulo pero que no se tratan aquí por limitaciones de tiempo y espacio.

Pilas Una pila es una colección ordenada de elementos homogéneos (todos del mismo tipo) en la que sólo pueden insertarse nuevos elementos o extraerse elementos por un extremo de la pila llamado tope . El primer elemento a extraerse es el último que se insertó. Sólo se puede acceder al elemento que se encuentre en el tope de la pila. La figura 16-1 representa una pila usada para almacenar enteros en diferentes estados. La flecha apunta al tope de la pila. (16-1a) La pila está vacía; (16-1b) la pila después de haber insertado el número 1; (16-1c) después de haber insertado el número 2; (16-1d)

ITSON

Manuel Domitsu Kono

386

Estructuras de Datos en C

después de haber insertado el número 3; (16-1e) después de haber extraído el número 3; (16-1f) después de haber insertado el número 4; (16-1g) después de haber extraído el número 4; (16-1h) después de haber extraído el número 2. Tope



1 →

(a)

1 2

1 2 3



(b)

(c)



1 2 →

(d)

(e)

1 2 4 →

(f)

1 2 →

(g)

1 →

(h)

Figura 16-1

Operaciones Primitivas de las Pilas Las operaciones primitivas definidas para una pila son: Inicializar:

Vacía la pila.

Vacía:

Regresa verdadero si la pila está vacía.

Llena:

Regresa verdadero si la pila está llena.

Insertar:

Inserta un elemento en la pila.

Extraer:

Extrae un elemento de la pila.

Implementación de una Pila en C A continuación se implementará una pila para almacenar enteros en C. El primer paso es determinar el tipo de datos en el que se almacenará la pila. Aquí se empleará un arreglo, aunque más adelante se estudiará otra estructura de datos, la lista ligada, en la que puede implementarse una pila. Primero definiremos un tipo estructura que contendrá los datos que nos permitan acceder a la pila: typedef struct { int *ppila; int *ptope; int tamPila; } PILA;

/* Apuntador al inicio del arreglo en que se implementa la pila. */ /* Apuntador al tope de la pila. */ /* Tamaño máximo de la pila. */

Todas las funciones que implementan las operaciones de la pila reciben como parámetro la dirección de una variable tipo PILA, la cual contiene los datos con los que las funciones van a operar. La operación de inicializar la pila se implementa con la función inicializarPila() cuyo código es:

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

387

void inicializarPila(PILA *spila) { spila->ptope = spila->ppila; }

La función inicializarPila() sólo requiere que el tope de la pila se encuentre al inicio del arreglo. Dado que sólo se puede acceder al elemento apuntado por ptope no se requiere borrar realmente los datos. La operación vacía se implementa mediante la función pilaVacia() que regresa un 1 si la pila está vacía, 0 en caso contrario y su código es int pilaVacia(PILA *spila) { return spila->ptope == spila->ppila; }

Vemos que si el apuntador ptope apunta al inicio del arreglo, la pila está vacía. La operación llena se implementa mediante la función pilaLlena() que regresa un 1 si la pila está llena, 0 en caso contrario. Su código es int pilaLlena(PILA *spila) { return spila->ptope == spila->ppila + pila->tamPila; }

La pila está llena si el apuntador ptope apunta a la localidad siguiente del último elemento del arreglo. La operación inserta se implementa mediante la función insertarPila() que inserta un dato en la pila si no está llena. La función regresa un 1 si hay éxito, 0 en caso contrario. int insertarPila(PILA *spila, int dato) { if(pilaLlena(spila)) return 0; *(spila->ptope) = dato; spila->ptope++; return 1; }

La función primero verifica que haya espacio para insertar el dato. Si lo hay lo inserta en la localidad apuntada por ptope y luego mueve ptope a que apunte a la siguiente localidad. La operación extraer se implementa con la función extraerPila() que extrae un dato de la pila si no está vacía. La función regresa un 1 si hay éxito, 0 en caso contrario. El dato extraído es almacenado en la dirección dada por el parámetro pdato. int extraerPila(PILA *spila, int *pdato) { if(pilaVacia(spila)) return 0; spila->ptope--; *pdato = *(spila->ptope); return 1; }

ITSON

Manuel Domitsu Kono

388

Estructuras de Datos en C

La función primero verifica que la pila contenga elementos. Si los hay mueve el apuntador ptope a que apunte al que está en el tope y luego lo copia a la localidad apuntada por el parámetro pdato. Note que no es necesario borrar físicamente de la pila el dato extraído. Ya que no es posible acceder a las localidades más allá del apuntador ptope. A continuación se muestra, como ejemplo, la secuencia de llamadas que produciría los diferentes estados de la pila mostrados en la figura 16-1. /* Tamaño máximo de la pila */ const int TAMPILA = 10; /* Se declara la pila */ int pila[10]; int main(void) { /* Se inicializa la estructura con los datos para el manejo de la pila */ PILA spila = {pila, pila, TAMPILA}; int dato; inicializarPila(&spila); insertarPila(&spila, 1); insertarPila(&spila, 2); insertarPila(&spila, 3); extraerPila(&spila, &dato); insertarPila(&spila, 4); extraerPila(&spila, &dato); extraerPila(&spila, &dato); return 0; }

Implementación de una Pila Generalizada en C Las funciones que se implementaron en la sección anterior sólo sirven para pilas de enteros. Si deseamos tener una pila de caracteres, flotantes u otro tipo de datos deberemos modificar las funciones. Esto, sin embargo, haría que tuviéramos una familia de funciones para cada tipo de dato. Otra solución es desarrollar un grupo de funciones que implementen las operaciones de una pila que sean independientes del tipo de dato almacenado en la pila. La razón de la dependencia de las funciones en el tipo de datos es por dos razones: Primero el tamaño de los datos, que determina el número de bytes que debe moverse ptope para apuntar de un dato a otro y segundo la forma de copiar los datos para hacer la inserción o extracción de la pila. Para eliminar la dependencia en el tamaño de los diferentes datos podemos hacer que las funciones para el manejo de la pila consideren a los datos como bloques de bytes en lugar de datos de un tipo en particular. Esto es, para la las funciones, cada elemento de la pila es un bloque de bytes, aunque el usuario declare la pila como un arreglo del tipo deseado.

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

389

Apuntadores Tipo void Para un modelo de memoria, los apuntadores ocupan el mismo espacio de memoria. por ejemplo en el modelo pequeño (small) los siguientes apuntadores ocupan 2 bytes cada uno int *px; float *py;

el identificador de tipo que precede al identificador del apuntador no establece el tamaño del apuntador sino el tamaño del dato al que éste apunta. El compilador utiliza esta información cuando realiza operaciones de la aritmética de apuntadores y para la desreferenciación de los apuntadores. Hay un tipo de apuntador, sin embargo, que sólo guarda la información de dirección sin conservar la información sobre el tamaño del dato al que apunta. Este tipo de apuntador se conoce como un apuntador genérico o void. La sintaxis de la declaración de un apuntador void es void *nomApunt; Las propiedades de los apuntadores void son las siguientes: 1. A un apuntador void le podemos asignar la dirección de una variable de cualquier tipo. Lo contrario no es válido. Esto es, no se puede desreferenciar a un apuntador void. Por ejemplo: int x; float y; void *pu, *pv; --pu = &x; pv = &y; --x = *pu; y = *pv; ---

/* Error */ /* Error */

La desreferenciación no es válida ya que no se conoce el tamaño del valor al que apunta un apuntador void. En lugar de ello debemos primero convertir los apuntadores void a un apuntador de un tipo dado usando el operador cast: --x = *(int *)pu; y = *(float *)pv; ---

2. A un apuntador void le podemos asignar el valor de un apuntador de cualquier tipo. Lo contrario, esto es, asignarle a un apuntador de un tipo dado el valor de un apuntador void también es válido. Por ejemplo: int *px; float *py; void *pu, *pv; --pu = px; pv = py;

ITSON

Manuel Domitsu Kono

390

Estructuras de Datos en C

--px = pu; py = pv; ---

3. La aritmética de apuntadores no es válida con los apuntadores void. Por ejemplo las siguientes instrucciones generan errores: void *pu, *pv; pu++; pv -= 4;

/* Error */ /* Error */

Estas operaciones sólo pueden hacerse convirtiendo el apuntador a un tipo apropiado y luego efectuar la operación aritmética: pu = (int *)pu + 1; pv = (float *)pv - 4;

o pu = (char *)pu + 1 * sizeof(int); pv = (char *)pv - 4 * sizeof(float);

La propiedad de que a un apuntador void le podemos asignar la dirección de una variable de cualquier tipo nos permite crear funciones genéricas para el manejo de pilas. A continuación implementaremos las funciones para el manejo de pilas genéricas como un módulo. El archivo de encabezados de este módulo es:

PILA.H #ifndef PILA_H #define PILA_H typedef struct { void *ppila; void *ptope; int tamPila; int tamElem;

/* Apuntador al arreglo donde se implementa la pila. */ /* Apuntador al final de la pila (donde se inserta o extrae un elemento). */ /* Tamaño máximo de la pila. */ /* Tamaño en bytes de cada dato a almacenarse en la pila. */

} PILA; void inicializarPila(PILA *spila); int pilaVacia(PILA *spila); int pilaLlena(PILA *spila); int insertarPila(PILA *spila, void *dato); int extraerPila(PILA *spila, void *dato); #endif

Los apuntadores ppila y ptope son declarados void para que puedan recibir direcciones de cualquier tipo de dato. También vemos que a la estructura PILA se le ha agregado el campo tamElem que contiene el tamaño en bytes de cada uno de los datos que van a almacenarse en la pila. El código de las

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

391

funciones para el manejo de la pila genérica se encuentra en PILA.C. Las funciones inicializarPila() y pilaVacia() no sufren modificaciones con respecto a las funciones para la pila de enteros. PILA.C /************************************************************* * PILA.C * * Este módulo implementa las operaciones de una pila * generalizada, esto es, que opera con cualquier tipo de dato. * La pila debe implementarse en un arreglo del tipo del dato * deseado. Todas las funciones reciben como parámetro un * apuntador a una estructura tipo PILA, que contiene la * información que requieren las funciones para manejar la * pila. La estructura está definida en "PILA.H". *************************************************************/ #include #include "pila.h" /************************************************************* * void inicializarPila(PILA *spila) * * Esta función inicializa (vacía) una pila. spila es un * apuntador a la estructura que contiene los datos que definen * la pila. *************************************************************/ void inicializarPila(PILA *spila) { spila->ptope = spila->ppila; } /************************************************************* * int pilaVacia(PILA *spila) * * Esta función regresa un 1 si la pila está vacía, 0 en caso * contrario. spila es un apuntador a la estructura contiene * los datos que definen la pila. *************************************************************/ int pilaVacia(PILA *spila) { return spila->ptope == spila->ppila; } /************************************************************* *int pilaLlena(PILA *spila) * * Esta función regresa un 1 si la pila está llena, 0 en caso * contrario. spila es un apuntador a la estructura que * contiene los datos que definen la pila. *************************************************************/ int pilaLlena(PILA *spila) { return spila->ptope == (char *)spila->ppila + spila->tamPila * spila->tamElem; } /************************************************************* * int insertarPila(PILA *spila, void *dato) * * Esta función inserta un dato en la pila si no está llena. * La función regresa un 1 si hay éxito, 0 en caso contrario. * spila es un apuntador a la estructura que contiene los

ITSON

Manuel Domitsu Kono

392

Estructuras de Datos en C

* datos que definen la pila. dato es apuntador al dato a * insertar. *************************************************************/ int insertarPila(PILA *spila, void *dato) { if(pilaLlena(spila)) return 0; memcpy(spila->ptope, dato, spila->tamElem); spila->ptope = (char *)spila->ptope + spila->tamElem; return 1; } /************************************************************* * int extraerPila(PILA *spila, void *dato) * * Esta función extrae un dato de la pila si no está vacía. * La función regresa un 1 si hay éxito, 0 en caso contrario. * spila es un apuntador a la estructura que contiene los * datos que definen la pila. dato es apuntador a la variable * en que se almacena el dato a extraer. *************************************************************/ int extraerPila(PILA *spila, void *dato) { if(pilaVacia(spila)) return 0; spila->ptope = (char *)spila->ptope - spila->tamElem; memcpy(dato, spila->ptope, spila->tamElem); return 1; }

La función pilaLlena() regresa 1 si el apuntador ptope apunta a la siguiente localidad después del último elemento del arreglo. La dirección de esa localidad se calcula sumándole a la dirección de inicio del arreglo el número de bytes que ocupa todo el arreglo que contiene la pila, el cual se obtiene por: spila->tamPila * spila->tamElem

Note la conversión del apuntador ppila de void a char para poder efectuar la suma. La función insertarPila() verifica primero si hay espacio en la pila para insertar el dato. Si lo hay utiliza la función memcpy() para copiar el dato a la localidad apuntada por ptope. En este caso el dato se maneja como un bloque de bytes de tamaño tamElem. La función memcpy() cuyo prototipo está en mem.h tiene la siguiente sintaxis: void *memcpy(void *dest , const void *fte, size_t n); memcpy() copia n bytes de la localidad apuntada por fuente a la localidad apuntada por dest. La función regresa el valor de dest. Por último, la función insertarPila() incrementa ptope para que apunte a la localidad siguiente al elemento insertado. La función extraerPila() trabaja en forma contraria a la función insertarPila(). El siguiente listado, DEMPILA.C , es un módulo que implementa un programa de demostración que muestra el comportamiento de una pila. Este módulo debe ligarse a los módulos: PILA y UTILS.

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

393

DEMPILA.C #include #include #include "utils.h" #include "pila.h" const int TAMPILA = 10;

/* Tamaño máximo de la pila */

char cpila[10];

/* Pila */

/* Estructura con los datos de la pila */ PILA spila = {cpila, cpila, TAMPILA, 1}; void despliegaPantallaInicial(PILA *spila); void despliegaPila(PILA *spila); int main(void) { char c; despliegaPantallaInicial(&spila); while((c = getch()) != ESC) { /* Si se presionó una tecla normal */ if(c) { /* Si la pila está llena */ if(!insertarPila(&spila, &c)) msj_error(0,22,"Pila llena!", "", ATTR(RED, LIGHTGRAY,0)); despliegaPila(&spila); } /* Si se presionó la tecla [Supr] */ else if(getch() == DEL) { /* Si la pila no está vacía */ if(extraerPila(&spila, &c)) { gotoxy(39,22); putchar(c); } else msj_error(0,22,"Pila vacía!", "", ATTR(RED, LIGHTGRAY,0)); despliegaPila(&spila); } } return 0; } void despliegaPantallaInicial(PILA *spila) { int ic; clrscr(); gotoxy(15,3); printf("Este programa demuestra el comportamiento de una"); gotoxy(15,4); printf("pila. En esta pila se almacenan caracteres con"); gotoxy(15,5); printf("sólo teclearlos. Para extraer un carácter de la"); gotoxy(15,6); printf("pila presiona la tecla [Supr]."); gotoxy(15,7); printf("Para terminar presiona la tecla [Esc]."); gotoxy(37, 9); printf("PILA"); gotoxy(37,10); printf("+---+");

ITSON

Manuel Domitsu Kono

394

Estructuras de Datos en C

gotoxy(30,11); printf("Tope \x10"); for(i = 0; i < spila->tamPila; i++) { gotoxy(37,11+i); printf(" "); } gotoxy(37,11+i); printf("+---+"); gotoxy(39,11); } void despliegaPila(PILA *spila) { int i; /* Borra la imagen de la pila anterior */ for(i = 0; i < spila->tamPila; i++) { gotoxy(30,11+i); printf(" "); gotoxy(39,11+i); putchar(' '); } gotoxy(30,12+i); printf(" "); for(i = 0; i < (char *)spila->ptope - spila->ppila; i++) { gotoxy(39,11+i); putchar(*((char *)(spila->ppila)+i)); } if(i == 10) gotoxy(30,12+i); else gotoxy(30,11+i); printf("Tope \x10"); if(i == 10) gotoxy(39,12+i); else gotoxy(39,11+i); }

Ejercicios Sobre Pilas 1. Escribe una función que nos permita inspeccionar el elemento del tope de la pila genérica. La pila deberá quedar intacta, es decir, el elemento en el tope de la pila deberá permanecer en la pila. La sintaxis de la función es int topePila(PILA *spila , void *dato); donde spila es el apuntador a la estructura con los datos de la pila y dato es un apuntador a la variable en la que se va a almacenar una copia del elemento en el tope de la pila. La función regresa 1 si hubo éxito, 0 en caso contrario (la pila está vacía). 2. Escribe una función que nos regrese el número de elementos en una pila genérica. La sintaxis de la función es int longPila(PILA *spila ); donde spila es el apuntador a la estructura con los datos de la pila.

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

395

Aplicaciones de Pilas Las aplicaciones de las pilas en las ciencias computacionales son muchas y van desde la implementación del mecanismo de las llamadas a subrutinas de los lenguajes de alto nivel, la evaluación de expresiones en notación postfija, la verificación de los niveles de paréntesis de una expresión, etc. Aquí se da una descripción, un poco simplificada, del mecanismo de la llamada a una subrutina mientras que en el problema 1, al final del capítulo se plantea otra aplicación de las pilas: Una calculadora RPN. Cuando se carga un programa en C a la memoria RAM, para ser ejecutado, se inicializa uno de los registros del microprocesador, llamado apuntador del programa (IP) a la primera instrucción dentro de la función main(). Se crean las variables con clase de almacenamiento estática en el segmento de datos y en el segmento que le corresponde según el modelo de memoria se crea una pila, llamada pila del sistema. Si la función main() tiene variables automáticas estas son creadas en la pila, es decir se reserva memoria en la pila para ellas. Para ejecutar el programa, el microprocesador lee de la dirección de memoria apuntada por el IP, la primera instrucción y en forma automática incrementa el IP para que apunte a la segunda instrucción. Luego decodifica la primera instrucción y la ejecuta. A continuación utiliza el IP para leer la segunda instrucción del programa incrementando en forma automática el IP para que apunte a la tercera instrucción, decodifica la segunda instrucción y la ejecuta y así consecutivamente. Si la instrucción decodificada es una llamada a una función, el procedimiento para ejecutar la función es el siguiente: 1. Si la función tiene parámetros, éstos son creados en la pila y los valores de los argumentos con los que se llama a la función son copiados a éstos. Los parámetros en la pila aparecen en el orden inverso del que se declaran. 2. Se almacena en la pila la dirección almacenada en el IP. Esta es la dirección de la instrucción a la que regresará el programa después de regresar de la función. 3. Si la función tiene variables automáticas, éstas son creadas en la pila. 4. Se carga en el IP la dirección de la primera instrucción de la función para su ejecución. 5. Se lee la primera instrucción de la función, se incrementa el IP, se decodifica la instrucción y se ejecuta. Este último paso se repite para el resto de las instrucciones de la función. Al terminar la ejecución, ocurren los siguientes eventos: 1. Las variables automáticas son destruidas, extrayéndolas de la pila y descartando su contenido. 2. Se extrae de la pila la dirección de regreso de la función y se carga al IP. 3. Se destruyen los parámetros de la función, extrayéndolos de la pila y descartando su contenido. 4. Se almacena el valor regresado por la función.

ITSON

Manuel Domitsu Kono

396

Estructuras de Datos en C

5. Como ya se cargó en el IP la dirección de la siguiente instrucción después de la llamada a la función, la siguiente instrucción leída por el microprocesador será ésa, con lo que el control del programa regresa a la función anterior. El proceso descrito anteriormente se ilustra con el siguiente programa. Las direcciones de memoria son supuestas y por simplificar supondremos que cada instrucción se almacena en dos bytes. En realidad, diferentes instrucciones tienen diferentes tamaños. En la tercera columna se muestra el número de la figura que ilustra el estado de la del sistema cada que ocurre un cambio.

Direcciones

Instrucciones int main(void) { int a, b;

0x1000 0x1002 0x1004

instrucción_01; instrucción_02; f1(a, b);

0x1006 0x1008

instrucción_03; instrucción_04;

Estado de la pila Figura 16-2a Figura 16-2b

Figura 16-2c Figura 16-2n

} int f1(int x, int y) { int c, d; 0x2000 0x2002 0x2004

instrucción_11; instrucción_12; f2(c);

0x2006 0x2008 0x2010

instrucción_13; instrucción_14; f3(d);

0x2012 0x2014

Figura 16-2d

Figura 16-2e Figura 16-2g Figura 16-2h Figura 16-2j

instrucción_15; instrucción_16; }

0x2100 0x2102

int f2(int z) { int e;

Figura 16-2f

instrucción_21; instrucción_22; } 0x2200 0x2202

int f3(int w) { int f;

Figura 16-2i

instrucción_31; instrucción_32; }

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

Tope →

a b →

a b y x

b a 0x1006



a b y x

397

b a 0x1006

c d →

a b y x c d z

b a 0x1006

c 0x2006



(a) a b y x

b a 0x1006

c d Tope →

(b) a b y x c d w

b a 0x1006

d 0x2012



(g)

(h)

(c) a b y x c d w f →

b a 0x1006

d 0x2012

(d) a b y x

b a 0x1006

a b y x c d z

b a 0x1006

c 0x2006

e →

(e)

(f) →

a b →

c d →

(i)

(j)

(k)

(l)

Figura 16-2.

Colas Colas lineales Una cola es una colección ordenada de elementos homogéneos en la que los nuevos elementos se insertan por uno de los extremos de la cola llamada final y sólo pueden extraerse elementos por el otro extremo de la cola llamado frente . El primer elemento a extraerse es el primero que se insertó. La figura 16-3 representa una cola usada para almacenar enteros en diferentes estados. (16-3a) La cola está vacía; (16-3b) la cola después de haber insertado el número 1; (16-3c) después de haber insertado el número 2; (16-3d) después de haber insertado el número 3; (16-3e) después de haber extraído el número 1; (16-3f) después de haber insertado el número 4; (16-3g) después de haber extraído el número 2; (16-3h) después de haber extraído el número 3.

ITSON

Manuel Domitsu Kono

398

Estructuras de Datos en C

Frente → = Final

Frente → Final →

(a) Frente → Final →

(e)

Frente →

Final →

1 2

Final →

(b) Frente →

2 3

1

2 3 4

Frente → Final →

(c)

Frente →

(f)

Final →

1 2 3

3 4

(g)

(d)

Frente → Final →

4

(h)

Figura 16-3.

Operaciones Primitivas de las Colas Las operaciones primitivas definidas para una cola son: Inicializar:

Vacía la cola.

Vacía:

Regresa verdadero si la cola está vacía.

Llena:

Regresa verdadero si la cola está llena.

Insertar:

Inserta un elemento en la cola.

Extraer:

Extrae un elemento de la cola.

Implementación de una Cola Lineal en C A continuación se implementará una cola para almacenar enteros en C. La cola se almacenará en un arreglo, aunque más adelante se estudiará otra estructura de datos, la lista ligada, en la que puede implementarse una cola. Primero definiremos un tipo estructura que contendrá los datos que nos permitan acceder a la cola typedef struct { int *pcola; int *pfrente; int *pfinal; int tamCola; } COLA;

ITSON

/* Apuntador al arreglo donde se implementa la cola. */ /* Apuntador al inicio de la cola (donde se extrae un elemento. */ /* Apuntador al final de la cola (donde se inserta un elemento. */ /* Tamaño máximo de la cola. */

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

399

Todas las funciones que implementan las operaciones de la cola reciben como parámetro la dirección de una variable tipo COLA, la cual contiene los datos con los que las funciones van a operar. La operación de inicializar la cola se implementa con la función inicializarCola() cuyo código es: void inicializarCola(COLA *scola) { scola->pfrente = scola->pfinal = scola->pcola; }

La función inicializarCola() sólo requiere que tanto el frente como el final de la cola se encuentren al inicio del arreglo. La operación vacía se implementa mediante la función colaVacia() que regresa un 1 si la cola está vacía, 0 en caso contrario y su código es int colaVacia(COLA *scola) { return scola->pfinal == scola->pfrente; }

Vemos que si los apuntadores al frente y al final de la cola apuntan a la misma localidad, la cola está vacía. La operación llena se implementa mediante la función colaLlena() que regresa un 1 si la cola está llena, 0 en caso contrario. Su código es int colaLlena(COLA *scola) { return scola->pfinal == scola->pcola + scola->tamCola; }

La cola está llena si el apuntador pfinal apunta a la localidad siguiente del último elemento del arreglo. La operación inserta se implementa mediante la función insertarCola() que inserta un dato en la cola si no está llena. La función regresa un 1 si hay éxito, 0 en caso contrario. int insertarCola(COLA *scola, int dato) { if(colaLlena(scola)) return 0; *(scola->pfinal) = dato; scola->pfinal++; return 1; }

La función primero verifica que haya espacio para insertar el dato. Si lo hay, lo inserta en la localidad apuntada por pfinal y luego mueve pfinal a que apunte a la siguiente localidad. La operación extraer se implementa con la función extraerCola() que extrae un dato de la cola si no está vacía. La función regresa un 1 si hay éxito, 0 en caso contrario. El dato extraído es almacenado en la dirección dada por el parámetro pdato. int extraerCola(COLA *scola, int *pdato) { if(colaVacia(scola)) return 0;

ITSON

Manuel Domitsu Kono

400

Estructuras de Datos en C

*pdato = *(scola->pfrente); scola->pfrente++; return 1; }

La función primero verifica que la cola contenga elementos. Si los hay, copia el elemento apuntado por pfrente a la localidad apuntada por el parámetro pdato, luego incrementa el apuntador pfrente a que apunte al siguiente elemento a extraer. Note que no es necesario borrar físicamente de la cola el dato extraído. Ya que no es posible regresar el apuntador pfrente para que apunte a un dato previamente extraído. A continuación se muestra, como ejemplo, la secuencia de llamadas que produciría los diferentes estados de la cola mostrados en la figura 16-3. const int

TAMCOLA = 10;

int cola[10]; int main(void) { COLA scola = {cola, cola, cola, TAMCOLA}; int dato; inicializarCola(&scola); insertarCola(&scola, 1); insertarCola(&scola, 2); insertarCola(&scola, 3); extraerCola(&scola, &dato); insertarCola(&scola, 4); extraerCola(&scola, &dato); extraerCola(&scola, &dato); return 0; }

Podemos ver de la figura 16-3 y de las funciones insertarCola() y extraerCola() que los apuntadores al frente y al final de la cola siempre avanzan. Por lo tanto, el espacio liberado al extraer un elemento no puede reutilizarse y la condición de cola llena puede ocurrir aún cuando el número de elementos en la cola sea inferior al tamaño del arreglo en que se almacena la cola, inclusive podemos tener la condición de cola llena con la cola vacía de elementos.

Colas Circulares La cola implementada en la sección anterior se llama cola lineal y como vimos presenta el problema de que eventualmente caeremos en la situación de cola llena al no poder reutilizar el espacio liberado al extraer elementos. Una implementación de una cola que permite la reutilización del espacio liberado al extraer elementos se obtiene permitiendo que los apuntadores al frente y al final de la cola se regresen al principio de la cola después de haber llegado al final del arreglo. Podemos imaginarnos que la cola forma un círculo con el principio del arreglo unido al final del arreglo. Este tipo de cola se llama cola circular. En la figura 16-4 se muestra una cola circular con tamaño 5. En la figura 16-4a se muestra a la cola con tres elementos: 1, 2, 3. En la figura 16-4b, se ha insertado un cuarto elemento, el número 4. En este

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

401

momento la cola se ha llenado. Note que no podemos insertar otro elemento en la localidad apuntada por pfinal ya que de hacerlo el apuntador pfinal se regresaría al principio del arreglo y apuntaría a la misma localidad apuntada por el apuntador pfrente. Esto sin embargo es la condición para que la cola se encuentre vacía. En la figura 16-4c se ha extraído el número 1 de la cola. Esto nos permite insertar un nuevo elemento en la cola, el número 5, como se muestra en la figura 16-d. Podemos ver que el apuntador al final de la cola se ha movido al inicio del arreglo. También podemos ver que de nuevo tenemos que la cola está llena ya que no podemos insertar un elemento en la localidad apuntada por pfinal ya que de hacerlo se volverían a empalmar los apuntadores al frente y final de la cola indicándonos que la pila esta vacía. De lo anterior podemos ver que en una cola circular cuando mucho podemos almacenar un número de elementos igual al tamaño del arreglo menos uno. Frente →

1 2 3

Final →

Frente →

1 2 3 4

Final →

(a)

Frente →

2 3 4

Final → Frente →

Final →

(b)

(c)

2 3 4 5

(d)

Figura 16-4.

Implementación de una Cola Circular en C La implementación de una cola circular sólo requiere que se modifiquen las rutinas colaLlena(), insertarCola() y extraerCola(). Las rutinas inicializarCola() y colaVacía() permanecen iguales. Consideremos de nuevo el caso de una cola para almacenar enteros. La figura 16-4b muestra que una de las maneras en que una cola circular puede llenarse es cuando el frente de la cola está al inicio del arreglo y el final de la cola apunta a la última localidad del arreglo. La figura 16-d muestra otra de las situaciones en las que la cola se llena. Aquí el final de la cola se encuentra a una localidad detrás del frente de la cola. Por lo tanto la función colaLlena() debe verificar ambas situaciones: int colaLlena(COLA *scola) { return scola->pfinal == scola->pfrente+scola->tamCola-1 || scola->pfrente == scola->pfinal + 1; }

Las modificaciones a las funciones insertarCola() y extraerCola() consisten en verificar si los apuntadores al final y al frente de la cola han llegado al final del arreglo, en cuyo caso se regresan al inicio del arreglo: int insertarCola(COLA *scola, int dato) { if(colaLlena(scola)) return 0; *(scola->pfinal) = dato; scola->pfinal++; if(scola->pfinal == scola->pcola + scola->tamCola) scola->pfinal = scola->pcola;

ITSON

Manuel Domitsu Kono

402

Estructuras de Datos en C

return 1; } int extraerCola(COLA *scola, int *pdato) { if(colaVacia(scola)) return 0; *pdato = *(scola->pfrente); scola->pfrente++; if(scola->pfrente == scola->pcola + scola->tamCola) scola->pfrente = scola->pcola; return 1; }

Implementación de una Cola Circular Generalizada en C Al igual que como lo hicimos con las pilas, podemos modificar las funciones que implementan las operaciones de una cola circular para que sean independientes del tipo de dato almacenado en la cola. Estas nuevas funciones se implementan como un módulo. El archivo de encabezados de este módulo es:

COLA.H #ifndef COLA_H #define COLA_H typedef struct { void *pcola; void *pfrente; void *pfinal; int tamCola; int tamElem;

/* Apuntador al arreglo donde se implementa la cola. */ /* Apuntador al inicio de la cola (donde se extrae un elemento). */ /* Apuntador al final de la cola (donde se inserta un elemento). */ /* Tamaño del arreglo donde implementa la cola. */ /* Tamaño en bytes de cada dato a almacenarse en la cola. */

} COLA; void inicializarCola(COLA *scola); int colaVacia(COLA *scola); int colaLlena(COLA *scola); int insertarCola(COLA *scola, void *dato); int extraerCola(COLA *scola, void *dato); #endif

El código de las funciones para el manejo de la cola genérica se encuentra en COLA.C. COLA.C. /************************************************************* * COLA.C * * Este módulo implementa las operaciones de una cola circular * generalizada, esto es, que opera con cualquier tipo de dato * La cola debe implementarse en un arreglo del tipo de dato

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

403

* deseado. Todas las funciones reciben como parámetro un * apuntador a una estructura tipo COLA, que contiene la * información que requieren las funciones para manejar la cola. * La estructura esta definida en "COLA.H". *************************************************************/ #include #include "cola.h" /************************************************************* * void inicializarCola(COLA *scola) * * Esta función inicializa (vacía) una cola. scola es un * aputador a la estructura que contiene los datos que definen * la cola. *************************************************************/ void inicializarCola(COLA *scola) { scola->pfrente = scola->pfinal = scola->pcola; } /************************************************************* * int colaVacia(COLA *scola) * * Esta función regresa un 1 si la cola esta vacía, 0 en caso * contrario. scola es un apuntador a la estructura que * contiene los datos que definen la cola. *************************************************************/ int colaVacia(COLA *scola) { return scola->pfinal == scola->pfrente; } /************************************************************* * int colaLlena(COLA *scola) * * Esta función regresa un 1 si la cola esta llena, 0 en caso * contrario. scola es un apuntador a la estructura que * contiene los datos que definen la cola. *************************************************************/ int colaLlena(COLA *scola) { return scola->pfinal == (char *)scola->pfrente + (scola->tamCola - 1)* scola->tamElem || scola->pfrente == (char *)scola->pfinal + scola->tamElem; } /************************************************************* * int insertarCola(COLA *scola, void *dato) * * Esta función inserta un dato en la cola si no esta llena. * La función regresa un 1 si hay éxito, 0 en caso contrario * scola es un apuntador a la estructura que contiene los * datos que definen la cola. dato es apuntador al dato a * insertar. *************************************************************/ int insertarCola(COLA *scola, void *dato) { if(colaLlena(scola)) return 0; memcpy(scola->pfinal, dato, scola->tamElem); scola->pfinal = (char *)scola->pfinal + scola->tamElem; if(scola->pfinal == (char *)scola->pcola + scola->tamCola * scola->tamElem) scola->pfinal = scola->pcola;

ITSON

Manuel Domitsu Kono

404

Estructuras de Datos en C

return 1; } /************************************************************* * int extraerCola(COLA *scola, void *dato) * * Esta función extrae un dato de la cola si no esta vacía. * La función regresa un 1 si hay éxito, 0 en caso contrario * scola es un apuntador a la estructura que contiene los * datos que definen la cola. dato es apuntador a la variable * en que se almacena el dato a extraer. *************************************************************/ int extraerCola(COLA *scola, void *dato) { if(colaVacia(scola)) return 0; memcpy(dato, scola->pfrente, scola->tamElem); scola->pfrente = (char *)scola->pfrente + scola->tamElem; if(scola->pfrente == (char *)scola->pcola + scola->tamCola * scola->tamElem) scola->pfrente = scola->pcola; return 1; }

El siguiente listado, DEMCOLA.C, es un módulo que implementa un programa de demostración que muestra el comportamiento de una cola circular. Este módulo debe ligarse a los módulos: COLA y UTILS.

DEMCOLA.C #include #include #include "utils.h" #include "cola.h" const int TAMCOLA = 10; char ccola[10];

/* Tamaño máximo de la cola */

/* Cola */

/* Estructura con los datos de la cola */ COLA scola = {ccola, ccola, ccola, TAMCOLA, 1}; void despliegaPantallaInicial(COLA *scola); void despliegaCola(COLA *scola); int main(void) { char c; despliegaPantallaInicial(&scola); while((c = getch()) != ESC) { /* Si se presionó una tecla normal */ if(c) { /* Si la cola esta llena */ if(!insertarCola(&scola, &c)) msj_error(0,22,"Cola llena!, [Enter]", "\r", ATTR(RED, LIGHTGRAY,0));

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

405

despliegaCola(&scola); } /* Si se presionó la tecla [Supr] */ else if(getch() == DEL) { /* Si la cola no esta vacía */ if(extraerCola(&scola, &c)) { gotoxy(39,22); putchar(c); } else msj_error(0,22,"Cola vacía!, [Enter]", "\r", ATTR(RED, LIGHTGRAY,0)); despliegaCola(&scola); } } return 0; } void despliegaPantallaInicial(COLA *scola) { int i; clrscr(); gotoxy(15,3); printf("Este programa demuestra el comportamiento de una"); gotoxy(15,4); printf("cola. En esta cola se almacenan caracteres con"); gotoxy(15,5); printf("solo teclearlos. Para extraer un carácter de la"); gotoxy(15,6); printf("cola presiona la tecla [Supr]."); gotoxy(15,7); printf("Para terminar presiona la tecla [Esc]."); gotoxy(37, 9); printf("COLA"); gotoxy(37,10); printf("+---+"); gotoxy(22,11); printf("Inicio = Fin \x10"); for(i = 0; i < scola->tamCola; i++) { gotoxy(37,11+i); printf(" "); } gotoxy(37,11+i); printf("+---+"); gotoxy(39,11); } void despliegaCola(COLA *scola) { int i, t; for(i = 0; i < scola->tamCola; i++) { gotoxy(22,11+i); printf(" gotoxy(39,11+i); putchar(' '); }

");

if(scola->pfrente pfinal) { for(i= 0; i < (char *)scola->pfrente - scola->pcola; i++) { gotoxy(39,11+i); putchar(' '); } if(scola->pfrente == scola->pfinal) { gotoxy(22,11+i); printf("Inicio =");

ITSON

Manuel Domitsu Kono

406

Estructuras de Datos en C

} else { gotoxy(28,11+i); printf("Inicio \x10"); } for(; i < (char *)scola->pfinal - scola->pcola; i++) { gotoxy(39,11+i); putchar(*((char *)(scola->pcola)+i)); } gotoxy(31,11+i); printf("Fin \x10"); gotoxy(39,11+i); } else /* scola->pfrente > scola->pfinal */ { for(i = 0; i < (char *)scola->pfinal - scola->pcola; i++ { gotoxy(39,11+i); putchar(*((char *)(scola->pcola)+i)); } t = i; gotoxy(31,11+i); printf("Fin \x10"); for(; i < (char *)scola->pfrente - scola->pcola; i++) { gotoxy(39,11+i); putchar(' '); } gotoxy(28,11+i); printf("Inicio \x10"); for(; i < scola->tamCola; i++) { gotoxy(39,11+i); putchar(*((char *)(scola->pcola)+i)); } gotoxy(39,11+t); } }

Ejercicio Sobre Colas Escribe una función que nos regrese el número de elementos en una cola circular genérica. La sintaxis de la función es int longCola(COLA *scola ); donde scola es el apuntador a la estructura con los datos de la cola.

Aplicaciones de Colas Las colas son eventos que ocurren en la vida diaria: Hacemos colas en los bancos, supermercados, oficinas de gobierno, etc. Una cola se forma cuando la tasa de llegada de los clientes es mayor que la velocidad con que son atendidos. En las ciencias computacionales, también ocurren colas ya que las

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

407

diferentes componentes de una computadora operan a diferentes velocidades: Por ejemplo los caracteres que tecleamo s se van almacenando en una cola llamada buffer del teclado en espera de ser procesados por la computadora. También la información que se desea mandar a un archivo o leer de un archivo es escrita o leída primero a una cola llamada buffer del archivo y luego enviada al archivo o al programa. Otra aplicación de colas son las llamadas colas de impresión. Las colas de impresión por lo general se implementan en ambientes multiproceso, esto es, en los sistemas operativos que pueden ejecutar más de un proceso simultáneamente. Cuando un proceso desea imprimir algo se lo envía a un proceso llamado servidor de impresión. El servidor de impresión almacena las peticiones de impresión en una cola y las va procesando una por una. Otra área de aplicación de las colas es la de simulación de eventos que generan colas como las de los bancos, supermercados, etc. En estas simulaciones se estudia el comportamiento de las colas con el fin de determinar el número adecuado de cajeras que deben estar atendiendo a los clientes. Si el número de cajeras es pequeño, las colas serán muy largas y probablemente los clientes busquen otro banco o supermercado. Si el número de cajeras es grande, el negocio incurrirá en fuertes gastos por salarios y prestaciones. En el problema 2 al final del capítulo se propone un programa que simule el comportamiento de un pequeño aeropuerto que consta de una sola pista para el despegue y aterrizaje de los aviones.

Asignación Dinámica de Memoria Cuando definimos variables, apartamos memoria para esas variables. Esa solicitud de memoria se dice que ocurre al tiempo de compilación, ya que es el compilador quien genera el código necesario para asignarnos la memoria requerida por las variables. Se dice que el compilador hace una asignación estática de memoria ya que una vez compilado el programa no podemos modificar el espacio asignado a las variables. Lo anterior presenta el inconveniente de que debemos conocer el tamaño de nuestras variables al momento de estar diseñando el programa. Por ejemplo si se define un arreglo de un tamaño dado y al estar ejecutando el programa, resulta que el número de datos que se desea almacenar es mayor, lo único que podemos hacer es modificar el código y recompilar el programa. Lo ideal sería que al ejecutar el programa y al saber el número de datos que se desean almacenar en el arreglo, hiciéramos la solicitud del espacio de memoria necesario. Esta solicitud de memoria al tiempo de ejecución se conoce como asignación dinámica de memoria. C posee un mecanismo mediante el cual podemos pedirle al sistema operativo bloques de memoria. También podemos liberar los bloques una vez que ya no los ocupemos para que estén disponibles para futuras solicitudes. La cantidad de memoria disponible para esas solicitudes depende del hardware, sistema operativo, e implementación del compilador. En un programa compilado en Turbo C para correr bajo el sistema operativo MSDOS en una computadora personal compatible con IBM, la memoria disponible para esas peticiones depende del modelo de memoria empleado (y por supuesto de la memoria física disponible en la computadora).

ITSON

Manuel Domitsu Kono

408

Estructuras de Datos en C

El Montículo La memoria asignada a un programa se divide en cuatro partes: La memoria ocupada por el código, la ocupada por las variables estáticas, la ocupada por la pila y la memoria ocupada por el montículo (heap). Es del mpntículo que el sistema operativo nos asigna los bloques que solicitamos dinámicamente. En la figura 16-5 se muestra los tamaños máximos de esas porciones dependiendo del modelo de memoria empleado en la compilación del programa. En el modelo pequeñito, todo el programa no debe exceder los 64 KB. Por lo tanto el tamaño máximo del montículo es: 64 KB - (código + variables estáticas + pila). En los modelos pequeño y mediano el código ocupa sus propios segmentos, y los datos también tienen su propio segmento, el tamaño máximo del montículo es: 64 KB - (variables estáticas + pila). Los primeros 640 KB de la memoria de la computadora se conocen como la memoria convencional. Parte de la memoria convencional está ocupada por el sistema operativo, manejadores de dispositivos, programas residentes en memoria, etc. y el programa que estamos ejecutando. En un programa compilado con el modelo de memoria pequeño o mediano, el resto de la memoria se conoce como el montículo lejano y también puede ser accesado por nuestro programa. Modelo pequeñito (tiny)

↓ ↑

Un sólo segmento (≤ 64KB) Código Variables estáticas Montículo Pila

Modelo pequeño (small)

Modelo mediano (medium)

Un segmento (≤ 64KB) Código

Uno o varios segmentos cada uno (≤ 64KB) Código ...

↓ ↑

Un segmento (≤ 64KB) Variables estáticas Montículo Pila



Hasta el resto de memoria Montículo lejano

↓ ↑



Un segmento (≤ 64KB) Variables estáticas Montículo Pila Hasta el resto de memoria Montículo lejano

Figura 16-5 NOTA:

En la figura 5, las flechas indican la dirección de crecimiento de la pila y el montículo, es decir el orden en que se va asignando la memoria conforme se va requiriendo.

En los modelos compacto, grande y gigante, el código también ocupa sus propios segmentos. Las variables estáticas tienen sus propios segmentos y la pila también tiene el suyo. El resto de la memoria convencional disponible es el montículo y está disponible para nuestro programa.

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

409

Modelo compacto (compact)

Modelo grande (large)

Modelo gigante (huge)

Un segmento ( ≤ 64KB) Código

Uno o varios segmentos cada uno (≤ 64KB) Código ...

Uno o varios segmentos cada uno (≤ 64KB) Código ...

Un segmento (≤ 64KB) Variables estáticas

Uno o varios segmentos cada uno (≤ 64KB) Variables estáticas ...

Un segmento ( ≤ 64KB) Variables estáticas



Un segmento ( ≤ 64KB) Pila



Hasta el resto de memoria Montículo



Un segmento (≤ 64KB) Pila



Hasta el resto de memoria Montículo





Un segmento (≤ 64KB) Pila Hasta el resto de memoria Montículo

Figura 16-5. Continuación.

Funciones Estándar para el Manejo del Montículo La biblioteca estándar de C, provee de cuatro funciones para el manejo del montículo. Las funciones calloc() y malloc() nos permiten pedirle al sistema operativo, bloques de la memoria. La función realloc() nos permite modificar el tamaño de un bloque previamente asignado y la función free() nos permite liberar dichos bloques. Los prototipos de estas funciones se encuentran en los archivos de encabezados stdlib.h o alloc.h. La tabla 16-1 describe dichas funciones. Tabla 16-1. Funciones de la biblioteca estándar para el manejo del montículo. void *calloc(size_t nElem, size_t tamaño); Función: Regresa: Comentarios:

Asigna un bloque de memoria de tamaño nElem x tamaño, del montículo, en forma dinámica. El bloque es inicializado a ceros. La función regresa un apuntador al bloque asignado. Si no hay memoria suficiente para el nuevo bloque o nbloques o tamaño son cero, calloc() regresa NULL. El tamaño máximo de un bloque no puede exceder los 64KB. size_t es un alias de unsigned y está definido en stdlib.h y alloc.h.

void free(void *bloque); Función:

ITSON

Libera un bloque de memoria asignado por las funciones calloc(), malloc() o realloc(). bloque es la dirección del bloque a liberar.

Manuel Domitsu Kono

410

Estructuras de Datos en C

Tabla 16-1. Funciones de la biblioteca estándar para el manejo del montículo, Continuación. void *malloc(size_t tamaño); Función: Regresa: Comentarios:

Asigna un bloque de memoria de tamaño tamaño, del montículo, en forma dinámica. El bloque no es inicializado. La función regresa un apuntador al bloque asignado. Si no hay memoria suficiente para el nuevo bloque o tamaño es cero, malloc() regresa NULL. El tamaño máximo de un bloque no puede exceder los 64KB.

void *realloc(void * obloque, size_t ntamaño); Función:

Regresa:

Comentarios:

Ajusta el tamaño de un bloque de memoria en el montículo, expandiéndolo o encogiéndolo. obloque es la dirección de un bloque previamente asignado por las funciones calloc(), malloc() o realloc() y ntamaño es el nuevo tamaño del bloque. La función regresa un apuntador al bloque ajustado, el cual puede ser diferente al bloque original ya que de ser necesario, la función copia los el contenido del bloque a una nueva posición. Si el bloque no puede ser ajustado o ntamaño es cero, la función regresa NULL. Si obloque es un apuntador nulo, realloc() trabaja igual que malloc(). El nuevo tamaño del bloque no puede exceder los 64KB.

Adicionalmente a las cuatro funciones de la biblioteca estándar, Turbo C nos proporciona otro grupo de funciones: farcalloc() y farmalloc() para pedir bloques de la memoria, farrealloc() para ajustar el tamaño de un bloque previamente asignado y farfree() para liberar bloques. Por último podemos determinar la cantidad de memoria disponible en el montículo usando las funciones coreleft() y farcoreleft(). Los prototipos de esas funciones se encuentran en el archivo de encabezados alloc.h. La tabla 16-2 describe dichas funciones.

Tabla 16-2. Funciones adicionales de Turbo C para el manejo del montículo. unsigned coreleft(void); unsigned long coreleft(void); Función: Comentarios:

En los modelos pequeñito, pequeño y mediano. En los modelos compacto, grande y gigante.

Regresa una medida de la memoria disponible en el montículo. El valor que regresa la función coreleft() depende del modelo de memoria empleado.

void far *farcalloc(unsigned long nElem, unsigned long tamaño); Función: Regresa: Comentarios:

Asigna un bloque de memoria de tamaño nElem x tamaño, del montículo lejano, en forma dinámica. La función regresa un apuntador al bloque asignado. Si no hay memoria suficiente para el nuevo bloque o nbloques o tamaño son cero, farcalloc() regresa NULL. Un bloque puede exceder los 64KB. Se utilizan apuntadores lejanos para accesar al bloque asignado.

unsigned long farcoreleft(void); Función:

ITSON

Regresa una medida de la memoria disponible en el montículo lejano.

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

411

Tabla 16-2. Funciones adicionales de Turbo C para el manejo del montículo. Continuación. void farfree(void far *bloque); Función: Comentarios:

Libera un bloque de memoria asignado del montículo lejano. bloque es la dirección del bloque a liberar. En los modelos pequeño y mediano, los bloques asignados por farmalloc() no pueden liberarse con free() y los bloque asignados con malloc() no pueden liberarse con farfree().

void far *farmalloc(unsigned long tamaño); Función: Regresa: Comentarios:

Asigna un bloque de memoria de tamaño tamaño, del montículo lejano, en forma dinámica. La función regresa un apuntador al bloque asignado. Si no hay memoria suficiente para el nuevo bloque o tamaño es cero, farmalloc() regresa NULL. Un bloque puede exceder los 64KB. Se utilizan apuntadores lejanos para accesar al bloque asignado.

void far *farrealloc(void far *obloque, unsigned long ntamaño); Función: Regresa: Comentarios:

Ajusta el tamaño de un bloque de memoria en el montículo lejano. obloque es la dirección del bloque a ajustar y ntamaño es el nuevo tamaño del bloque. La función regresa un apuntador al bloque ajustado, el cual puede ser diferente al bloque original. Si el bloque no puede ser ajustado la función regresa NULL. Un bloque puede exceder los 64KB. Se utilizan apuntadores lejanos para accesar al bloque asignado.

Para ilustrar el uso de las funciones para el manejo del montículo, considere el siguiente programa que lee una serie de números reales y construye un histograma de frecuencias de esos datos.

DEMOHEAP.C /************************************************************* * DEMOHEAP.C * * Este programa lee una serie de números reales y construye * un histograma de frecuencias de esos datos. Los arreglos * que contienen los datos y el histograma se crean en forma * dinámica. *************************************************************/ #include #include typedef struct { float limInf; int frec; } HISTO;

/* Límite inferior del intervalo */ /* No. de datos que caen en el intervalo */

float *leeDatos(int nDatos); HISTO *construyeHistograma(float *pDatos, int nDatos, int nIntervalos); void despliegaHistograma(HISTO *pHisto, int nIntervalos); int main(void) { int nDatos, nIntervalos; HISTO *pHisto; float *pDatos;

ITSON

Manuel Domitsu Kono

412

Estructuras de Datos en C

do { printf("\nNúmero de datos a leer: "); scanf("%d", &nDatos); } while(nDatos *(pDatos + i)) menor = *(pDatos + i); if(mayor < *(pDatos + i)) mayor = *(pDatos + i); } /* Encuentra el ancho de cada intervalo del histograma */ inc = (mayor - menor)/nIntervalos; /* Calcula los límites de los intervalos e inicializa a ceros el contador de número de datos que caen en el intervalo */ for(j = 0; j dato 2 * < 0 si el dato 1 < dato 2 *************************************************************/ void quicksort(void *base, int principio, int fin, int tamElem, int (* fcmp)(const void *pdato1, const void *pdato2)) { int pivote, i; /* Apuntador a bloque de memoria usado para intercambiar los elementos al estar ordenando */ void *temp; /* Si hay más de un elemento en el arreglo, encuentra el elemento pivote del arreglo */ if(principio < fin) {

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

417

/* Reserva espacio para un bloque de memoria usado para intercambiar los elementos al estar ordenando */ if((temp = malloc(tamElem)) == NULL) return; /* Selecciona el elemento pivote en forma aleatoria. Para ello se utiliza un generador de números pseudoaleatorios que genera el valor a seleccionar como pivote */ pivote = principio + rand() % (fin - principio); /* Particiona el arreglo en dos arreglos uno con los elementos de principio a pivote-1 y otro con los elementos de pivote a fin */ /* intercambia(base[principio], base[pivote]) */ memcpy(temp, (char *)base+principio*tamElem, tamElem); memcpy((char *)base+principio*tamElem, (char *)base+pivote*tamElem), tamElem); memcpy((char *)base+pivote*tamElem), temp, tamElem); pivote = principio; for(i = principio+1; i 3) { puts("\nUso: dirsort [] [/o[orden]]"); puts("\n\nLa opcion /o[orden]puede ser:"); puts("\n /o ordenado directo por el nombre"); puts("\n /on ordenado directo por el nombre"); puts("\n /o-n ordenado inverso por el nombre"); return 1; } /* Obtiene el directorio y el tipo de ordenamiento */ /* Si no hay argumentos en la línea de comando */ if(argc == 1) n = obtenDir("*.*", &dir); /* Si sólo hay un argumento en la línea de comando */ else if(argc == 2) { /* Si el argumento es la opción de ordenamiento */ if(argv[1][0] == '/') { n = obtenDir("*.*", &dir); orden = obtenOrden(argv[1]+1); } /* Si el argumento es el nombre de archivo */ else n = obtenDir(argv[1], &dir); } /* Si se provee nombre de archivo y opción de ordenamiento */ else { n = obtenDir(argv[1], &dir); if(argv[2][0] == '/') orden = obtenOrden(argv[2]+1); else { puts("\nParametro incorrecto"); return 1; } } if(!n) { puts("\nNo se encontro archivos o directorio"); return 0; } if(n == -1) { puts("\nMemoria insuficiente"); return 1; } if(orden == NO_VAL) { puts("\nOpcion invalida"); return 2; } /* Ordena el directorio */ switch(orden)

ITSON

Manuel Domitsu Kono

420

Estructuras de Datos en C

{ case DIRNOM: quicksort(dir, 0, n-1, sizeof(FFBLK), dircmp_dn); break; case INVNOM: quicksort(dir, 0, n-1, sizeof(FFBLK), dircmp_in); break; } /* Muestra el directorio */ despliegaDir(dir, n); /*Libera la memoria empleada para almacenar el directorio */ free(dir); return 0; } /************************************************************* * ORDEN obtenOrden(char *s) * * Esta función regresa el tipo de ordenamiento deseado. El * tipo de ordenamiento viene en la cadena s. *************************************************************/ ORDEN obtenOrden(char *s) { if(!strcmp(s, "o")) return DIRNOM; if(!strcmp(s, "on")) return DIRNOM; if(!strcmp(s, "o-n")) return INVNOM; return NO_VAL; } int obtenDir(char *nomDir, FFBLK **pdir) /************************************************************* * int obtenDir(char *nomDir, FFBLK **pdir) * * Esta función obtiene la lista de archivos del directorio * dado por nomDir y los almacena en el arreglo pdir. nomDir * es una cadena que puede contener el nombre de la unidad de * disco, ruta y nombre del (los) archivo(s) de los que se * quiere el directorio. Pueden usarse los caracteres comodín * ? y *. La función regresa el número de archivos en el * directorio. *************************************************************/ int obtenDir(char *nomDir, FFBLK **pdir) { int i, fin; FFBLK f; /* Determina el número de archivos en el directorio */ fin = findfirst(nomDir, &f, FA_DIREC); for(i = 0; !fin; i++) fin = findnext(&f); /* Si no hay archivos, termina */ if(!i) return(i); /* Pide un bloque de memoria para almacenar el directorio */ *pdir = (FFBLK *)malloc(i*sizeof(FFBLK)); if(*pdir == NULL) return(-1); /* Lee el directorio y lo guarda en pdir */ fin = findfirst(nomDir, &f, FA_DIREC); for(i = 0; !fin; i++) { *(*pdir + i) = f; fin = findnext(&f); } return(i); }

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

421

La función obtenOrden() determina el tipo de ordenamiento deseado a partir de la cadena de la línea de comando que empieza con el carácter '/' y que se le pasa a la función en el parámetro s. Para obtener el directorio de un disco se utilizan un par de funciones de la biblioteca de funciones de Turbo C: findfirst () y findnext() cuyos prototipos se encuentran en el archivo de encabezados dir.h. Las sintaxis de esas funciones son: int findfirst(const char *pathname, struct ffblk *ffblk , int attrib); int findnext(struct ffblk * ffblk ); La función findfirst() inicia la búsqueda del directorio de disco. pathname es una cadena que contiene el nombre de la unidad de disco, la ruta y el nombre del archivo a buscar con el siguiente formato: [unidad:][ruta\]nomArch El nombre del archivo puede contener caracteres comodín ? y *. Si se encuentra un archivo que corresponda a pathname, la función llena la estructura ffblk con la información del directorio para ese archivo. Si se emplean caracteres comodín la función sólo regresa el primer nombre de archivo. La definición de esta estructura se encuentra en el archivo dir.h y se describe más adelante. attrib es el byte de atributo de archivo de DOS y se utiliza para determinar qué archivos son tomados en cuenta en la búsqueda. Los atributos de archivo están definidos como macros en el archivo de encabezados dos.h y se muestran en la tabla 16-3. Tabla 16-3. Atributos de archivo Valor

Macro

0x00

Descripción Archivos ordinarios

0x01

FA_RDONLY

Atributo de solo lectura

0x02

FA_HIDDEN

Archivo oculto

0x04

FA_SYSTEM

Archivo de sistema

0x08

FA_LABEL

Etiqueta de Volumen

0x10

FA_DIREC

Directorio

0x20

FA_ARCH

Archivo

Si attrib vale 0, sólo se encuentran archivos ordinarios (aquellos que no están marcados como de sólo lectura, ocultos, de sistema, etiquetas de volumen o directorios). Si se especifica el atributo de etiqueta de volumen, sólo se regresará etiquetas de volumen (si están presentes). Cualquier otro atributo o combinación de atributos (oculto, de sistema o directorio) hará que se encuentren los archivos con esos atributos además de los archivos ordinarios. Por ejemplo si attrib toma el valor de FA_DIREC se desplegarán tanto los nombres de los subdirectorios como los nombres de los archivos ordinarios que concuerden con pathname. En cambio si attrib toma el valor de FA_HIDDEN | FA_SYSTEM se

ITSON

Manuel Domitsu Kono

422

Estructuras de Datos en C

desplegarán tanto los nombres de los archivos ordinarios como los ocultos y del sistema que concuerden con pathname. Suponiendo que pathname contiene caracteres comodines y que la llamada a la función findfirst() tuvo éxito, los siguientes archivos que concuerdan con pathname pueden encontrarse llamando repetidamente a la función findnext(). ffblk debe apuntar a la misma estructura con la que se llamó a findfirst () ya que esta estructura contiene la información necesaria para continuar la búsqueda. findfirst () y findnext() regresan 0 si tienen éxito en encontrar un archivo que concuerde con pathname. Cuando no se puedan encontrar más archivos o hay un error en el nombre del archivo las funciones regresan -1 y la variable global errno toma el valor de: ENOENT (o ENOFILE) ENMFILE

No existe archivo o directorio No hay más archivos

La estructura tipo ffblk en la que las funciones findfirst() y findnext() colocan la información sobre el directorio de un archivo está definida en el directorio de encabezados dir.h de la siguiente forma: struct ffblk { char char unsigned unsigned long char };

ff_reserved[21]; ff_attrib; ff_ftime; ff_fdate; ff_fsize; ff_name[13];

/* /* /* /* /*

Atributo de archivo */ Hora codificada */ Fecha codificada */ Tamaño en bytes */ Nombre */

La tablas 16-4 y 16-5 muestran cómo están codificadas la fecha y hora de creación o última modificación de un archivo: Tabla 16-4. Codificación de la fecha de un archivo. Bits

Contenido

0- 4

Día (0 - 31)

5- 8

Mes (0 - 12)

9 - 15

Año, relativo a 1980. Un valor de 0 significa 1980, 1 representa 1981, etc.

Tabla 16-5. Codificación de la hora de un archivo. Bits

ITSON

Contenido

0- 4

Segundos, en incrementos de 2 segundos (0 - 29)

5 - 10

Minutos (0 - 12)

11 - 15

Horas (0 - 23)

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

423

DIRSORT.C. Continuación. /************************************************************* * void despliegaDir(FFBLK *dir, int n) * * Esta función despliega la lista de archivos del directorio * en el formato: * * nombre.ext dd-mm-aa hh:mm[a|p] * * en el caso de directorios y * * nombre.ext tamaño dd-mm-aa hh:mm[a|p] * * en el caso de archivos. *************************************************************/ void despliegaDir(FFBLK *dir, int n) { int i; for(i = 0; i < n; i++) { /* Si es un directorio */ if(dir[i].ff_attrib == FA_DIREC) printf("\n%-12s %02u-%02u-%02u%02u:%02u%c", dir[i].ff_name, dir[i].ff_fdate & 0x1F, (dir[i].ff_fdate >> 5) & 0xF, ((dir[i].ff_fdate >> 9) & 0x7F) + 80, ((dir[i].ff_ftime >> 11) & 0x1F) % 12, ((dir[i].ff_ftime >> 5) & 0x3F), (((dir[i].ff_ftime >> 11) & 0x1F)/12? 'p': 'a')); /* Si es un archivo */ else printf("\n%-12s%14ld %02u-%02u-%02u %02u:%02u%c", dir[i].ff_name, dir[i].ff_fsize, dir[i].ff_fdate & 0x1F, (dir[i].ff_fdate >> 5) & 0xF, ((dir[i].ff_fdate >> 9) & 0x7F) + 80, ((dir[i].ff_ftime >> 11) & 0x1F) % 12, ((dir[i].ff_ftime >> 5) & 0x3F), (((dir[i].ff_ftime >> 11) & 0x1F)/12? 'p': 'a')); } printf("\n"); } /************************************************************* * int dircmp_dn(const void *pdato1, const void *pdato2) * * Esta función compara los nombres de dos archivos. Los * nombres de los archivos se encuentran en las estructuras * apuntadas por pdato1 y pdato2. La función regresa: * * 0 si nomArch1 == nomArch2 * (+) si nomArch1 > nomArch2 * (-) si nomArch1 < nomArch2 *************************************************************/ int dircmp_dn(const void *pdato1, const void *pdato2) { FFBLK *pd1 = (FFBLK *)pdato1, *pd2 = (FFBLK *)pdato2; return strcmp(pd1->ff_name, pd2->ff_name); }

ITSON

Manuel Domitsu Kono

424

Estructuras de Datos en C

/************************************************************* * int dircmp_in(const void *pdato1, const void *pdato2) * * Esta función compara los nombres de dos archivos. Los * nombres de los archivos se encuentran en las estructuras * apuntadas por pdato1 y pdato2. La función regresa: * * 0 si nomArch1 == nomArch2 * (-) si nomArch1 > nomArch2 * (+) si nomArch1 < nomArch2 *************************************************************/ int dircmp_in(const void *pdato1, const void *pdato2) { FFBLK *pd1 = (FFBLK *)pdato1, *pd2 = (FFBLK *)pdato2; return strcmp(pd2->ff_name, pd1->ff_name); }

Note que el formato para desplegar los datos de un directorio difiere del formato para desplegar los datos de un archivo ordinario. Las funciones dircmp_dn() y dircmp_in() son las que se le pasan a la función quicksort() como parámetro para ordenar el directorio en forma directa e inversa, respectivamente. Note que las funciones reciben como parámetros apuntadores de tipo void para que coincidan con la declaración de la función quicksort(). Ya dentro de la función esos apuntadores se convierten en apuntadores a estructuras de tipo ffblk para poder accesar al contenido de esas estructuras. Por último se utiliza la función strcmp() para comparar los nombre de archivo.

Ejercicio Sobre Apuntadores a Funciones Modifique la función busquedaBinaria() desarrollada en el Capítulo 15: Recursividad para que sea una función generalizada, es decir que trabaje con cualquier tipo de datos.

Listas Ligadas Las listas son colecciones ordenadas de objetos, el término ordenado aquí no significa ordenado de mayor a menor o de menor a mayor, sino que cada miembro a excepción del primero tiene un predecesor y cada miembro a excepción del último tienen un sucesor. Además si agregamos un objeto en determinada posición, el objeto que estaba en esa posición y todos los que le siguen se desplazan una posición. Las listas tienen las siguientes propiedades: •

Una lista puede tener cero o más elementos.



Podemos agregar un elemento a la lista, en cualquier posición.



Podemos borrar a cualquier elemento de la lista.

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C



Podemos accesar a cualquier elemento de la lista.



Podemos recorrer toda la lista visitando a cada elemento de ella.

425

Operaciones Primitivas con las Listas Las operaciones primitivas definidas para una lista son: Inicializar:

Vacía la lista.

Vacía:

Regresa verdadero si la lista está vacía.

Insertar:

Inserta un elemento en la lista en determinada posición.

Extraer:

Extrae el elemento de la lista que se encuentra en determinada posición.

Visitar:

Recorre la lista efectuando una operación sobre cada elemento de la lista.

Buscar:

Busca en la lista el primer elemento que cumpla con cierto criterio.

Implementación de una Lista en C La forma más sencilla de implementar una lista es mediante un arreglo. Sin embargo, esto presenta varios inconvenientes: Primero, los arreglos tienen un tamaño fijo y en muchos casos no sabemos de antemano el tamaño que tendrá una lista. Segundo, al extraer un elemento de la lista es necesario recorrer los demás elementos para ocupar el hueco dejado por el elemento que se extrajo, este proceso puede ser costoso en términos de tiempo de ejecución si la lista tiene muchos elementos y/o se extraen muchos elementos de la lista. Lo que deseamos es un mecanismo que nos permita obtener un bloque de memoria para un nuevo elemento sólo cuando sea necesario y regresar ese bloque de memoria cuando ya no lo necesitemos. Esto se logra con una lista ligada. Una lista ligada está compuesta de un conjunto de nodos, donde cada nodo tiene dos campos. El primero es el campo de información que contiene el o los datos reales y el segundo es el campo siguiente que es un apuntador al siguiente nodo de la lista. A diferencia de un arreglo, los nodos de una lista ligada no están necesariamente almacenados en localidades de memoria contigua por lo que para accesar a un determinado nodo de la lista debemos empezar al inicio de la lista y usar la información almacenada en los nodos de la lista para localizar el siguiente nodo. este proceso se repite hasta lo calizar el nodo deseado. Una lista ligada puede definirse formalmente como: • •

Una lista ligada es un apuntador a un nodo. Un nodo de una lista ligada tiene dos campos: 1. El campo de información, que es el dato del elemento. 2. El campo siguiente , el cual es una lista ligada.

ITSON

Manuel Domitsu Kono

426

Estructuras de Datos en C

La definición de una lista ligada es una definición recursiva. Una lista ligada es un apuntador a un nodo el cual a su vez apunta a una lista ligada. El caso base esta definición es una lista ligada nula. Una lista ligada nula se representa como un apuntador nulo como se muestra en la figura 16-6a. En la figura 166b se muestra una lista ligada con tres nodos. Note que el campo siguiente del último elemento de una lista ligada es un apuntador nulo. Esto nos permite identificar al último elemento de la lista ligada.

Figura 16-6 A continuación implementaremos una lista ligada en C en la que el campo de información de cada nodo es un entero. Posteriormente se generalizará para que contenga cualquier tipo de dato. Primero se define el tipo de dato que contendrá a un nodo. struct nodo { int info; struct nodo *pSig; };

/* Campo de información */ /* Campo siguiente */

En la definición anterior podríamos pensar que hay un error pues uno de los miembros de la estructura nodo, pSig hace referencia a la estructura que no acabamos de definir y por lo tanto no existe información sobre el tipo nodo. Sin embargo hay que notar que pSig no representa a una estructura tipo nodo sino un apuntador a la estructura y esto si se permite. Para simplificar las declaraciones de las variables y apuntadores a la estructura nodo, definiremos el siguiente alias: typedef struct nodo NODO;

/* Alias de la estructura nodo */

Además para declarar el apuntador a la lista creamos el siguiente alias: typedef NODO *LISTA;

/* Alias del apuntador a NODO

*/

Todas las funciones para el manejo de listas ligadas reciben como parámetros apuntadores del tipo LISTA o apuntadores a NODO. La operación para inicializar la lista está implementada por la función inicializarLista() cuyo código es

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

427

void inicializarLista(LISTA *pLista) { LISTA pTemp; /* Mientras haya nodos en la lista */ while(*pLista) { pTemp = *pLista; *pLista = (*pLista)->pSig; destruyeNodo(pTemp); } }

La función inicializarLista() elimina los nodos de la lista cuya dirección esta almacenada en la localidad dada por pLista, eliminando en forma repetitiva al nodo al inicio de la lista. En cada iteración realiza los siguientes tres pasos, mismos que se ilustran en la figura 16-7.

Figura 16-7 1. Guarda temporalmente la dirección del inicio de la lista en pTemp. Figuras 16-7a y 16-7d. 2. Mueve el apuntador de la lista para que apunte al siguiente nodo de la lista (o a NULL si se trata del último nodo de la lista). Figuras 16-7b y 16-7e. 3. Elimina el nodo llamando a la función destruyeNodo() la cual libera el bloque de memoria ocupado por el nodo. Figuras 16-7c y 16-7f. La función destruyeNodo() en este ejemplo sólo contiene una llamada a la función free() pero como veremos más adelante al implementar una lista generalizada, puede contener más instrucciones. void destruyeNodo(NODO *pNodo) { free(pNodo); }

ITSON

Manuel Domitsu Kono

428

Estructuras de Datos en C

Es importante notar que la función inicializarLista() recibe como parámetro un apuntador a LISTA, pero como el tipo LISTA es un apuntador, el parámetro pLista constituye la dirección de una variable de tipo apuntador. La función requiere la dirección de la variable que contiene la dirección de la lista para cambiar su valor a NULL para indicar que la lista está vacía. La operación vacía implementada por la función listaVacia() regresa un 1 si la lista dada por lista se encuentra vacía, 0 en caso contrario. int listaVacia(LISTA lista) { return lista == NULL; }

La operación insertar está implementada por la función insertarLista(). Esta función inserta el nodo apuntado por pNodo en la lista cuya dirección está almacenada en la localidad apuntada por pLista. pPos es un apuntador al nodo después del cual se va insertar el nodo. Si se desea insertar el dato al inicio de la lista, pPos debe valer NULL. La función no verifica que pPos sea una dirección de un nodo de la lista. El usuario debe asegurarse que pPos apunte a un nodo de la lista. El código de esta función se muestra a continuación void insertarLista(LISTA *pLista, NODO *pPos, NODO *pNodo) { /* Si la lista está vacía */ if(listaVacia(*pLista)) { *pLista = pNodo; return; } /* Si el nodo se va a insertar al inicio de la lista */ if(!pPos) { pNodo->pSig = *pLista; *pLista = pNodo; return; } /* Si se va a insertar después del nodo pPos */ pNodo->pSig = pPos->pSig; pPos->pSig = pNodo; }

Al querer insertar un nodo se pueden presentar tres casos. Caso I: La lista está vacía. En este caso solo es necesario hacer que el apuntador a la lista apunte al nodo a insertar. Figura 16-8. Caso II: El nodo se desea insertar al inicio de la lista. Figura 16-9a. Aquí se requieren dos pasos. 1. Hacer que el apuntador siguiente del nodo a insertar apunte al nodo al inicio de la lista. Figura 16-9b. 2. Hacer que el apuntador al inicio de la lista apunte al nodo a insertar. Figura 16-9c.

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

429

Figura 16-8

Figura 16-9 Caso III: El nodo se desea insertar después del nodo pPos. Figura 16-10a. Aquí, también se requieren dos pasos. 1. Hacer que el apuntador siguiente del nodo que se va a insertar, pNodo apunte al nodo en donde se desea insertar el nodo, el nodo siguiente a pPos. Figura 16-10b. 2. Hacer que el apuntador siguiente de pPos apunte al nodo a insertar. Figura 16-10c. Note que al igual que la función inicializarLista(), la función insertarLista() recibe como primer parámetro un apuntador a LISTA, esto es necesario para permitir que la función modifique la dirección de inicio de la lista en los casos en que la lista esté vacía o el nodo se inserte al inicio de la lista.

ITSON

Manuel Domitsu Kono

430

Estructuras de Datos en C

Figura 16-10 El nodo a insertar ya ha sido creado y la función insertarLista() recibe un apuntador a ese nodo, pNodo. Una implementación de la función para crear un nodo está dada por creaNodo(), la cual hace una petición dinámica de memoria llamando a la función malloc(), e inicializa su campo de información con el valor del dato y el campo siguiente con un apuntador nulo. Si hay éxito la función regresa un apuntador al nodo creado, NULL en caso contrario. NODO *creaNodo(int dato) { NODO *pNodo; /* Pide un bloque de memoria en forma dinámica */ if((pNodo = malloc(sizeof(NODO)))) { /* Almacena en el campo de información el dato */ pNodo->info = dato; /* Almacena en el campo siguiente un apuntador nulo */ pNodo->pSig = NULL; } return pNodo; }

La operación extraer se implementa mediante la función extraerLista(). Esta función extrae un nodo de la lista si no está vacía. pLista es la dirección de la localidad en la que está almacenada la dirección del inicio de la lista. pPos es un apuntador al nodo después del cual esta el nodo que se va extraer. Si se desea extraer el dato al inicio de la lista pPos debe valer NULL. La función no verifica que pPos sea una dirección de un nodo de la lista. El usuario debe asegurarse que pPos apunte a un nodo de la lista. El código de esta función se muestra a continuación

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

431

int extraerLista(LISTA *pLista, NODO *pPos, int *pDato) { NODO *pTemp; /* Si la lista esta vacía */ if(listaVacia(*pLista)) return 0; /* Si se va a extraer el primer elemento */ if(!pPos) { pTemp = *pLista; *pLista = (*pLista)->pSig; } /* Si se va a extraer un elemento intermedio */ else { pTemp = pPos->pSig; pPos->pSig = pTemp->pSig; } /* extrae el dato */ *pDato = pTemp->info; /* Libera el nodo */ destruyeNodo(pTemp); return 1; }

Al querer extraer un nodo se pueden presentar tres casos. Caso I: La lista está vacía. En este caso la función sólo regresa 0 indicando su fracaso. Caso II: El nodo se desea extraer está al inicio de la lista. Figura 16-11a. Aquí se requieren cuatro pasos.

Figura 16-11

ITSON

Manuel Domitsu Kono

432

Estructuras de Datos en C

1. Hacer que el apuntador pTemp apunte al nodo al inicio de la lista. Figura 16-11b. 2. Hacer que el apuntador al inicio de la lista apunte al siguiente nodo. Figura 16-11c. 3. Copia el campo de información del nodo a extraer a la localidad de memoria apuntada por pDato. Figura 16-11d. 4. Elimina el nodo llamando a la función destruyeNodo(), figura 16-11e, la cual libera el bloque de memoria ocupado por el nodo, figura 16-11f. Caso III: El nodo que se desea extraer está después del nodo pPos. Figura 16-12a. Aquí, también se requieren cuatro pasos.

Figura 16-12 1. Hacer que el apuntador pTemp apunte al nodo a extraer, el siguiente del apuntado por pPos. Figura 16-12b. 2. Hacer que el apuntador siguiente de pPos apunte al nodo siguiente del nodo apuntado por pTemp. Figura 16-12c.

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

433

3. Copia el campo de información del nodo a extraer a la localidad de memoria apuntada por pDato. Figura 16-12d. 4. Elimina el nodo apuntado por pTemp, llamando a la función destruyeNodo(), figura 1612e, la cual libera el bloque de memoria ocupado por el nodo, figura 16-12f. Note que al igual que las funciones inicializarLista() e insertarLista(), la función extraerLista() recibe como primer parámetro un apuntador a LISTA, esto es necesario para permitir que la función modifique la dirección de inicio de la lista en el caso en que el nodo a extraer esté al inicio de la lista. La operación visitar se implementa mediante la función visitarLista() la cual recorre la lista dada por lista. En cada uno de los nodos de la lista la función ejecuta la operación dada por la función fvisita(). void visitarLista(LISTA lista, void (* fvisita)(NODO *pNodo)) { /* Mientras no se llegue al final de la lista */ while(lista) { /* Visita al nodo. Ejecuta la función fvisita() sobre los datos del nodo */ fvisita(lista); /* Ve al siguiente nodo */ lista = lista->pSig; } }

La función fvisita(), proporcionada por el usuario, es una función de tipo void y que recibe como parámetro un apuntador al nodo sobre el que va a actuar. Por ejemplo supongamos que deseamos desplegar para cada nodo de la lista ligada, su dirección, el valor de su campo de información y la dirección del siguiente nodo, podríamos emplear la siguiente función: void despliegaNodo(NODO *pNodo) { printf("\nDir nodo: %p dato: %d sig: %p", pNodo, pNodo->info, pNodo->pSig); }

y la llamada a la función visitarLista() quedaría de la forma: visitarLista(lista, despliegaNodo);

La operación buscar se implementa mediante la función buscaLista() la cual busca en la lista dada por lista la primera ocurrencia de un nodo cuyo campo de información al compararse con llave cumple la condición establecida por la función fcmp(). La función regresa la dirección del nodo que contiene esa primera ocurrencia, NULL en caso de no encontrar un nodo que cumpla con la condición. pAnt apunta al nodo anterior al nodo con la primera ocurrencia. Si el nodo con la primera ocurrencia es el primer nodo de la lista, pAnt apunta a NULL. fcmp es un apuntador a la función utilizada para comparar la llave con los nodos de la lista. La función para comparar es suministrada por el usuario y es de tipo entero y recibe como parámetros: info, el campo de información de un nodo y la llave. La función fcmp() debe regresar 0 si el campo de información y la llave cumplen con la condición establecida por la función, diferente de cero en caso contrario.

ITSON

Manuel Domitsu Kono

434

Estructuras de Datos en C

NODO *buscaLista(LISTA lista, LISTA *pAnt, int llave, int (* fcmp)(int info, int llave)) { *pAnt = NULL; /* Mientras no se llegue al final de la lista */ while(lista) { /* Si la encontró */ if(!fcmp(llave, lista->info)) break; /* Avanza al siguiente nodo */ *pAnt = lista; lista = lista->pSig; } return lista; }

Implementación de una Lista Ligada Generalizada en C Al igual que como lo hicimos con las pilas y las colas, podemos modificar las funciones que implementan las operaciones de una lista ligada para que sean independientes del tipo de dato almacenado en la lista. Para lograr esto modificaremos el contenido de los nodos que forman la lista ligada para que el campo de información en lugar de estar formada por uno o más datos, sea un apuntador a un bloque con los datos, figura 16-13. Este apuntador será de tipo void y la información sobre el tamaño de los bloques de datos será proporcionado a las funciones que la requieran mediante un parámetro adicional.

Figura 16-13 Las funciones que implementan las operaciones para una lista ligada generalizada se desarrollan como un módulo. En el archivo de encabezados de este módulo, se presenta en el listado LISTA.H. Podemos ver en la definición de la estructura tipo nodo que el campo de información, pInfo, es un apuntador al bloque de datos. También podemos ver que los prototipos de algunas de las funciones se modificaron para reflejar el hecho de que ahora se va a manejar un apuntador void a los datos y un parámetro extra para el tamaño de los datos.

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

435

LISTA.H #ifndef LISTA_H #define LISTA_H struct nodo { void *pInfo; struct nodo *pSig; }; typedef struct nodo NODO; typedef struct nodo *LISTA; void inicializarLista(LISTA *pLista); int listaVacia(LISTA lista); void insertarLista(LISTA *pLista, NODO *pPos, NODO *pNodo); int extraerLista(LISTA *pLista, NODO *pPos, void *pDato, int tamDato); void visitarLista(LISTA lista, void (* fVisita)(NODO *pNodo)); NODO *buscaLista(LISTA lista, LISTA *pAnt, void *pLlave, int (* fcmp)(void *pInfo, void *pLlave)); NODO *creaNodo(void *pDato, int tamDato); void destruyeNodo(NODO *pNodo); #endif

El código de las funciones para la lista ligada generalizada está en LISTA.C. Las funciones inicializarLista(), listaVacia() e insertarLista() no sufren modificaciones de sus versiones que almacenan enteros.

LISTA.C /************************************************************* * LISTA.C * * Este módulo implementa las operaciones de una lista ligada * generalizada, esto es, que opera con cualquier tipo de dato. * Las funciones que implementan las operaciones de la lista * reciben como parámetros datos o apuntadores de tipo LISTA y * NODO definidos como: * * struct nodo * { * void pInfo; /* Campo de información */ * struct nodo *pSig; /* Campo siguiente */ * }; * * typedef struct nodo NODO; * typedef struct nodo *LISTA; * * Aunque una variable y un apuntador de tipo LISTA declaradas * como: * * LISTA lista; * LISTA *pLista; * * podrían haberse declarado como: * * NODO *lista; * NODO **pLista; * * sin necesidad de crear el tipo LISTA, el definir el tipo * LISTA permite utilizar a LISTA par hacer referencia a la

ITSON

Manuel Domitsu Kono

436

Estructuras de Datos en C

* lista y a NODO para hacer referencia a un nodo de la lista. * El uso del tipo LISTA simplifica las declaraciones como * la de NODO **plista a LISTA *pLista. *************************************************************/ #include #include #include "lista.h" /************************************************************* *void inicializarLista(LISTA *pLista) * * Esta función inicializa (vacía) una lista. pLista es un * apuntador al apuntador a la lista. *************************************************************/ void inicializarLista(LISTA *pLista) { LISTA pTemp; /* Mientras haya nodos en la lista */ while(*pLista) { pTemp = *pLista; *pLista = (*pLista)->pSig; destruyeNodo(pTemp); } } /************************************************************* * int listaVacia(LISTA lista) * * Esta función regresa un 1 si la lista está vacía, 0 en * caso contrario. lista es un apuntador a la lista. *************************************************************/ int listaVacia(LISTA lista) { return lista == NULL; } /************************************************************* * void insertarLista(LISTA *pLista, NODO *pPos, NODO *pNodo) * * Esta función inserta el nodo apuntado por pNodo en la lista * apuntada por el apuntador pLista. pPos es un apuntador al * nodo después del cual se va insertar el nodo. Si se desea * insertar el dato al inicio de la lista pPos debe valer NULL. * La función no verifica que pPos sea una dirección de un * nodo de la lista. *************************************************************/ void insertarLista(LISTA *pLista, NODO *pPos, NODO *pNodo) { /* Si la lista está vacía */ if(listaVacia(*pLista)) { *pLista = pNodo; return; } /* Si se va a insertar al inicio de la lista */ if(!pPos) { pNodo->pSig = *pLista; *pLista = pNodo; return; }

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

437

/* Si se va a insertar después del nodo pPos */ pNodo->pSig = pPos->pSig; pPos->pSig = pNodo; }

La función extraerLista() sí se modifica ya que en lugar de recibir un apuntador a entero, en donde se va a almacenar el dato extraído de la lista, recibe por separado la dirección de la localidad donde se va al almacenar el dato y el tamaño del dato en los parámetros pDato y tamDato. En el código de la función podemos ver que para copiar el dato del nodo a la localidad dada por pDato se utiliza la función memcpy() que nos copia el dato byte por byte.

LISTA.C. Continuación. /************************************************************* * int extraerLista(LISTA *pLista, NODO *pPos, void *pDato, * int tamDato) * * Esta función extrae un nodo de la lista si no está vacía. * pPos es un apuntador al nodo después del cual esta el nodo * que se va extraer. Si se desea extraer el dato al inicio de * la lista pPos debe valer NULL. pDato es la localidad de * memoria en la que se almacena el campo de información del * nodo extraído. tamDato es el tamaño en bytes del campo de * información del nodo. La función no verifica que nPos sea * una dirección de un nodo de la lista. *************************************************************/ int extraerLista(LISTA *pLista, NODO *pPos, void *pDato, int tamDato) { LISTA pTemp; /* Si la lista esta vacía */ if(listaVacia(*pLista)) return 0; /* Si se va a extraer el primer elemento */ if(!pPos) { pTemp = *pLista; *pLista = (*pLista)->pSig; } /* Si se va a extraer un elemento intermedio */ else { pTemp = pPos->pSig; pPos->pSig = pTemp->pSig; } /* Extrae el dato */ memcpy(pDato, pTemp->pInfo, tamDato); /* Libera el nodo */ destruyeNodo(pTemp); return 1; }

ITSON

Manuel Domitsu Kono

438

Estructuras de Datos en C

La función visitarLista() tampoco se modifica de su versión previa. La función buscaLista(), en cambio, requiere que el parámetro que representan la llave de búsqueda y los parámetros de la función empleada para comparar la llave con los nodos sean apuntadores void.

LISTA.C. Continuación. /************************************************************* * void visitarLista(LISTA lista, void (* fVisitar)(NODO *pNodo)) * * Esta función recorre la lista dada por lista. En cada uno * de los nodos de la lista la función ejecuta la operación * dada por la función fVisitar(). La función fVisitar() es * suministrada por el Usuario y es de tipo void y recibe como * parámetro un apuntador a NODO *************************************************************/ void visitarLista(LISTA lista, void (* fVisitar)(NODO *pNodo)) { /* Mientras no se llegue al final de la lista */ while(lista) { /* Opera sobre el nodo */ fVisitar(lista); /* Va al siguiente nodo */ lista = lista->pSig; } } /************************************************************* * NODO *buscaLista(LISTA lista, LISTA *pAnt, void *pLlave, * int (* fcmp)(void *pInfo, void *pLlave)) * * Esta función busca en la lista dada por lista la primera * ocurrencia de un nodo cuyo campo de información al * compararse con llave cumpla la condición establecida por la * función fcmp(). La función regresa la dirección del nodo * que contiene esa primera ocurrencia, NULL en caso de no * encontrar un nodo que cumpla con la condición. pAnt apunta * al nodo anterior al nodo con la primera ocurrencia. Si el * nodo con la primera ocurrencia es el primer nodo de la * lista, pAnt apunta a NULL. fcmp es un apuntador a la función * utilizada para comparar la llave con los nodos de la lista. * La función para comparar es suministrada por el usuario y es * de tipo entero y recibe como parámetros dos apuntadores void * a los datos a comparar: pInfo y pLlave que corresponden al * campo de información y a la llave. La función fcmp() debe * regresar 0 si el campo de información y la llave cumplen con * la condición establecida por la función, diferente de cero * en caso contrario. * *************************************************************/ NODO *buscaLista(LISTA lista, LISTA *pAnt, void *pLlave, int (* fcmp)(void *pInfo, void *pLlave)) { *pAnt = NULL; /* Mientras no se llegue al final de la lista */ while(lista) { /* Si la encontró */ if(!fcmp(lista->pInfo, pLlave)) break; /* avanza al siguiente nodo */ *pAnt = lista; lista = lista->pSig;

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

439

} return lista; } /************************************************************* * NODO *creaNodo(void *pDato, int tamDato) * * Esta función crea un nodo haciendo una petición dinámica de * memoria e inicializa su campo de información con el valor * de info y el campo siguiente con un apuntador nulo. *************************************************************/ NODO *creaNodo(void *pDato, int tamDato) { NODO *pNodo; /* Pide un bloque de memoria para el nodo, en forma dinámica */ if((pNodo = malloc(sizeof(NODO)))) { /* Pide un bloque de memoria para el dato, en forma dinámica */ if((pNodo->pInfo = malloc(tamDato))) /* Almacena en el campo de información el dato */ memcpy(pNodo->pInfo, pDato, tamDato); else return NULL; /* Almacena en el campo siguiente un apuntador nulo */ pNodo->pSig = NULL; } return pNodo; } /************************************************************* * void destruyeNodo(NODO *pNodo) * * Esta función libera el bloque de memoria ocupada por el * nodo. *************************************************************/ void destruyeNodo(NODO *pNodo) { /* Libera el bloque de memoria ocupada por el dato */ free(pNodo->pInfo); /* Libera el bloque de memoria ocupada por el nodo */ free(pNodo); }

Una de las funciones que más se ve modificada es la función creaNodo() ya que ya que en lugar de recibir un entero, el dato a almacenar en el nodo, recibe por separado la dirección de la localidad donde está el dato y el tamaño del dato en los parámetros pDato y tamDato. En el código de la función podemos ver que se requiere pedir dos bloques de memoria en forma dinámica: Una para almacenar el nodo y otra para almacenar el dato. Por último la función destruyeNodo() requiere liberar los dos bloques de memoria pedidos por la función creaNodo(). El siguiente listado, DEMO_LL.C, es un módulo que implementa un programa de demostración que muestra el comportamiento de una lista ligada. Este módulo debe ligarse a los módulos: LISTA y UTILS.

ITSON

Manuel Domitsu Kono

440

Estructuras de Datos en C

DEMO_LL.C /************************************************************* * DEMO_LL.C * * Este programa ilustra el comportamiento de una lista ligada * de enteros. *************************************************************/ #include #include #include #include



#include "utils.h" #include "lista.h" void despliegaPantallaInicial(void); void borraLista(void); void despliegaNodo(NODO *pNodo); int igual(void *pInfo, void *pLlave); int mayor(void *pInfo, void *pLlave); LISTA lista = NULL; int main(void) { NODO *pNodo, *pPos, *pAnt; int dato; char operador, sdato[6]; clrscr(); inicializarLista(&lista); despliegaPantallaInicial(); while(1) { gotoxy(28,10); printf(" "); gotoxy(51,10); printf(" "); gotoxy(28,10); if((operador = getch()) == ESC) break; if(!operador) { switch(operador = getch()) { case INS: printf("Insertar"); gotoxy(51,10); gets(sdato); dato = atoi(sdato); pNodo = creaNodo(&dato, sizeof(int)); if(listaVacia(lista)) pAnt = NULL; else pPos = buscaLista(lista, &pAnt, &dato, mayor); insertarLista(&lista, pAnt, pNodo); break; case DEL: printf("Extraer "); if(listaVacia(lista)) msj_error(0,25,"Lista vacía!, [Enter]", "\r",ATTR(RED, LIGHTGRAY,0)); else { gotoxy(51,10); gets(sdato); dato = atoi(sdato); if(!(pPos = buscaLista(lista, &pAnt, &dato, igual))) msj_error(0,25,"Valor inexistente!,\ [Enter]", "\r", ATTR(RED, LIGHTGRAY,0)); else extraerLista(&lista, pAnt, &dato, sizeof(int)); }

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

441

break; } } borraLista(); visitarLista(lista, despliegaNodo); putchar('\n'); } inicializarLista(&lista); visitarLista(lista, despliegaNodo); putchar('\n'); return 0; } /************************************************************* * void despliegaPantallaInicial(void) * * Esta función despliega la explicación de lo que hace el * programa y las instrucciones de su uso. *************************************************************/ void despliegaPantallaInicial(void) { clrscr(); gotoxy(5,3); printf("Este programa demuestra el comportamiento de una"); printf(" lista ligada. En esta"); gotoxy(5,4); printf("lista se almacenan enteros los cuales son"); printf(" acomodados en orden ascendente."); gotoxy(5,5); printf("Hay dos operaciones: Para insertar un numero a "); printf("la lista presiona la tecla"); gotoxy(5,6); printf("[Insert], luego el numero a insertar. Para "); printf("extraer un numero de la lista"); gotoxy(5,7); printf("presiona la tecla [Supr], luego el numero a extraer."); printf(" Para terminar presiona"); gotoxy(5,8); printf("la tecla [Esc]."); gotoxy(15,10); printf("Operacion [ ] Valor [ ]"); } /************************************************************* * void despliegaNodo(NODO *pNodo) * * Esta función despliega para un nodo, su campo de información * y su campo siguiente. *************************************************************/ void despliegaNodo(NODO *pNodo) { static x = 10, y = 13; /* Si es el primer nodo de la lista */ if(pNodo == lista) { x = 10; y = 13; } /* Si se va a saltar de renglón */ if(x == 74) { x = 10; y += 3; } /* Despliega el contenido del nodo en un recuadro */

ITSON

Manuel Domitsu Kono

442

Estructuras de Datos en C

gotoxy(x,y); putch(0x10); gotoxy(x+1,y-1); printf("+-------------+"); gotoxy(x+1,y); printf("%6d%6p", *(int *)(pNodo->pInfo),pNodo->pSig); gotoxy(x+1,y+1); printf("+-------------+"); x += 16; /* Si va a desplegar otra pantalla */ if(pNodo->pSig && y == 22 && x == 74) { msj_error(0,25,"[Enter] para continuar", "\r", ATTR(RED, LIGHTGRAY,0)); x = 10; y = 13; borraLista(); } } /************************************************************* * void borraLista(void) * * Borra de la pantalla la lista. *************************************************************/ void borraLista(void) { int i; for(i = 12; i < 25; i++) { gotoxy(1,i); clreol(); } } /************************************************************* * int igual(void *pInfo, void *pLlave) * * Esta función regresa 0 si info y llave son iguales, diferente * de cero en caso contrario. *************************************************************/ int igual(void *pInfo, void *pLlave) { return *(int *)pInfo - *(int *)pLlave; } /************************************************************* * int mayor(void *pInfo, void *pLlave) * * Esta función regresa 0 si info > llave, diferente de cero * en caso contrario. *************************************************************/ int mayor(void *pInfo, void *pLlave) { return *(int *)pInfo pIzq = inicializarArbol(raiz->pIzq);

ITSON

Manuel Domitsu Kono

448

Estructuras de Datos en C

/* Inicializa (vacía) el subárbol derecho */ raiz->pDer = inicializarArbol(raiz->pDer); /* Libera el nodo ocupado por la raíz */ destruyeHoja(raiz); } /* Hace el árbol nulo */ return NULL; }

La función elimina los nodos del árbol binario cuya dirección está dada por raíz. Al terminar la función regresa NULL, valor que se debe asignar al apuntador al árbol para indicar que está vacío, esto se puede lograr mediante la siguiente llamada a la función: raiz = inicializarArbol(raiz);

La función es recursiva y para entender su funcionamiento hay que recordar que un árbol binario es un nodo que apunta a dos subárboles, esto es cada nodo de un árbol es la raíz del árbol formado por sus dos subárboles. La definición recursiva de la función inicializarArbol() es la siguiente: Para cada nodo del árbol, empezando por su raíz: Caso base: Si el nodo es nulo, regresa NULL. Caso inductivo: Elimina el subárbol izquierdo del nodo. Elimina el subárbol derecho del nodo. Elimina el nodo. Podemos ver que para eliminar un árbol, la función inicializarArbol(), debe eliminar primero su subárbol izquierdo, luego su subárbol derecho y luego su raíz. Como el subárbol izquierdo también es un árbol, primero debe eliminar su subárbol izquierdo, luego su subárbol derecho y luego su raíz, etc. El proceso anterior termina hasta que la función llega a la hoja que se encuentra más a la izquierda del árbol y como ésta ya no tiene subárboles, puede ser eliminada, a continuación elimina la siguiente hoja más a la izquierda y así consecutivamente, como se ilustra en la secuencia mostrada en la figura 16-17. Para eliminar un nodo del árbol, la función inicializarArbol() llama a la función destruyeHoja() que en el caso de un árbol que almacena enteros sólo requiere a una llamada a la función free(). En el caso de un árbol generalizado, como veremos más adelante, la función destruyeHoja() tendrá más instrucciones. void destruyeHoja(HOJA *pHoja) { free(pHoja); }

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

449

Figura 16-17 La operación vacía se implementa mediante la función arbolVacio() la cual regresa un 1 si el árbol dado por raiz está vacío, 0 en caso contrario. El código de la función arbolVacio() es el siguiente. int arbolVacio(ARBOL raiz) { return raiz == NULL; }

La operación insertar se implementa mediante la función insertarArbol(), cuyo código se muestra a continuación:

ITSON

Manuel Domitsu Kono

450

Estructuras de Datos en C

ARBOL insertarArbol(ARBOL raiz, HOJA *pHoja, int (* fcmp)(int info1, int info2)) { int rc; /* Si el árbol/subárbol no está vacío */ if(raiz) { /* Compara el nodo a insertar con raíz */ rc = fcmp(raiz->info, pHoja->info); /* Si el nodo a insertar es menor que la raíz, inserta en el subárbol izquierdo */ if(rc > 0) raiz->pIzq = insertarArbol(raiz->pIzq, pHoja, fcmp); /* Si el nodo a insertar es mayor que la raíz, inserta en el subárbol derecho */ else if(rc < 0) raiz->pDer = insertarArbol(raiz->pDer, pHoja, fcmp); return(raiz); } /* Si el árbol/subárbol está vacío */ return pHoja; }

La función insertarArbol() inserta el nodo apuntado por pHoja en el árbol apuntado por raíz. El parámetro fcmp es un apuntador a la función utilizada para comparar el nodo a insertar y la raíz del árbol. Esta función es necesaria ya que hay que recordar que en un árbol binario los nodos están ordenados. La función para comparar es suministrada por el usuario y es de tipo entero y recibe como parámetros los campos de información de los nodos a comparar: info1 e info2 que corresponden a los de la raíz y del nodo a insertar, respectivamente. La función para comparar debe regresar: 0 si info1 == info2 (+) si info1 > info2 (-) si info1 < info2 Al terminar la función regresa un apuntador al árbol, valor que puede ser diferente al valor del parámetro raíz, si inicialmente el árbol estaba vacío. La llamada a la función tiene la forma: raiz = insertarArbol(raiz, pNodo, fcmp);

La función insertarArbol() también es una función recursiva y puede definirse recursivamente como: Caso base: Si el subárbol es nulo, inserta ahí el nodo. Caso inductivo: Si el nodo a insertar es menor que la raíz, inserta el nodo en el subárbol izquierdo.

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

451

Si el nodo a insertar es mayor que la raíz, inserta el nodo en el subárbol derecho. De la definición anterior, caso base, podemos notar que el nodo siempre se inserta en un subárbol nulo, esto es, en la parte inferior del árbol como una hoja y nunca entre dos nodos. El nodo a insertar ya ha sido creado y la función insertarArbol() recibe un apuntador a ese nodo, pHoja. Una implementación de la función para crear un nodo está dada por creaHoja(), la cual hace una petición dinámica de memoria llamando a la función malloc(), e inicializa su campo de información con el valor de dato y los apuntadores a los subárboles izquierdo y derecho a apuntadores nulos. Si hay éxito la función regresa un apuntador al nodo creado, NULL en caso contrario. HOJA *creaHoja(int dato) { HOJA *pHoja; /* Pide un bloque de memoria en forma dinámica */ if((pHoja = malloc(sizeof(HOJA)))) { /* Almacena en el campo de información el dato */ pHoja->info = dato; /* Almacena en los apuntadores a los subárboles izquierdo y derecho apuntadores nulos */ pHoja->pIzq = pHoja->pDer = NULL; } return pHoja; }

La operación extraer se implementa mediante la función extraerArbol() la cual extrae el nodo cuya dirección se encuentra almacenada en la dirección dada por pPos. pDato es la localidad de memoria en la que se almacena el campo de información del nodo extraído. La función regresa 1 si hubo éxito, 0 en caso de que el nodo a extraer sea un nodo nulo. La dirección almacenada en pPos debe ser la dirección de un nodo del árbol ya que la función no verifica esto. El código de la función es int extraerArbol(ARBOL *pPos, int *pDato) { HOJA *pTemp; /* Si el nodo a extraer es nulo */ if(!(*pPos)) return 0; /* Si el subárbol derecho del nodo a extraer es nulo */ if(!(*pPos)->pDer) { pTemp = *pPos; /* Conecta el subárbol izquierdo del nodo a extraer */ *pPos = (*pPos)->pIzq; } /* Si el subárbol izquierdo del nodo a extraer es nulo */ else if(!(*pPos)->pIzq) { pTemp = *pPos; /* Conecta el subárbol derecho del nodo a extraer */ *pPos = (*pPos)->pDer; }

ITSON

Manuel Domitsu Kono

452

Estructuras de Datos en C

/* Si ninguno de los subárboles del nodo a extraer son nulos */ else { /* Encuentra el nodo más a la izquierda del subárbol derecho */ for(pTemp = (*pPos)->pDer; pTemp->pIzq; pTemp = pTemp->pIzq); /* Conecta el subárbol izquierdo del nodo a extraer a la hoja más a la izquierda del subárbol derecho */ pTemp->pIzq = (*pPos)->pIzq; pTemp = *pPos; /* Conecta el subárbol derecho del nodo a extraer */ *pPos = (*pPos)->pDer; } /* Extrae el dato */ *pDato = pTemp->info; /* Libera el nodo */ destruyeHoja(pTemp); return 1; }

La sintaxis para llamar a la función anterior es: HOJA **pPos; int dato; extraerArbol(pPos, &dato);

Al querer extraer un nodo del árbol se pueden presentar cuatro casos. Caso I:

El nodo a extraer es un nodo nulo. En este caso la función sólo regresa 0 indicando su fracaso.

Caso II:

El subárbol derecho del nodo a extraer es un subárbol nulo, figura 16-18a. Aquí se requieren cuatro pasos: 1. Hacer que el apuntador pTemp apunte al nodo a eliminar. Figura 16-18b. 2. Hacer que el apuntador al nodo a eliminar apunte a su subárbol izquierdo. Figura 16-18c. 3. Copia el campo de información del nodo a extraer a la localidad apuntada por pDato. Figura 16-18d. 4. Elimina el nodo llamando a la función destruyeHoja(), figura 16-18e, la cual libera el bloque de memoria ocupado por el nodo, figura 16-18f.

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

453

Figura 16-18 Caso III:

ITSON

El subárbol izquierdo del nodo a extraer es un subárbol nulo, figura 16-19a. Aquí se requieren cuatro pasos:

Manuel Domitsu Kono

454

Estructuras de Datos en C

Figura 16-19 1. Hacer que el apuntador pTemp apunte al nodo a eliminar. Figura 16-19b. 2. Hacer que el apuntador al nodo a eliminar apunte a su subárbol derecho. Figura 1619c. 3. Copia el campo de información del nodo a extraer a la localidad apuntada por pDato. Figura 16-19d. 4. Elimina el nodo llamando a la función destruyeHoja(), figura 16-19e, la cual libera el bloque de memoria ocupado por el nodo, figura 16-19f.

ITSON

Manuel Domitsu Kono

Capítulo 16

Caso IV:

Estructuras de Datos en C

455

Ninguno de los subárboles del nodo a extraer son nulos, figura 16-20a. Aquí se requieren cuatro pasos:

Figura 16-20 1. Hacer que el apuntador pTemp apunte al nodo más izquierdo del subárbol derecho del nodo a eliminar. Figura 16-20b. 2. Conectar el subárbol izquierdo del nodo a eliminar al nodo más izquierdo del subárbol derecho del nodo a eliminar. Figura 16-20c.

ITSON

Manuel Domitsu Kono

456

Estructuras de Datos en C

3. Hacer que el apuntador pTemp apunte al nodo a eliminar. Figura 16-20d. 4. Hacer que el apuntador al nodo a eliminar apunte a su subárbol derecho. Figura 1620e. 5. Copia el campo de información del nodo a extraer a la localidad apuntada por pDato. Figura 16-20f. 6. Elimina el nodo llamando a la función destruyeHoja(), figura 16-20g, la cual libera el bloque de memoria ocupado por el nodo, figura 16-20h. La operación visitar un árbol implica recorrer el árbol efectuando una operación sobre cada elemento del árbol. Por lo general se desea recorrer los nodos siguiendo un patrón regular. En cada nodo hay tres tareas a realizar en un determinado orden. Visitar el nodo, visitar el subárbol izquierdo y visitar el subárbol derecho. Si imponemos la restricción de visitar primero el subárbol izquierdo y luego el derecho tenemos tres formas de recorrer el árbol: I

Visitar el árbol en preorden en la que las tareas se realizan en el siguiente orden: 1. 2. 3.

Visita el nodo Visita el subárbol izquierdo. Visita el subárbol derecho.

II Visitar el árbol en orden en la que las tareas se realizan en el siguiente orden: 1. 2. 3.

Visita el subárbol izquierdo. Visita el nodo Visita el subárbol derecho.

III Visitar el árbol en postorden en la que las tareas se realizan en el siguiente orden: 1. 2. 3.

Visita el subárbol izquierdo. Visita el subárbol derecho. Visita el nodo

El código de las funciones que implementan las tres formas de visitar un árbol dado por raiz se muestran a continuación. En cada uno de los nodos del árbol la función ejecuta la operación dada por la función fVisita(). void visitarArbolPreOrden(ARBOL raiz, void (* fVisita)(HOJA *pHoja)) { /* Si el árbol no está vacío */ if(raiz) { /* Visita la raíz */ fVisita(raiz); /* Visita el subárbol izquierdo */ visitarArbolPreOrden(raiz->pIzq, fVisita);

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

457

/* Visita el subárbol derecho */ visitarArbolPreOrden(raiz->pDer, fVisita); } } void visitarArbolEnOrden(ARBOL raiz, void (* fVisita)(HOJA *pHoja)) { /* Si el árbol está vacío */ if(raiz) { /* Visita el subárbol izquierdo */ visitarArbolEnOrden(raiz->pIzq, fVisita); /* Visita la raíz */ fVisita(raiz); /* Visita el subárbol derecho */ visitarArbolEnOrden(raiz->pDer, fVisita); } } void visitarArbolPostOrden(ARBOL raiz, void (* fVisita)(HOJA *pHoja)) { /* Si el arbol está vacío */ if(raiz) { /* Visita el subárbol izquierdo */ visitarArbolPostOrden(raiz->pIzq, fVisita); /* Visita el subárbol derecho */ visitarArbolPostOrden(raiz->pDer, fVisita); /* Visita la raíz */ fVisita(raiz); } }

Las tres funciones anteriores son recursivas. Por ejemplo, la definición recursiva de la función visitarArbolPreOrden() es: Caso base: Si el árbol es nulo, termina. Caso inductivo: Visita el nodo Visita el subárbol izquierdo en preorden. Visita el subárbol derecho en preorden. Las otras dos definiciones recursivas son similares. La función fvisita(), proporcionada por el usuario, es una función de tipo void y que recibe como parámetro un apuntador al nodo sobre el que va a actuar. Por ejemplo supongamos que deseamos desplegar para cada nodo del árbol, su dirección, el valor de su campo de información y las direcciones de sus subárboles izquierdo y derecho, podríamos emplear la siguiente función:

ITSON

Manuel Domitsu Kono

458

Estructuras de Datos en C

void despliegaHoja(HOJA *pHoja) { printf("\nDir nodo: %p dato: %d izq: %p der: %p", pHoja, pHoja->info, pHoja->pIzq, pHoja->pDer); }

La operación buscar se implementa mediante la función buscaArbol() la cual busca en el árbol cuya dirección está almacenada en la localidad apuntada por pRaiz, el nodo cuyo campo de información es igual a llave. La función regresa la dirección de la localidad de memoria donde está almacenada la dirección del nodo, si existe, NULL en caso contrario. fcmp es un apuntador a la función utilizada para comparar el campo de información de la raíz con la llave. La función para comparar es suministrada por el usuario y es de tipo entero y recibe como parámetros: info, el campo de información de la raíz y llave, la llave. La función para comparar debe regresar: 0 si info == llave (+) si info > llave (-) si info < llave El código de la función es: HOJA **buscaArbol(ARBOL *pRaiz, int llave, int (* fcmp)(int info, int llave)) { int rc; HOJA **ppPos = pRaiz; /* Si el árbol no está vacío */ if(*pRaiz) { /* Compara llave con nodo */ rc = fcmp((*pRaiz)->info, llave); /* Si no, busca en el subárbol izquierdo */ if(rc > 0) ppPos = buscaArbol(&((*pRaiz)->pIzq), llave, fcmp); /* si no, busca el subárbol derecho */ else if(rc < 0) ppPos = buscaArbol(&((*pRaiz)->pDer), llave, fcmp); } return ppPos; }

La sintaxis para llamar a la función anterior es: HOJA **pPos; int dato; pPos = buscaArbol(&raiz, 3, fcmp); extraerArbol(pPos, &dato);

La función anterior es una función recursiva. La definición recursiva de la función es:

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

459

Caso base: Si la raíz del árbol/subárbol es nula, regresa NULL. Si info == llave regresa la posición de la raíz. Caso inductivo: Si info > llave busca en el subárbol izquierdo. Si info < llave busca en el subárbol derecho.

Implementación de un Árbol Binario de Búsqueda Generalizado en C Al igual que como lo hicimos con la lista ligada, podemos modificar las funciones que implementan las operaciones de un árbol binario de búsqueda para que sean independientes del tipo de dato almacenado en el árbol. Para lograr esto modificaremos el contenido de los nodos que forman el árbol para que el campo de información en lugar de estar formada por uno o más datos, sea un apuntador a un bloque con los datos. Este apuntador será de tipo void y la información sobre el tamaño de los bloques de datos será proporcionado a las funciones que la requieran mediante un parámetro adicional. Las funciones que implementan las operaciones para un árbol binario de búsqueda generalizado se desarrollan como un módulo. En el archivo de encabezados de este módulo, se presenta en el listado ARBOL.H. Podemos ver en la definición de la estructura tipo hoja que el campo de información, pInfo, es un apuntador al bloque de datos. También podemos ver que los prototipos de algunas de las funciones se modificaron para reflejar el hecho de que ahora se va a manejar un apuntador void a los datos y un parámetro extra para el tamaño de los datos.

ARBOL.H #ifndef ARBOL_H #define ARBOL_H struct hoja { void *pInfo; /* Campo de (apuntador) información */ struct hoja *pIzq; /* Apuntador al subárbol izquierdo */ struct hoja *pDer; /* Apuntador al subárbol derecho */ }; typedef struct hoja HOJA; /* Alias de la estructura hoja */ typedef struct hoja *ARBOL;/* Alias del apuntador a la raíz del árbol */ ARBOL inicializarArbol(ARBOL raiz); int arbolVacio(ARBOL raiz); ARBOL insertarArbol(ARBOL raiz, HOJA *pHoja, int (* fcmp)(void *pInfo, void *pLlave)); int extraerArbol(ARBOL *pPos, void *pDato, int tamDato); void visitarArbolPreOrden(ARBOL raiz, void (* fVisita)(HOJA *pHoja)); void visitarArbolEnOrden(ARBOL raiz, void (* fVisita)(HOJA *pHoja)); void visitarArbolPostOrden(ARBOL raiz, void (* fVisita)(HOJA *pHoja)); HOJA **buscaArbol(ARBOL *pRaiz, void *pLlave, int (* fcmp)(void *pInfo, void *pLlave));

ITSON

Manuel Domitsu Kono

460

Estructuras de Datos en C

HOJA *creaHoja(void *pInfo, int tamDato); void destruyeHoja(HOJA *pHoja); #endif

El código de las funciones para un árbol binario de búsqueda generalizado está en ARBOL.C. Las funciones inicializarArbol() y arbolVacio() no sufren modificaciones de sus versiones que almacenan enteros. En cambio, la función insertarArbol() generalizada difiere de su correspondiente para enteros en los parámetros de la función utilizada para comparar los nodos, ya que en este caso los parámetros son apuntadores a void para permitir que apunten a cualquier tipo de dato.

ARBOL.C. /************************************************************* * ARBOL.C * * Este módulo implementa las operaciones de un árbol binario * generalizado, esto es, que opera con cualquier tipo de dato. * Las funciones que implementan las operaciones del árbol * reciben como parámetros datos o apuntadores de tipo ARBOL y * HOJA definidos como: * * struct hoja * { * void *pInfo; Apuntador al campo de información * struct hoja *pIzq; Apuntador al subárbol izquierdo * struct hoja *pIzq; Apuntador al subárbol derecho * }; * * typedef struct hoja HOJA; Alias de la estructura hoja * typedef struct hoja *ARBOL; Alias del apuntador a la * raíz del arbol *************************************************************/ #include #include #include "arbol.h" /************************************************************* * ARBOL inicializarArbol(ARBOL raiz) * * Esta función inicializa (vacía) un arbol. raíz es un * apuntador al árbol *************************************************************/ ARBOL inicializarArbol(ARBOL raiz) { /* Si el árbol no está vacío */ if(raiz) { /* Inicializa (vacía) el subárbol izquierdo */ raiz->pIzq = inicializarArbol(raiz->pIzq); /* Inicializa (vacía) el subárbol izquierdo */ raiz->pDer = inicializarArbol(raiz->pDer); /* Libera el nodo ocupado por la raíz */ destruyeHoja(raiz); } /* Hace que el apuntador al árbol sea nulo */ return NULL; }

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

461

/************************************************************* *int arbolVacio(ARBOL raiz) * * Esta función regresa un 1 si el árbol está vacío, 0 en caso * contrario. raíz es un apuntador al árbol. *************************************************************/ int arbolVacio(ARBOL raiz) { return raiz == NULL; } /************************************************************* * ARBOL insertarArbol(ARBOL raiz, HOJA *pHoja, * int (* fcmp)(void *pInfo1, void *pInfo2)) * * Esta función inserta el nodo apuntado por pHoja en el árbol * apuntado por raíz. fcmp es un apuntador a la función * utilizada para comparar el nodo a insertar y la raíz del * árbol. La función para comparar es suministrada por el * usuario y es de tipo entero y recibe como parámetros * apuntadores a los campos de información comparar: pInfo1 e * pInfo2 que corresponden a los de la raíz y del nodo a * insertar, respectivamente. La función para comparar debe * regresar: * * 0 si info1 == info2 * (+) si info1 > info2 * (-) si info1 < info2 *************************************************************/ ARBOL insertarArbol(ARBOL raiz, HOJA *pHoja, int (* fcmp)(void *pInfo1, void *pInfo2)) { /* Si el árbol no está vacío */ if(raiz) { /* Si el nodo a insertar es menor que la raíz inserta en el subárbol izquierdo */ if(fcmp(raiz->pInfo, pHoja->pInfo) > 0) raiz->pIzq = insertarArbol(raiz->pIzq, pHoja, fcmp); /* Si no inserta en el subárbol derecho */ else raiz->pDer = insertarArbol(raiz->pDer, pHoja, fcmp); return(raiz); } return pHoja; }

La función extraerArbol() también se modifica ya que en lugar de recibir un apuntador a entero, en donde se va a almacenar el dato extraído del árbol, recibe por separado la dirección de la localidad donde se va al almacenar el dato y el tamaño del dato en los parámetros pDato y tamDato. En el código de la función podemos ver que para copiar el dato del nodo a la localidad dada por pDato se utiliza la función memcpy() que nos copia el dato byte por byte.

ARBOL.C. Continuación. /************************************************************* * int extraerArbol(ARBOL *pPos, void *pDato, int tamDato) *

ITSON

Manuel Domitsu Kono

462

Estructuras de Datos en C

* Esta función extrae un nodo del árbol si el nodo no está * vacío. pPos es un apuntador al nodo que se va extraer. * pDato es la localidad de memoria en la que se almacena el * campo de información del nodo extraído. La función no * verifica que pPos sea una dirección de un nodo del árbol. *************************************************************/ int extraerArbol(ARBOL *pPos, void *pDato, int tamDato) { HOJA *pTemp; /* Si el nodo está vacío */ if(!(*pPos)) return 0; /* Si el subárbol derecho del nodo a extraer está vacío */ if(!(*pPos)->pDer) { pTemp = *pPos; /* Conecta el subárbol izquierdo del nodo a extraer */ *pPos = (*pPos)->pIzq; } /* Si el subárbol izquierdo del nodo a extraer está vacío */ else if(!(*pPos)->pIzq) { pTemp = *pPos; /* Conecta el subárbol derecho del nodo a extraer */ *pPos = (*pPos)->pDer; } /* Si ninguno de los subárboles del nodo a extraer está vacío */ else { /* Encuentra la hoja más a la izquierda del subárbol derecho */ for(pTemp = (*pPos)->pDer; pTemp->pIzq; pTemp = pTemp->pIzq); /* Conecta el subárbol izquierdo del nodo a extraer a la hoja más a la izquierda del subárbol derecho */ pTemp->pIzq = (*pPos)->pIzq; pTemp = *pPos; /* Conecta el subárbol derecho del nodo a extraer */ *pPos = (*pPos)->pDer; } /* Extrae el dato */ memcpy(pDato, pTemp->pInfo, tamDato); /* Libera el nodo */ destruyeHoja(pTemp); return 1; }

La funciones visitarArbolPreOrden(), visitarArbolEnOrden() y visitarArbolPostOrden(), tampoco se modifican de sus versiones para enteros.

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

463

ARBOL.C. Continuación. /************************************************************* * void visitarArbolPreOrden(ARBOL raiz, * void (* fVisita)(HOJA *pHoja)) * * Esta función recorre el árbol binario dado por raíz en * preorden, esto es, primero la raíz, luego el subárbol * izquierdo y por último el subárbol derecho. En cada uno de * los nodos del árbol la función ejecuta la operación dada * por la función fVisita() proporcionada por el usuario. *************************************************************/ void visitarArbolPreOrden(ARBOL raiz, void (* fVisita)(HOJA *pHoja)) { /* Si el árbol no está vacío */ if(raiz) { /* Visita la raíz */ fVisita(raiz); /* Visita el subárbol izquierdo */ visitarArbolPreOrden(raiz->pIzq, fVisita); /* Visita el subárbol derecho */ visitarArbolPreOrden(raiz->pDer, fVisita); } } /************************************************************* * void visitarArbolEnOrden(ARBOL raiz, * void (* fVisita)(HOJA *pHoja)) * * Esta función recorre el árbol binario dado por raíz en * orden, esto es, primero el subárbol izquierdo, luego la * raíz y por último el subárbol derecho. En cada uno de los * nodos del árbol la función ejecuta la operación dada por * función fVisita() proporcionada por el usuario. *************************************************************/ void visitarArbolEnOrden(ARBOL raiz, void (* fVisita)(HOJA *pHoja)) { /* Si el árbol no está vacío */ if(raiz) { /* Visita el subárbol izquierdo */ visitarArbolEnOrden(raiz->pIzq, fVisita); /* Visita la raíz */ fVisita(raiz); /* Visita el subárbol derecho */ visitarArbolEnOrden(raiz->pDer, fVisita); } } /************************************************************* * void visitarArbolPostOrden(ARBOL raiz, * void (* fVisita)(HOJA *pHoja)) * * Esta función recorre el árbol binario dado por raíz en * orden, esto es, primero el subárbol izquierdo, luego el * subárbol derecho y por último la raíz. En cada uno de los * nodos del árbol la función ejecuta la operación dada por * la función fVisita() proporcionada por el usuario. *************************************************************/ void visitarArbolPostOrden(ARBOL raiz, void (* fVisita)(HOJA *pHoja)) {

ITSON

Manuel Domitsu Kono

464

Estructuras de Datos en C

/* Si el árbol está vacío */ if(raiz) { /* Visita el subárbol izquierdo */ visitarArbolPostOrden(raiz->pIzq, fVisita); /* Visita el subárbol derecho */ visitarArbolPostOrden(raiz->pDer, fVisita); /* Visita la raíz */ fVisita(raiz); } }

La función buscaArbol(), en cambio, requiere que el parámetro que representan la llave de búsqueda y los parámetros de la función empleada para comparar la llave con los nodos sean apuntadores void.

ARBOL.C. Continuación. /************************************************************* * HOJA **buscaArbol(ARBOL *pRaiz, void *pLlave, * int (* fcmp)(void *pInfo, void *pLlave)) * * Esta función busca en el árbol cuya dirección está * almacenada en la localidad de memoria dada por pRaiz el nodo * cuyo campo de información tenga una llave igual a la llave * apuntada por pLlave. La función regresa la dirección de la * localidad de memoria donde está almacenada la dirección del * nodo buscado, NULL en caso de no encontrar un nodo. fcmp() * es un apuntador a la función utilizada para comparar la * llave con los nodos de la lista. La función para comparar es * suministrada por el usuario y es de tipo entero y recibe * como parámetros apuntadores a los datos a comparar: pinfo y * pLlave que corresponden al campo de información y a la llave. * La función fcmp() debe regresar: * * 0 si campo de información == llave * (+) si campo de información > llave * (-) si campo de información < llave *************************************************************/ HOJA **buscaArbol(ARBOL *pRaiz, void *pLlave, int (* fcmp)(void *pInfo, void *pLlave)) { int rc; HOJA **ppPos = pRaiz; /* Si el árbol no está vacío */ if(*pRaiz) { /* Compara llave con nodo */ rc = fcmp((*pRaiz)->pInfo, pLlave); /* Si no es igual, busca en el subárbol izquierdo */ if(rc > 0) ppPos = buscaArbol(&((*pRaiz)->pIzq), pLlave, fcmp); /* si no está, busca el subárbol derecho */ else if(rc < 0) ppPos = buscaArbol(&((*pRaiz)->pDer), pLlave, fcmp); } return ppPos; }

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

465

La función creaHoja() también se modifica ya que ya que en lugar de recibir un entero, el dato a almacenar en el nodo, recibe por separado la dirección de la localidad donde está el dato y el tamaño del dato en los parámetros pDato y tamDato. En el código de la función podemos ver que se requiere pedir dos bloques de memoria en forma dinámica: Una para almacenar el nodo y otra para almacenar el dato. Por último la función destruyeNodo() requiere liberar los dos bloques de memoria pedidos por la función creaNodo().

ARBOL.C. Continuación. /************************************************************* * HOJA *creaHoja(void *pDato, int tamDato) * * Esta función crea un nodo de un árbol haciendo una petición * dinámica de memoria e inicializa su campo de información * con el valor de info y los apuntadores a los subárboles * izquierdo y derecho con apuntadores nulos. *************************************************************/ HOJA *creaHoja(void *pDato, int tamDato) { HOJA *pHoja; /* Pide un bloque de memoria para el nodo, en forma dinámica */ if(pHoja = malloc(sizeof(HOJA))) { /* Pide un bloque de memoria para el dato, en forma dinámica */ if((pHoja->pInfo = malloc(tamDato))) /* Almacena en el campo de información el dato */ memcpy(pHoja->pInfo, pDato, tamDato); else return NULL; /* Almacena en los apuntadores a los subárboles izquierdo y derecho apuntadores nulos. */ pHoja->pIzq = pHoja->pDer = NULL; } return pHoja; } /************************************************************* * void destruyeHoja(HOJA *pHoja) * * Esta función libera el bloque de memoria ocupada por el * nodo de un árbol. *************************************************************/ void destruyeHoja(HOJA *pHoja) { free(pHoja->pInfo); free(pHoja); }

El siguiente listado, DEMO_AB.C, es un módulo que implementa un programa de demostración que muestra el comportamiento de un árbol binario de búsqueda. Este módulo debe ligarse a los módulos: ARBOL y UTILS.

ITSON

Manuel Domitsu Kono

466

Estructuras de Datos en C

DEMO_AB.C. /************************************************************* * DEMO_AB.C * * Este programa ilustra el comportamiento de un árbol binario * de enteros. *************************************************************/ #include #include #include #include



#include "utils.h" #include "arbol.h" void despliegaPantallaInicial(void); void borraArbol(void); void despliegaHoja(HOJA *pHoja); int fcmp(void *pInfo, void * pLlave); int x, y; ARBOL raiz = NULL; int main(void) { HOJA *pHoja, **pPos; int dato; char operador, sdato[6]; clrscr(); raiz = inicializarArbol(raiz); despliegaPantallaInicial(); while(1) { gotoxy(28,9); printf(" "); gotoxy(51,9); printf(" "); gotoxy(28,9); if((operador = getch()) == ESC) break; if(!operador) { switch(operador = getch()) { case INS: printf("Insertar"); gotoxy(51,9); gets(sdato); dato = atoi(sdato); pHoja = creaHoja(&dato, sizeof(int)); raiz = insertarArbol(raiz, pHoja, fcmp); break; case DEL: printf("Extraer "); if(arbolVacio(raiz)) msj_error(0,25, "Arbol vacío!, [Enter]", "\r", ATTR(RED, LIGHTGRAY,0)); else { gotoxy(51,9); gets(sdato); dato = atoi(sdato); pPos = buscaArbol(&raiz, &dato, fcmp); if(!(*pPos)) msj_error(0,25, "Valor inexistente!, [Enter]", "\r", ATTR(RED, LIGHTGRAY,0));

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

467

else extraerArbol(pPos, &dato, sizeof(int)); } break; } } borraArbol(); x = 28; y = 12; visitarArbolEnOrden(raiz, despliegaHoja); putchar('\n'); } raiz = inicializarArbol(raiz); borraArbol(); x = 28; y = 12; visitarArbolEnOrden(raiz, despliegaHoja); putchar('\n'); return 0; } /************************************************************* * void despliegaPantallaInicial(void) * * Esta función despliega la explicación de lo que hace el * programa y las instrucciones de su uso. *************************************************************/ void despliegaPantallaInicial(void) { clrscr(); gotoxy(5,3); printf("Este programa demuestra el comportamiento de un"); printf(" arbol binario. En este"); gotoxy(5,4); printf("arbol se almacenan enteros. Hay dos operaciones:"); printf(" Para insertar un numero"); gotoxy(5,5); printf("en el arbol presiona la tecla [Insert], luego el "); printf(" numero a insertar."); gotoxy(5,6); printf("Para extraer un numero del arbol presiona la "); printf("tecla [Supr], luego el numero"); gotoxy(5,7); printf("a extraer. Para terminar presiona la tecla [Esc]."); gotoxy(15,9); printf("Operacion [ ] Valor [ ]"); } /************************************************************* * void despliegaHoja(HOJA *pHoja) * * Esta función despliega la dirección de un nodo del árbol, * así como el contenido de su campo de información y las * direcciones de sus subárboles izquierdo y derecho. *************************************************************/ void despliegaHoja(HOJA *pHoja) { /* Despliega la dirección del nodo y su contenido */ gotoxy(x+1,y-1); printf(" +--------------------+"); gotoxy(x+1,y); printf("%6p \x10%6p%6d%6p", pHoja, pHoja->pIzq, *(int *)(pHoja->pInfo), pHoja->pDer); gotoxy(x+1,y+1); printf(" +--------------------+"); y += 3; /* Si va a desplegar otra pantalla */ if((pHoja->pIzq || pHoja->pDer) && y == 24) { msj_error(0,25,"[Enter] para continuar", "\r", ATTR(RED, LIGHTGRAY,0));

ITSON

Manuel Domitsu Kono

468

Estructuras de Datos en C

x = 28; y = 12; borraArbol(); } } /************************************************************* * void borraArbol(void) * * Borra de la pantalla el árbol *************************************************************/ void borraArbol(void) { int i; for(i = 10; i < 25; i++) { gotoxy(1,i); clreol(); } } /************************************************************* * int fcmp(void *pInfo, void * pLlave) * * Esta función regresa: * * 0 si *pInfo == *pLlave * (+) si *pInfo > *pLlave * (-) si *pInfo < *pLlave *************************************************************/ int fcmp(void *pInfo, void * pLlave) { return *(int *)pInfo - *(int *)pLlave; }

Ejercicio Sobre Árboles Binarios Escribe una función que nos regrese el número de nodos en un árbol binario genérico. La sintaxis de la función es int longArból(Arbol raiz); donde raiz es el apuntador al árbol binario.

Aplicaciones de Árboles Binarios En las secciones anteriores se ha descrito el concepto de árbol binario, particularizando en el de árbol de búsqueda binaria. El uso de árboles binarios para ordenar datos nos permiten realizar búsquedas en forma muy eficiente. Hay que notar que la operación de buscar implementada por la función buscaArbol() no tiene que buscar en todos los nodos del árbol sino sólo en el subárbol apropiado: en el izquierdo si la llave del dato a buscar es menor que la raíz del árbol o en el derecho si es mayor. Si el árbol binario está balanceado, esto es, todas las hojas se encuentran en el mismo nivel, la eficiencia de está función es similar a la de una búsqueda binaria, estudiada en el capítulo anterior. Sin embargo esa

ITSON

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

469

eficiencia disminuye conforme el árbol se desbalancea y en el peor de los casos degenera en una lista ligada, con lo que la eficiencia se reduce a la de una búsqueda lineal. Aparte de la aplicación de árboles de búsqueda binarios, los árboles binarios tienen un gran número de aplicaciones. En [Tenenbaum, 1993] se desarrollan otras aplicaciones como evaluadores de expresiones, árboles de decisiones para teoría de juegos, codificación y compresión de datos.

Bibliografía 1. Dale, Nell y Lilly, Susan C., Pascal y Estructuras de Datos, Segunda edición, McGraw-Hill, Madrid, 1989. 2. Esakov, Jeffrey, Weiss, Tom, Data Structures: An Advanced Approach Using C, Prentice Hall, Inc., New Jersey, 1989. 3. Kernighan, Brian W. y Ritchie, Dennis M., El Lenguaje de Programación C, Segunda Edición, Prentice Hall, México, 1991. 4. Kruse, Robert L., Leung, Bruce P., Tondo, Clovis L., Data Structures and Program Design in C, Prentice Hall, Inc., New Jersey, 1991. 5. Schildt, Herbert, Turbo C. Programación Avanzada, Segunda edición, McGraw-Hill, Madrid, 1990. 6. Tenenbaum, Aaron M., Langsam Yedidyah, Augenstein Moshe A., Estructuras de Datos en C, Prentice Hall Hispanoamercana S. A., México, 1993. 7. Weiss, Mark A., Data Structures and Algorithm Analysis, The Benjamin/Cummings Publishing Company, Inc., Redwood City, California, 1992.

Problemas 1. Implementar una calculadora RPN, esto es, que evalúe expresiones en notación sufija, utilizando una pila generalizada para almacenar los datos. El tamaño de la pila es de cuatro. En las calculadoras HP, podemos visualizar el contenido de cada uno de los elementos del arreglo que constituyen la pila mediante cuatro "ventanas" llamadas registros. Esos registros tienen los nombres: X, Y, Z y T. Si sólo hay un dato almacenado en la pila, su valor se muestra en el registro X. Si hay dos datos almacenados en la pila, el dato que se almacenó primero se muestra en el registro Y y el último en el registro X. Si hay tres datos almacenados en la pila, el dato que se almacenó primero se muestra en el registro Z, el segundo en el registro Y y el último en el registro X, etc. La calculadora deberá mostrar tanto el contenido de los registros como el resultado, tal como se muestra en la figura 16-21. ITSON

Manuel Domitsu Kono

470

Estructuras de Datos en C

Figura 16-21 La ventana Resultado (R:) despliega el resultado de las operaciones que es el valor almacenado en el registro X. También es en esta ventana donde se leen los números. Las operaciones a implementar se muestran en la figura 16-21. 2. Se desea simular el comportamiento de un aeropuerto pequeño que sólo tiene una pista. En cada unidad de tiempo un avión puede aterrizar o despegar pero no puede haber un aterrizaje y un despegue en la misma unidad de tiempo. Los aviones arriban para aterrizar o despegar en tiempos aleatorios. De tal manera que en cualquier unidad de tiempo la pista puede estar ociosa, un avión puede estar despegando, un avión puede estar aterrizando y puede haber varios aviones en espera de aterrizar o despegar. Este programa utiliza dos colas una para los aviones a aterrizar y otra para los aviones a despegar. Es preferible mantener esperando a un avión en tierra que en el aire, así que sólo se permite que un avión despegue si no hay aviones esperando aterrizar. Cada avión se representa mediante una estructura con la siguiente información typedef struct { int identAvion; int tArribo; } AVION;

/* Número de identificación del avión */ /* Tiempo de arribo del avión a la cola */

El pseudocódigo del programa se muestra a continuación int main(void) { ìnt tActual; int tFinal;

ITSON

/* Unidad de tiempo actual de la simulación */ /* Número de unidades de tiempo que dura la simulación */

Manuel Domitsu Kono

Capítulo 16

Estructuras de Datos en C

471

int i, nAviones; AVION avion /* Estructura con los datos de un avión */ leeDatos(&tFinal, &vEspAvionesAterr, &vEspAvionesDesp); inicializarCola(&colaAterrizar); inicializarCola(&colaDespegar); for(tActual = 1; tActual
View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF