74
Apuntes para el curso de “Estructuras de datos en C/C++ Dr. Abdiel E. C´ aceres Gonz´ alez ITESM-CCM 2 de junio de 2005 Resumen Una estructura de datos es una manera de almacenar y organizar datos para facilitar el acceso y modificaciones. No hay una estructura de datos que sirva para todos los prop´ ositos, y por eso es importante saber sus ventajas y desventajas. Este documen- to es una colecci´ on de apuntes para el curso de Estructuras de Datos. Los apuntes se han tomado de algunas fuentes que son detalladas en la secci´ on de bibliograf´ ıa. ´ Indice 1. Preliminares de programaci´ on en C/C++ 3 1.1. Arreglos 3 1.2. Apuntadores 10 1.3. Estructuras C/C++ 15 1.4. Ejercicios de programaci´ on 19 2. La pila 21 2.1. Definici´ on y ejemplos 21 2.2. Operaciones b´ asicas 24 2.3. Ejemplo: N´ umero de par´ entesis 25 2.4. La estructura de datos Pila en C/C++ 26 2.5. La representaci´ on en C/C++ de las operaciones de una pila 27 2.6. Problemas de programaci´ on 29 1

e Structur A

Embed Size (px)

Citation preview

Page 1: e Structur A

Apuntes para el curso de

“Estructuras de datos en C/C++ ”

Dr. Abdiel E. Caceres Gonzalez

ITESM-CCM

2 de junio de 2005

Resumen

Una estructura de datos es una manera de almacenar y organizar datos para facilitarel acceso y modificaciones. No hay una estructura de datos que sirva para todos lospropositos, y por eso es importante saber sus ventajas y desventajas. Este documen-to es una coleccion de apuntes para el curso de Estructuras de Datos. Los apuntesse han tomado de algunas fuentes que son detalladas en la seccion de bibliografıa.

Indice

1. Preliminares de programacion en C/C++ 3

1.1. Arreglos 3

1.2. Apuntadores 10

1.3. Estructuras C/C++ 15

1.4. Ejercicios de programacion 19

2. La pila 21

2.1. Definicion y ejemplos 21

2.2. Operaciones basicas 24

2.3. Ejemplo: Numero de parentesis 25

2.4. La estructura de datos Pila en C/C++ 26

2.5. La representacion en C/C++ de las operaciones de una pila 27

2.6. Problemas de programacion 29

1

Page 2: e Structur A

3. Colas 31

3.1. Estructura de las colas en C/C++ 32

3.2. Colas con prioridad 33

3.3. Ejercicio de programacion 34

4. Recursion 36

4.1. Peligros en la recursividad 39

4.2. Ejercicios de programacion 40

5. Listas 42

5.1. Grafos 42

5.2. Listas simplemente encadenadas 44

5.3. El uso de memoria dinamica en C/C++ 51

5.4. Listas ligadas usando memoria dinamica 54

5.5. Ejercicios de programacion 56

6. Arboles 57

6.1. Concepto general de arbol 57

6.2. Arboles binarios 57

6.3. Representacion en C/C++ de los arboles binarios 64

6.4. Arboles 66

6.5. Ejercicios de programacion 69

7. Grafos 71

7.1. Recordatorio de las definiciones 71

7.2. Aplicacion ejemplo 73

2

Page 3: e Structur A

1. Preliminares de programacion en C/C++

En esta seccion recordaremos tres temas de programacion en C/C++ que sonfundamentales para estudiar estructuras de datos; estos temas son los arreg-los, los registros y los punteros. Los tres temas han sido tomados fundamen-talmente de [MP97]

1.1. Arreglos

Definicion 1 Un arreglo se compone de elementos de igual tamano almace-nados linealmente en posiciones de memoria consecutiva.

Se puede acceder a cada elemento de datos individual utilizando un subındice,o ındice, para seleccionar uno de los elementos. En C/C++ , un arreglo no esun tipo de datos estandar; es un tipo agregado compuesto de cualquier otrotipo de datos.

Los arreglos se pueden definir usando tipos de datos mixtos debido a que sesupone que todos los elementos son del mismo tamano. Puesto que todos loselementos son del mismo tamano y ya que este hecho se utiliza para ayudara determinar como localizar un elemento dado, resulta que los elementos sonalmacenados en localidades de memoria contiguas.

Lo mas importante a tener en cuenta es: El nombre de un arreglo es visto por elcompilador como un puntero-constante al primer elemento del arreglo. Esto esmuy importante: a) El nombre del arreglo es visto como un tipo puntero, y masespecıficamente, b) un puntero constante -significa una direccion de memoriabloqueada para el primer elemento de un arreglo-. Por ejemplo, aunque unadeclaracion de arreglo toma la forma generica:

Tipo_ElementoArray NombreArray [ NumeroDeElementos ]

El compilador ve la declaracion como

Tipo_ElementoArray * const NombreArray = &NombreArray[0];

Por esta razon, un identificador de arreglo no puede ser usado nunca como unvalor-i (valor izquierdo). Los valores izquierdos representan variables que sucontenido puede ser alterado por el programa; frecuentemente aparecen a laizquierda de las sentencias de asignacion.

Si los nombres de arreglo fueran variables izquierdos permitidos, el programapodrıa cambiar sus contenidos.

3

Page 4: e Structur A

float SalariosDeEmpleados[Max_empleados];

.

.

.

SalariosDeEmpleados = 45739.0;

El efecto harıa cambiar la direccion inicial del propio arreglo.

1.1.1. Declaraciones de un arreglo

La sintaxis de declaracion de arreglos es:

tipo nombre_arreglo [numero_de_elementos];

Los siguientes son dos ejemplos de declaraciones de arreglos validas en C/C++

:

int CoordenadasDePantalla[5]; /*Un arreglo de 5 enteros */

char IDCompania[20]; /*Un arreglo de 20 caracteres */

Figura 1. Arreglo CoordenadasDePantalla con ındices de desplazamiento valido

En la figura 1 se muestra el primer arreglo que fue declarado con el tipode numeros enteros, llamado CoordenadasDePantalla, ocupa en memoria 5localidades de memoria contiguas, cada una de ellas capaz de almacenar unnumero entero. Actualmente es comun que los numeros enteros sean de 32bits, esto hace que el arreglo CoordenadasDePantalla ocupe 32 × 5 = 160bits

No se permite utilizar nombres de variables dentro de los corchetes. Por esto noes posible evitar la especificacion del tamano del arreglo hasta la ejecucion delprograma. La expresion debe ser un valor constante, para que el compiladorsepa exactamente cuanto espacio de memoria tiene que reservar para el arreglo.

Una buena practica de programacion es usar constantes predefinidas.

4

Page 5: e Structur A

#define Coordenadas_Max 20

#define Tamano_MaX_Compania_Id 15

int CoordenadasDePantalla[Coordenadas_Max];

char IDCompania[Tamano_MaX_Compania_Id];

El uso de constantes predefinidas garantiza que futuras referencias al arreglono excedan el tamano del arreglo definido.

1.1.2. Iniciacion del arreglo

C/C++ proporciona 3 maneras de iniciar elementos del arreglo:

Por defecto: Cuando son creados, se aplica solamente a arreglos globales yestaticos.

Explıcita: Cuando son creados, suministrando datos de iniciacionTiempo de ejecucion: Durante la ejecucion del programa cuando se asig-

nan o copias datos en el arreglo.

1.1.3. Acceso a los elementos de un arreglo

Si se tiene un error cuando se utilizan arreglos en C/C++ , de seguro el errorinvolucra el acceso a los elementos del arreglo, por la simple razon de queel primer elemento esta en una posicion 0, no 1. De manera que el ultimoelemento del arreglo lo encontramos en n-1, donde n es el numero de elementos.Supongamos la siguiente declaracion:

int Estado[Rango_Maximo_Estado]={-1,0,1};

La siguiente sentencia tiene acceso a -1:

Estado[0];

Si escribimos Estado[3] causara un error porque no hay 4 elementos.

1.1.4. Calculo del tamano de un arreglo (sizeof())

Es frecuente utilizar el operador sizeof() para calcular la cantidad de espacioque se necesita almacenar para un objeto:

/*

* exploresz.cpp

*/

#include<iostream.h>

5

Page 6: e Structur A

#define maxDiasSemana 7

int main(void){

int desplazamiento, maxHorasDiarias[maxDiasSemana];

cout<<"sizeof(int) es"<<(int)sizeof(int)<<"\n\n";

for(desplazamiento=0;desplazamiento<maxDiasSemana;

desplazamiento++)

cout<<"&maxHorasDiarias["

<<desplazamiento

<<"]="

<<&maxHorasDiarias[desplazamiento]<<"\n";

return 0;

}

1.1.5. Arreglos multidimensionales

El termino dimension representa el numero de ındices utilizados para referirsea un elemento particular en el arreglo. Los arreglos de mas de una dimensionse llaman arreglos multidimensionales.

/*

/ dosDim.cpp

*/

#include <iostream>

#define numFilas 4

#define numColumnas 5

int main (int argc, char * const argv[]) {

int despFila, despColumna, desplazamiento, multiplo,

despCalculados[numFilas][numColumnas];

for(despFila=0;despFila<numFilas;despFila++)

for(despColumna=0;despColumna<numColumnas;despColumna++){

desplazamiento=numColumnas-despColumna;

multiplo=despFila;

despCalculados[despFila][despColumna]=

(despFila+1)*despColumna+desplazamiento * multiplo;

};

for(despFila=0;despFila<numFilas;despFila++){

std::cout<<"Fila actual: "<<despFila<<"\n";

std::cout<<"Distancia relativa desde la base: "<<"\n";

6

Page 7: e Structur A

for(despColumna=0;despColumna<numColumnas;despColumna++)

std::cout<<" "

<<despCalculados[despFila][despColumna]

<<" ";

std::cout<<"\n\n";

}

return 0;

}

}

El programa utiliza dos ciclos for para calcular e inicial cada uno de loselementos del arraglo a su respectiva distancia relativa desde la base. El arreglocreado tiene 4 filas y 5 columnas por fila, haciendo un total de 20 elementosenteros.

Los arreglos multidimensionales son almacenados de forma lineal en la memo-ria de la computadora. Los elementos en los arreglos multidimensionales estanagrupados desde el ındice mas a la derecha hacia el centro. En el ejemplo an-terior, fila 1, columna 1 serıa el elemento 3 del arreglo almacenado. Aunque elcalculo del desplazamiento aparece un poco difıcil, es referenciado facilmentecada elemento del arreglo.

La salida del programa anterior es:

Fila actual: 0

Distancia relativa desde la base:

0 1 2 3 4

Fila actual: 1

Distancia relativa desde la base:

5 6 7 8 9

Fila actual: 2

Distancia relativa desde la base:

10 11 12 13 14

Fila actual: 3

Distancia relativa desde la base:

15 16 17 18 19

dosdim has exited with status 0.

7

Page 8: e Structur A

1.1.6. Arreglos como argumentos de funciones

Es necesario recordar tres cosas al pasar arreglos como parametros de fun-ciones:

1. Todos los arreglos son pasados en llamada-por referencia.2. Debido a que el arreglo es pasado en llamada por referencia, serıa in-

correcto para la funcion llamada devolver el arreglo en una sentenciareturn();. Esta sentencia esta de mas.

3. Todos los elementos del arreglo son pasados a las funciones en llamada porvalor. lo que significa que se pasa una copia del elemento, no la direcciondel elemento.

/*

// ereArray.xcode

*/

#include <iostream>

#include <ctype.h>

#define maxArray 5

void ArrayMayuscula(char Array[maxArray]);

int main (int argc, char * const argv[]) {

int desplazamiento;

char Array[maxArray]=

{’a’,’e’,’i’,’o’,’u’};

for(desplazamiento=0;desplazamiento<maxArray;

desplazamiento++)

std::cout<<Array[desplazamiento];

std::cout<<"\n";

ArrayMayuscula(Array);

for(desplazamiento=0;desplazamiento<maxArray;

desplazamiento++)

std::cout<<Array[desplazamiento];

return 0;

}

void ArrayMayuscula(char Array[maxArray])

{

for(int desplazamiento=0;desplazamiento<maxArray;

desplazamiento++)

Array[desplazamiento]=toupper(Array[desplazamiento]);

//Aqui return array seria incorrecto

}

8

Page 9: e Structur A

La salida del programa demuestra que el arreglo se pasa en llamada por refer-encia, ya que el primer ciclo for da como salida los contenidos de minusculasoriginales: aeiou, mientras que el segundo ciclo for en main() da como salidalos contenidos del arreglo despues del llamado a la funcion ArrayMayuscula():

AEIOU.

Claramente, dentro del cuerpo de la funcion ArrayMayuscula(), ha cambiadoel arreglo de regreso en la funcion main(). el siguiente ejemplo es una simplemodificacion de este algoritmo, solo que en vez de pasar el arreglo completo,se pasa cada elemento individual:

/*

// ereArray2.xcode

*/

#include <iostream>

#include <ctype.h>

#define maxArray 5

void ElementosArrayMayuscula(char unChar);

int main (int argc, char * const argv[]) {

int desplazamiento;

char Array[maxArray]=

{’a’,’e’,’i’,’o’,’u’};

for(desplazamiento=0;desplazamiento<maxArray;desplazamiento++)

std::cout<<Array[desplazamiento];

std::cout<<"\n";

for(desplazamiento=0;desplazamiento<maxArray;desplazamiento++)

ElementosArrayMayuscula(Array[desplazamiento]);

for(desplazamiento=0;desplazamiento<maxArray;desplazamiento++)

std::cout<<Array[desplazamiento];

return 0;

}

void ElementosArrayMayuscula(char unChar)

{

unChar=toupper(unChar);

}

La salida del programa es:

9

Page 10: e Structur A

aeiou

aeiou

valarray has exited with status 0.

1.2. Apuntadores

Definicion 2 Un apuntador es una variable que contiene una direccion dememoria.

Supongamos una variable de tipo entero que se llama contenidoRAM y otravariable que se llama direccionRAM que puede contener una variable de tipoentero. En C/C++ una variable precedida del operador & devuelve la direccionde la variable en lugar de su contenido. Ası que para asignar la direccion deuna variable a otra variable del tipo que contiene direcciones se usan sentenciascomo esta:

direccionRam = &contenidoRAM

Figura 2. contenidoRAM se asigna a la localidad de memoria con direccion 7751

En la figura 2 se ilustra el nombre de la variable contenidoRAM y se observaque se encuentra en la direccion 7751 de la memoria. El contenido de estalocalidad no se muestra. Una variable que contiene una direccion, tal comodireccionRAM, se llama variable apuntador o simplemente apuntador.

Despues que la sentencia anterior se ejecuta, la direccion de contenidoRAM

sera asignada a la variable apuntador direccionRAM. La relacion se expresadiciendo que direccionRAM apunta a contenidoRAM. La figura 3 ilustra estarelacion.

El accceso al contenido de una celda cuya direccion esta almacenada en lavariable direccionRAM es tan sencillo como poner al inicio de la variableapuntador un asterisco: *direccionRAM. Lo que se ha hecho es eliminar lareferencia directa. Por ejemplo, si se ejecutan las siguientes dos sentencias, elvalor de la celda llamada contenidoRAM sera de 20 (vease la figura 4).

10

Page 11: e Structur A

Figura 3. Notacion de flecha para los apuntadores

direccionRAM = &contenidoRAM;

*direccionRAM = 20;

Figura 4. A contenidoRAM se le asigna el valor entero 20

1.2.1. Declaraciones de variables apuntador

C/C++ requiere una definicion para cada variable. Para definir una variableapuntador direccionRAM que pueda contener la direccion de una variableint, se escribe:

int *direccionRAM;

Realmente existen dos partes separadas en esta declaracion. El tipo de datode direccionRAM es:

int *

y el identificador para la variable es

direccionRAM

El asterisco que sigue a int significa “apuntador a”. Esto es, el siguiente tipode dato es una variable apuntador que puede contener una direccion a un int:int *

11

Page 12: e Structur A

En C/C++ una variable apuntador contiene la direccion de un tipo de datoparticular:

char *direccion_char;

char *direccion_int;

El tipo de dato de direccion char es diferente del tipo de dato de la variableapuntador direccion int. En un programa que define un apuntador a untipo de dato y utliza este para apuntar a otro tipo de dato, pueden ocurrirerrores en tiempo de ejecucion y advertencias en tiempo de compilacion. Unapractica de programacion pobre serıa definir un apuntador de una forma yluego utilizar este de alguna otra forma. Por ejemplo:

int *direccion_int;

float un_float = 98.34;

direccion_int = &un_float;

1.2.2. Utilizacion de punteros en sentencias sencillas

Veamos el siguiente ejemplo:

/*

// changeVals.xcode

*/

(01) #include <iostream>

(02)

(03) int main (int argc, char * const argv[]) {

(04) int A_int=15, B_int=37, Temp_int;

(05) int *direccion_int;

(06)

(07) std::cout<<"El contenido de A_int es:"<<A_int<<"\n";

(08) std::cout<<"El contenido de B_int es:"<<B_int<<"\n";

(09) direccion_int = &A_int;

(10) Temp_int = *direccion_int;

(11) *direccion_int = B_int;

(12) B_int = Temp_int;

(13) std::cout<<"Despues del intercambio:"<<"\n\n";

(14)

(15) std::cout<<"El contenido de A_int es:"<<A_int<<"\n";

(16) std::cout<<"El contenido de B_int es:"<<B_int<<"\n";

(17) return 0;

(18) }

12

Page 13: e Structur A

En la lınea (04) se han declarado tres variables de tipo entero, se da a cadacelda un nombre y se inicializan 2 de estas. Supondremos que la direccion dememoria asignada para la variable A int es la direccion 5328, y la direccionen memoria RAM asignada para la variable B int es la direccion 7916, y lacelda llamada Temp int se le ha asignado la direccion 2385. Vease la figura 5;

Figura 5. Descripcion de las tres variables en la memoria

En la lınea (05) se define un apuntador a un tipo de dato entero llamadodireccion int. La sentencia asigna la celda y da a esta un nombre.

Luego, en la lınea (09), la tercera sentencia asigna a direccion_int la direc-cion de A_int (figura 6).

Figura 6. direccion int dada la direccion de A int

La lınea (10) utiliza la expresion *direccion_int para acceder al contenidode la celda a la cual apunta direccion_int:

Temp_int = *direccion_int;

Por consiguiente, el valor entero 15 se almacena en la variable Temp_int. Sino se pone el * enfrente de direccion_int;, la sentencia de asignacion al-macenarıa ilegalmente el contenido de direccion_int en la celda nombradaTemp_int, pero se supone que Temp_int contiene un entero, no una direccion.

13

Page 14: e Structur A

Este puede ser un error muy difıcil de localizar puesto que muchos compi-ladores no emiten ninguna advertencia/error.

Para empeorar el asunto, la mayorıa de los apuntadores son cercanos, lo quesignifica que ocupan 2 bytes (4 bytes para aplicaciones de 32-bits), el mismotamano que un entero en una PC.

La sentencia (11) copia el contenido de la variable B int en la celda apuntadapor la direccion almacenada en direccion int(figura 7):

*direccion_int = B_int;

Figura 7. Se copia el contenido de B int usando la notacion de flecha de apuntadores

La ultima sentencia en la lınea (12) simplemente copia el contenido de unavariable entera, Temp int en otra variable entera B int (figura 8

Figura 8. Se copia Temp int en B int utilizando asignacion normal.

Debemos de asegurarnos de comprender la diferencia entre que se referenciacuando una variable puntero esta precedida por el operador de indireccion ycuando no esta precedida por este operador.

Para este ejemplo, la primera sintaxis es un apuntador a una celda que puedecontener un valor entero. La segunda sintaxis referencia la celda que contienela direccion de otra celda que puede contener un entero.

14

Page 15: e Structur A

1.2.3. Utilizacion incorrecta del operador de direccion

No se puede utilizar el operador de direccion sobre toda expresion C/C++ . Elsiguiente ejemplo demuestra aquellas situaciones donde no se puede aplicar eloperador de direccion &.

puedeAlmacenarDireccionDeConstante = &37;

int RAM_int = 5;

puedeAlmacenarDireccionDeExpresionTemp = &(RAM_int +15);

puedeAlmacenarDireccionDeRegistro = &varRegistro;

La primera sentencia trata de obtener ilegalmente la direccion de un valorconstante integrado. La sentencia no tiene sentido puesto que 37 no tiene unacelda de memoria asociada con este.

La segunda sentencia de asignacion intenta devolver la direccion de la expre-sion RAM_int+15. No existe direccion asociada con la expresion puesto que laexpresion en sı misma es realmente un proceso de manipulacion de pila.

Normalmente, el ultimo ejemplo respeta la demanda del programador paradefinir varRegistro como un registro mas que como una celda de almace-namiento en la memoria interna. Por consiguiente, no podrıa devolverse yalmacenarse la direccion de celda de memoria. El compilador C/C++ da lamemoria de variable, no el almacenamiento de registro.

1.3. Estructuras C/C++

Definicion 3 Una estructura es un grupo de variables las cuales pueden serde diferentes tipos sostenidas o mantenidas juntas en una sola unidad. Launidad es la estructura.

1.3.1. Sintaxis y reglas para estructuras en C/C++

En C/C++ se forma una estructura utilizando la palabra reservada struct,seguida por un campo etiqueta opcional, y luego una lista de miembros dentrode la estructura. La etiqueta opcional se utiliza para crear otras variables deltipo particular de la estructura:

struct campo_etiqueta{

tipo_miembro miembro_1;

tipo_miembro miembro_2;

15

Page 16: e Structur A

tipo_miembro miembro_3;

:

:

tipo_miembro miembro_n;

};

Un punto y coma finaliza la definicion de una estructura puesto que esta esrealmente una sentencia C/C++ . Algunos de los ejemplos usan la estructura:

struct stbarco{

char sztipo[iString15+iNull_char];

char szmodelo[iString15+iNull_char];

char sztitular[iString20+iNull_char];

int ianio;

long int lhoras_motor;

float fprecioventa;

};

En un programa, podemos asociar una variable con una estructura utilizandouna sentencia similar a la siguiente:

struct stbarco stbarco_usado;

La sentencia define stbarco_usado de tipo struct stbarco. La declaracionrequiere el uso del campo etiqueta de la estructura. Si esta sentencia esta con-tenida dentro de una funcion, entonces la estructura, llamada stbarco_usado,tiene un ambito local a esa funcion. Si la sentencia esta contenida fuera detodas las funciones de programa, la estructura tendra un ambito global. Esposible declarar una variable usando esta sintaxis:

struct stbarco{

char sztipo[iString15+iNull_char];

char szmodelo[iString15+iNull_char];

char sztitular[iString20+iNull_char];

int ianio;

long int lhoras_motor;

float fprecioventa;

} stbarco_usado;

Aquı la declaracion de variable va antes del punto y coma final. Cuando seasocia solo una variable con el tipo estructura, el campo etiqueta puede sereliminado, por lo que serıa posible escribir:

struct {

char sztipo[iString15+iNull_char];

char szmodelo[iString15+iNull_char];

16

Page 17: e Structur A

char sztitular[iString20+iNull_char];

int ianio;

long int lhoras_motor;

float fprecioventa;

} stbarco_usado;

1.3.2. Utilizacion de miembros de estructuras

Para accesar a los miembros de las estructuras se usa el punto u operadormiembro (.). La sintaxis es:

estructuraNombre.miembroNombre

Por ejemplo en:

gets(stbarco_usado.szmodelo);

Aquı, stbarco_usado es el nombre asociado con la estructura, y szmodelo esuna variable miembro de la estructura, otro ejemplo:

std::cin>> stbarco_usado.sztipo;

Esta sentencia leera la marca del stbarco_usado en el arreglo de caracteres,mientras la proxima sentencia imprimira el precio de venta de stbarco_usadoen la pantalla.

srd::cout<< stbarco_usado.fprecioventa;

Ejemplo de estructuras:

/* fractionStruct.cpp -

Programa para demostrar el uso de los

tipos Struct en C++, este tipo de datos

es util para los programadores para crear

sus propias estructuras de tipos.

*/

#include <iostream>

using namespace std;

// Definimos un nuevo tipo de estructura llamada Fraction

// como la definicion se puso antes del "main"

// los tipos Fraction se pueden usar como prototipos

17

Page 18: e Structur A

struct Fraction {

// declaramos sus dos miembros

int numerator;

int denominator;

}; // Note el punto y coma al final

// funciones prototipos

void getFraction(Fraction &f);

void printFraction(const Fraction &f);

int main (int argc, char * const argv[])

{

// declaramos variables de tipo Fraction

Fraction f1, f2;

// obtenemos dos fracciones y las desplegamos

getFraction(f1);

cout << "\nf1 = ";

printFraction(f1);

getFraction(f2);

cout << "\nf2 = ";

printFraction(f2);

cout << endl;

return 0;

}

// pedimos al usuario los valores del denominador y numerador

// los almacenamos en su adecuado lugar en la estrcututra; checamos si

// el valor del denominador es valido y lo ponemos en 1 si no lo es.

void getFraction(Fraction &f) {

cout << "\nEnter the numerator: ";

cin >> f.numerator;

cout << "Enter the denominator: ";

cin >> f.denominator;

if (f.denominator == 0) {

cout << "\nIllegal denominator! Denominator is being set to 1.\n";

f.denominator = 1;

}

}

// imprimimos la fraccion

void printFraction(const Fraction &f) {

cout << f.numerator << "/"

18

Page 19: e Structur A

<< f.denominator << "\n";

}

Nota sobre las funciones prototipos:

Las funciones prototipo tienen los siguientes usos importantes:

Establecen el tipo devuelto para las funciones que devuelven otros tiposdiferentes que int. Aunque las funciones que devuelven valores enteris nonecesitan prototipos, se recomienda tener prototipos.Sin prototipos completos, se hacen las conversiones estandares, pero no sechecan los tipos o los numeros de argumentos con el numero de parametros.Los prototipos se usan para inicializar apuntadores a funciones, antes deque las funciones sean definidas.La lista de parametros se usa para checar la correspondencia de los argu-mentos en la llamada a la funcion con los parametros en la definicion de lafuncion

const en parmetros de funciones

El especificador const puede ser utilizado en la definicion de parametros defunciones. Esto resulta de especial utilidad en tres casos. En los tres el finque se persigue es el mismo: indicar que la funcion no podra cambiar dichosargumentos :

Con parametros de funciones que sean de tipo matriz (que se pasan porreferencia). Ejemplo: int strlen(const char[]);

Cuando los parametros son punteros (a fin de que desde dentro de la funcionno puedan ser modificados los objetos referenciados). Ejemplo: int printf

(const char *format, ...);

Cuando el argumento de la funcion sea una referencia, previniendo ası que lafuncion pueda modificar el valor referenciado. Ejemplo: int dimen(const

X &x2);

1.4. Ejercicios de programacion

1. El siguiente algoritmo es el metodo de insercion para ordenar elementosen un arreglo:insertionSort(A)

for j:=2 to length[A]

do key:=A[j]

-> Inserta el elemento A[j]

-> en la secuencia ordenada A[1..j-1]

i:=j-1

19

Page 20: e Structur A

while i>0 and A[i]>key

do A[i+1]=A[i]

i:=i-1

A[i+1]:=key

a) desarrolle un programa en C/C++ del metodo de insercionb) ilustre como opera el algoritmo insertionSort(A) usando como en-

trada el arreglo A=<31,41,59,26,41,58>

2. Reescriba el programa y nombrelo insertionSortNondec para que or-dene los elementos en orden decreciente

3. Considere el siguiente problema de busqueda:Input: Una secuencia de n numeros A = 〈a1, a2, . . . , an〉 y un valor v.Output: Un ındice i tal que v = A[i] o el valor espacial NIL si v no

ocurre en A.Escriba un programa que resuelva este problema de busqueda.

4. Considere el problema de sumar dos numeros binarios de longitud n.Cada numero se almacena en uno de los arreglos A y B de tamano n. Lasuma se almacena en un arreglo C de tamano n + 1, tambien como unnumero binario. Escriba un programa que resuelva este problema.

20

Page 21: e Structur A

2. La pila

Uno de los conceptos mas utiles en las ciencias de la computacion es el de pila.En esta seccion vamos a definir este concepto de manera abstracta y veremoscomo se usa para convertirse en una herramienta concreta y de gran valor enlas soluciones de problemas. La informacion contenida en esta seccion se hatomado de [TA83].

2.1. Definicion y ejemplos

Definicion 4 Una pila (stack) es una coleccion ordenada de elementos en lacual se pueden insertar nuevos elementos por un extremo y se pueden retirarotros por el mismo extremo; ese estremos se llama “la parte superior” de lapila.

Si tenemos un par de elementos en la pila, uno de ellos debe estar en la partesuperior de la pila, que se considera “el mas alto” en la pila que el otro. Enla figura 9 el elemento F es el mas alto de todos los elementos que estan en lapila. El elemento D es el mas alto de los elementos A,B,C, pero es menor quelos elementos E y F.

Figura 9. Pila con 6 elementos

Para describir como funciona esta estructura, debemos agregar un nuevo ele-mento, el elemento G. Despues de haber agregado el elemento G a la pila, lanueva configuracion es la que se muestra en la figura 10.

De acuerdo con la definicion, existe solamente un lugar en donde cualquierelemento puede ser agregado a la pila. Despues de haber insertado el nuevoelemento, G ahora es el elemento en la cima. Debedos aclarar en que piladeseamos insertar elementos, puesto que es posible tener mas de una pila almismo tiempo.

21

Page 22: e Structur A

Figura 10. Operacion de insertar el elemento G en la pila P

Cuando se desea retirar un elemento de la pila, solo basta ordenar que searetirado un elemento; no podemos decir “retira C de la pila”, porque C noesta en la cima de la pila y solamente podemos retirar el elemento que esta enla cima. Para que la sentencia “retira C de la pila” tenga sentido, debemosreplantear las ordenes a algo como:

Retira de la pila hasta que el elemento retirado sea C.

Ni siquiera es necesario decir: “Retira un elemento de la pila...” porque essobreentendido que solamente se puede sacar un elemento a la vez.

Siguiendo nuestro ejemplo, ahora deseamos retirar de la pila P. La configu-racion global de la pila es como se muestra en la figura 11

Figura 11. Operacion de retirar de la pila P

El concepto de pila es muy importante en computacion y en especial en teorıade lenguajes de programacion. En lenguajes procedurales como Pascal o C, lapila es una estructura indispensable, debido a las llamadas a funcion.

Resulta que el flujo de instrucciones va de arriba hacia abajo, y cuando ocurreuna llamada a alguna funcion, el estado global del sistema se almacena en unregistro y este en una pila. Ası que la pila va a contenr todas las llamadas aprocedimientos que se hagan.

22

Page 23: e Structur A

Cuando se termina de ejecutar algun procedimiento, se recupera el registro queesta en la cima de la pila. En ese registro estan los valores de las variables comoestaban antes de la llamada a la funcion, o algunas pueden haber cambiado sivalor, dependiendo del ambito de las variables.

Cada elemento en la pila que es retirado, significa que se ha terminado deejecutar alguna funcion. Cuando se termina de ejecutar el programa, la pilade llamadas a subprogramas debe haber quedado en 0 tambien, de otro modopodrıa causar algun tipo de error.

Esto nos lleva a pensar en otras utilidades de la pila. La pila sirve para en-contrar errores.

La dinamica de la pila, es decir, la manera en como entran los datos a laestructura de datos y como salen, se denomina fifo, que viene del inges firstin first out (primero en entrar, primero en salir).

Figura 12. Dinamica de la pila P

En la figura 12 se muestran “fotografıas” en distintos momentos de la pila,cuando se desea insertar H justo debajo de F. Para hacer esto se requiere,retirar tantos elementos como sean necesarios, aquı se han retirado de la cimaG y F para luego insertar H, que quedara posteriormente debajo de F.

Lo que sucede es que, cuando se retira el elemento G se debe hacer una evalu-acion para determinar si el elemento retirado es el elemento objetivo, en estecaso el elemento objetivo es F, puesto que se desea insertar un elemento debajode F.

Despues de haber insertado F, insertamos de nuevo los elementos F y G en eseorden, ademas de insertar finalmente el elemento I que queda en la cima de lapila. Enseguida veremos con mas detalle las operaciones basicas de las pilas.

23

Page 24: e Structur A

2.2. Operaciones basicas

Las operaciones basicas de una pila son:

1. En la pila S, insertar un elemento e: push(S,e),2. Retirar un elemento de la pila S: pop(S),3. Verificar si la pila S esta vacıa: stackempty(S) y4. Saber cual es el elemento en la cima de la pila S: stacktop(S).

enseguida cada una de estas operaciones:

2.2.1. La operacion push

Esta operacion sirve para insertar un elemento e en la pila S, lo vamos aescribir como:

push(S,e)

Despues de hacer esta operacion sucede que:

El elemento en la cima de la pila S ahora es e

2.2.2. La operacion pop

Para retirar un elemento de la pila S y asignarlo a una variable del mismo tipoque el tipo de los elementos de la pila, usaremos la operacion pop escribiendolacomo:

v=pop(S);

En donde v es una variable que almacena el valor del elemento que estaba enla cima de S. Hacer esta operacion tiene algunas implicaciones:

La variable v debe ser del mismo tipo que los elementos almacenados en lapila.Solamente se puede retirar un elemento de la pila a la vez.Antes de la operacion, e era el elemento en la cima, ahora ya no lo es mas.El apuntador “cima” decrece en una unidad.

2.2.3. La operacion stackempty

Esta operacion toma como argumento una estructura del tipo stack (pila) ydevuelve un valor booleano, devuelve un true si la pila esta vacıa y devuelve

24

Page 25: e Structur A

un false si la pila tiene al menos un elemento, es decir:

stackempty(S) =

true si S tiene 0 elementos

false si S tiene mas de 0 elementos

2.2.4. La operacion stacktop

La operacion stacktop(S) devuelve el valor del elemento en la cima de la pilaS. Para hacer esta operacion escribiremos:

v=stacktop(S)

las implicaciones de usar esta operacion son:

Se hace una copia del elemento que esta en la cimaEn realidad se hacen dos operaciones, primero se hace v=pop(S), luego unpush(S,v), porque despues de la operacion stacktop, la pila S queda sincambio alguno.

2.3. Ejemplo: Numero de parentesis

Supongamos ahora la expresion ((5+6)*4)/(17+9), una de las condicionespara que sea una expresion aritmetica correcta en que tengas sus parentesisbalanceados, ası que deseamos saber si el numero de parentesis que abres esel mismo numero de parentesis que cierran.

Para resolver este problema usaremos el concepto de pila. La idea es simple.Vamos a leer cada elemento de la expresion, si se trata de un parentesis queabre, entonces lo insertaremos en una pila; si se trata de un parentesis quecierra, entonces sacamos un elemento de la pila. Al terminar de leer la expre-sion revisaremos si la pila esta vacıa, en cuyo caso habremos concluıdo que elnumero de parentesis que abre es el mismo que el numero de parentesis quecierra y la expresion tiene parentesis balanceados.

Veamos como funciona:

‘(’ : push(S,‘(’)

‘(’ : push(S,‘(’)

‘5’ : nada que hacer‘+’ : nada que hacer‘6’ : nada que hacer‘)’ : v=pop(S)

25

Page 26: e Structur A

‘*’ : nada que hacer‘4’ : nada que hacer‘)’ : v=pop(S)

‘/’ : nada que hacer‘(’ : push(S,‘(’)

‘17’: nada que hacer‘+’ : nada que hacer‘9’ : nada que hacer‘)’ : v=pop(S)

Empezamos con un contador iniciado en 0, y por cada push aumentamos uncontador, y por cada pop decrementamos el contador. Al final vemos el valordel contador, si el contador=0 entonces terminamos con exito, de otro modsenalamos el error.

En la figura 13 se muestra la actividad de la pila a medida que se van agregandoy quitando elementos.

Figura 13. Evaluacion del balance de parentesis en una expresion aritmetica

2.4. La estructura de datos Pila en C/C++

Una pila esta conformada por dos elementos:

Un espacio suficientemente grande para almacenar los elementos insertadosen la pilaUna parte que nos senale cual es el elemento en la cima de la pila.

Estas partes las conformamos en una estructura, descrita como sigue:

definir numero maximo de elementos en la pila

definir nuevo tipo estructura llamado "stack" con

item : un arreglo de 1 a maximos elementos enteros

top : un numero de 0 a maximos elementos

26

Page 27: e Structur A

fin de la nueva estructura

Facilmente podemos describir un codigo en C/C++ que represente lo anterior-mente propuesto.

// En la parte de definiciones

#define maxElem 100

// En la parte de tipos

struct stack {

int item[maxElem];

int top;

};

// En la parte de variables

struct stack A;

2.5. La representacion en C/C++ de las operaciones de una pila

En esta seccion veremos una implementacion de las cuatro operaciones basicasde las pilas. Todas estas operaciones se han hecho desde un punto de vista deprogramacion funcional, sin duda se pueden describir en un modelo orientadoa objetos.

2.5.1. La operacion push

El siguiente segmento de codigo ilustra como se puede implementar la op-eracion insertar un elemento en una pila. Hemos supuesto que la pila yaesta definida como una estructura stack.

(1) void push(struct stack *S,int e){

(2) S->top++;

(3) S->item[S->top]=e;

(4) }

En la lınea (1) se observa que la operacion push recibe dos parametros: ladireccion de una estructura de tipo pila y un elemento de tipo entero.

La lınea (2) incrementa el tope (cima) de la pila en una unidad, con el fin deagregar el elemento en una posicion libre de la pila, lo cual se logra en la lınea(3), asignando el valor e en la casilla S->top del arreglo item de la pila.

27

Page 28: e Structur A

2.5.2. La operacion pop

La operacion pop se escribe en forma de codigo en C/C++ con la siguientesecuencia de ordenes:

(1) int pop(struct stack *S){

(2) int valReturn;

(3)

(4) valReturn=S->item[S->top];

(5) S->top--;

(6) return valReturn;

(7) }

La lınea (1) describe que esta funcion devuelve un tipo entero, el tipo deelementos guardados en la pila; luego notamos que debemos dar solo la direc-cion de alguna variable de tipo estructura de pila (struct stack *). Obtener ladireccion se logra con el operador de indireccion (&).

Las lıneas (4) y (5) hacen todo el trabajo de esta funcion, se almacena el valorque sera devuelto en una variable de tipo entero y luego se decrementa el topede la pila.

2.5.3. La operacion stackempty

La operacion stackempty se describe en el siguiente segmento de codigo:

(1) bool stackempty(struct stack *S){

(2) bool valorDevuelto;

(3) if(S->top== -1)

(4) valorDevuelto=true;

(5) else

(6) valorDevuelto=false;

(7) return valorDevuelto;

(8) }

El encabezado de la funcion que se muestra en la lınea (1) establece quese devuelve un valor booleano, y que se debe dar un parametro, que es ladireccion de una localidad de memoria que almacena una estructura de tipopila. El objetivo de esta funcion es claro:

La lınea (3) establece la verdacidad o falsedad del predicado (S->top==-1),determinando si el nivel del tope es igual que -1, en cuyo caso devuelve unverdadero (4), de otro modo ha de devolver un valor falso (6). Se ha establecidoun -1 como vacıo porque el manejo de arreglos en C/C++ empieza en el ındice0, que a diferencia de otros lenguajes como Pascal, empiezan en 1.

28

Page 29: e Structur A

2.5.4. La operacion stacktop

Este es un caso especial porque no se requiere hacer ningun codigo.

Esta funcion debe devolver un numero entero y dejar la pila sin cambio. Paralograr esto se debe hacer un pop(&A), mostrar el elemento y luego insertar denuevo el elemento en la pila haciendo un push(&A,elemento), notemos que sehan usado los operadores de direccion para dar la direccion de la variable quealberga una estructura de tipo pila. El siguiente segmento de codigo ilustracomo se han usado las funciones antes creadas, por supuesto que se puedenseparar y crear una nueva funcion que haga lo mismo:

...

(1) case 4:{

(2) if(not stackempty(&A)){

(3) valor=pop(&A);

(4) std::cout<<"La cima de la pila es: "<<valor<<"\n";

(5) push(&A,valor);

(6) } else

(7) std::cout<<"La pila esta vacia";

(8) break;

(9) }

...

2.6. Problemas de programacion

Los siguientes ejercicios deben ser resueltos en un programa (en C/C++ ):

1. Expresiones entrefijas y prefijas. Las expresiones aritmeticas puedenrepresentarse de varias maneras, una de ellas, la mas usual es la notacionentrefija.

La notacion entrefija establece que en medio de dos operandos se escribeun operador, como por ejemplos:a) a ∗ b, donde los operandos son a y b, y el operador es el sımbolo ∗;b) 2 + 5 ∗ ((5 + 7)/4) Donde el parentesis mas interno establece la may-

or prioridad, de manera que primero se debe evaluar (5 + 7), luego(12/4), luego 2+(5∗3) y fimalmente (2+15), dando como resultado17.

c) −1 No hay nada que hacer, pues es un operador unario.

En las expresiones prefijas se establece que el orden de escritura debeser, primero el operador y luego la lista de operandos:a) ∗ab, donde los operandos son a y b, y el operador es el sımbolo ∗;b) +2 ∗ 5/ + 574 Lo primero que hay que hacer es tomar el primer

29

Page 30: e Structur A

operador y tomar los operandos necesarios siguientes (dos si se tratade un operador binario y uno si es un operador unario). En este casose trata de evaluar 2 + [∗5/ + 574]. Cada uno de los operandos debeser tratado de nuevo como una expresion en prefijo, de manera quese repite lo anterior, tomar el operador y la lista de sus operandos ytratar cada uno de sus operandos como expresiones en prefijo: 2 +[5 ∗ [/+574]], luego 2+ [5 ∗ [[+57]/4]] y finalmente 2+ [5 ∗ [[5+7]/4]]y evaluar. Los parentesis cuadrados son para ilustrar el ejemplo y noson necesarios para su evaluacion.

c) −1 No hay nada que hacer, pues es un operador unario.

Haga un programa en C/C++ que transforme expresiones de entrefijoa prefijo, y de prefijo a entrefijo. Los caracteres validos son: las letrasmayusculas y minusculas, los numeros enteros, los parentesis normales,los cuatro operadores (+,−, ∗, /, ) y el operador unario (−).

Figura 14. Ilustracion del estacionamiento mencionado en el problema 2

2. en cierto punto de la ciudad hay un estacionamiento como el que semuestra en la figura 14, en donde hay lugar para 9 vehıculos. haga unprograma que muestre el manejo de este estacionamiento, considerandolos siguientes requisitos:a) Los vehıculos proporcionan la siguiente informacion: Placas (6 digi-

tos), Estado (2-3 caracteres, p.e. SON, DF, CHI, YUC), Marca, Mod-elo, Ano-Modelo, Nombre del propietario.

b) Al llegar un veıculo se acepta solamente si hay lugar disponible.c) Validar todas las operaciones de la pila.d) En cualquier momento se puede sacar algun vehıculo del estacionamien-

to, regresando los vehıculos en el orden en que estaban.e) Toda la corrida del programa debe hacerse hacia/desde la terminal

estandar.3. Haga un programa que implemente 2 pilas en 1 arreglo A[1..n] de man-

era que ninguna pila se desborde a menos que el numero de elementos enambas pilas sea n

30

Page 31: e Structur A

3. Colas

Definicion 5 Las colas son una estructura de datos similar a las pilas. Recorde-mos que las pilas funcionan en un deposito en donde se insertan y se retiranelementos por el mismo extremo. En las colas sucede algo diferente, se inser-tan elementos por un extremo y se retiran elementos por el otro extremo. Dehecho a este tipo de dispositivos se les conoce como dispositivos “fifo” (firstin, first out) porque funcionan como una tuberıa, lo que entra primero por unextremo, sale primero por el otro extremo.

En una cola hay dos extremos, uno es llamado la parte delantera y el otroextremo se llama la parte trasera de la cola. En una cola, los elementos seretiran por la parte delantera y se agregan por la parte trasera.

Figura 15. Dinamica de una cola. a) estado actual con una cola con tres elementosa,b,c; b) estado de la cola cuando se agrega el elemento d; c) estado de la colacuando se elimina el elemento a del frente de la cola

En la figura 15 se muestra una actividad tıpica de la cola, en donde se muestraque se agregan datos por la parte trasera de la cola y se eliminana datos porel frente de la cola.

Si Q es una cola y x es un elemento, se pueden hacer tres operaciones basicascon las colas:

1. insert(Q,x), que inserta el elemento x en la parte trasera de la cola Q.

2. x=remove(Q), que almacena en x el valor del elemento retirado de la partefrontal de la cola Q.

3. empty(Q), que es un predicado de valor booleano, y es verdadero cuandola cola Q tiene 0 elementos, y es falso cuando la cola Q tiene al menos unelemento, en cuyo caso, ese unico elemento es la parte frontal y la partetrasera de la cola al mismo tiempo.

31

Page 32: e Structur A

Teoricamente no hay lımite para el tamano de la cola, asi que siempre sedeberıa poder insertar elementos a una cola, sin embargo, al igual que laspilas, normalmente se deja un espacio de memoria para trabajar con estaestructura. Por el contrario, la operacion remove solamente se puede hacer sila cola no esta vacıa.

3.1. Estructura de las colas en C/C++

De manera similar a las pilas, las colas definen una estructura no estandar, demanera que se debe crear un nuevo tipo de dado, el tipo cola, que debe tenerlos siguientes elementos:

Un arreglo de n elementos de algun tipo especıfico, puede incluso ser untipo estandar o no.Un numero que indica el elemento que esta en la posicion del frente de lacola.Un numero que indica el elemento que esta en la posicion trasera de la cola.

Suponiendo que los elementos son numeros enteros, una idea para representaruna cola en C/C++ es usar un arreglo para contener los elementos y emplearotras dos variables para representar la parte frontal y trasera de la cola.

#define maxQueue 100

struct cola{

int items[maxQueue];

int front;

int rear;

};

Esta representacion con arreglos es completamente valida, pero debemos tenercuidado con los lımites del arreglo. Suponiendo que no existiera la posibilidadde caer en un desbordamiento del arreglo, es decir, que se insertaran maselementos de lo que el arreglo puede almacenar, la operacion insert podrıaquedar como:

void insert(struct cola *C, int e){

C->items[++C->rear]=e;

}

y al operacion x=remove(Q)

int remove(struct cola *C){

return C->items[C->front++];

32

Page 33: e Structur A

}

y finalmente la operacion empty(Q):

bool empty(struct cola *C){

if(C->front>C->rear)

return true;

else

return false;

}

3.2. Colas con prioridad

Una cola con prioridad es una estructura de datos en la que se ordenan losdatos almacenados de acuerdo a un criterio de prioridad. Hay dos tipos decolas de prioridad:

Las colas de prioridad con ordenamiento descendente.Las colas de prioridad con ordenamiento ascendente.

En las colas de prioridad ascendente se pueden insertar elementos en formaarbitraria y solamente se puede remover el elemento con menor prioridad. SiCPA es una cola de prioridad ascendente, la operacion insert(CPA,x) insertael elemento x en la cola CPA; y la operacion x=minRemove(CPA) asigna a x elvalor del elemento menor (de su prioridad) y lo remueve de la cola.

En las colas de prioridad descendente es similar, pero solo permite la supresiondel elemento mas grande. Las operaciones aplicables a la cola de prioridaddescendente son insert(CPD,x) y x=maxRemove(CPD), cuando CPD es unacola de prioridad descendente y x es un elemento.

La operacion empty(C) se aplica a cualquier tipo de cola y determina si unacola de prioridad esta vacıa. Las operaciones de insertar y borrar se aplicansolamente si la pila no esta vacıa.

Los elementos de la cola de prioridad no necesitan ser numeros o caracterespara que puedan compararse directamente. Pueden ser estructuras complejasordenadas en uno o varios campos. Por ejemplo, las agendas telefonicas constande apellidos, nombres, direcciones y numeros de telefono y estan ordenadaspor apellido.

A diferencia de las pilas y las colas, en las colas de prioridad se pueden sacarlos elementos que no estan en el primer sitio del extremo donde salen loselementos. Esto es porque el elemento a retirar puede estar en cualquier parte

33

Page 34: e Structur A

del arreglo.

Cuando se requiere eliminar un dato de una cola de prioridad se necesitaverificar cada uno de los elementos almacenados para saber cual es el menor(o el mayor). Esto conlleva algunos problemas, el principal problema es que eltiempo necesario para eliminar un elemento puede crecer tanto como elementostenga la cola.

Para resolver este problema hay varias soluciones:

1. Se coloca una marca de “vacıo” en la casilla de un elemento suprimido.Este enfoque realmente no es muy bueno, porque de cualquier modo seaccesan los elementos para saber si es una localidad vacıa o no lo es. Porotro lado, cuando se remueven elementos, se van creando lugares vacıosy despues es necesario hacer una compactacion, reubicando los elementosen el frente de la cola.

2. Cada supresion puede compactar el arreglo, cambiando los elementosdepues del elemento eliminado en una posicion y despues decrementandorear en 1. La insercion no cambia. En promedio, se cambian la mitad delos elementos de una cola de prioridad para cada supresion, por lo queesta operacion no es eficiente.

3.3. Ejercicio de programacion

1. Modifique los procedimientos de insertar, retirar y verificar-cola-vacıapara que considere aprovechar los espacios dejados al retirar elementos.

2. Un deque es un conjunto ordenado de elementos del cual pueden elimi-narse elementos en cualquier extremo y en el cual pueden insertarse ele-mentos en cualquier extremo. Llamemos a los dos extremos de un dequeleft (izquierdo) y right (derecho). ?’como se representa un deque enun arreglo en C/C++ ? escriba un programa que maneje un deque, y queconsidere las cuatro rutinasremoveLeft

removeRight

insertLeft

insertRight

para remover e insertar elementos en los extemos izquierdo y derecho deun deque. Asegurese de que las rutinas funcionan adecuadamente paraque un deque vacıo y que detectan desbordamiento y subdesbordamiento.

3. Programe las colas de prioridad ascendente y descendente.

4. Existe un estacionamiento que tiene un solo carril que aloja hasta 10

34

Page 35: e Structur A

carros. Los autos llegan por el extremo sur del estacionamiento y salen porel extremo norte del mismo. Si llega un cliente para recoger un carro queno esta en el extremo norte, se sacan todos los automoviles de ese lado,se retira el auto y los otros coches se restablecen en el mismo orden queestaban. Cada vez que sale un auto, todos los autos del lado sur se muevenhacia adelante para que en todas las ocasiones todos los espacios vacıosesten en la parte sur del estacionamiento. Escriba un programa que lea ungrupo de lineas de ingreso. Cada lınea contiene una “A” para las llegadasy una “D” para las salidas y un numero de placa. Se supone que loscarros llegan y salen en el orden especificado en la entrada. El programadebe imprimir (en la terminal estandar) un mensaje cada vez que entrao sale un auto. Cuando llega un carro, el mensaje debe especificar si hayespacio o no para el en el estacionamiento. Si no hay espacio, el carroespera hasta que hay espacio o hasta que se lee una lınea de salida parael auto. Cuando queda disponible espacio, debe imprimirse otro mensaje.Cuando salga un coche, el mensaje debe incluir la cantidad de veces quese movio el auto dentro del estacionamiento, incluyendo la salida misma,pero no la llegada. Este numero es 0 si el carro sale de la fila de espera.

35

Page 36: e Structur A

4. Recursion

Un tema fundamental para los proximos temas es el de recusrion. La recursiones muy importante tanto en mateaticas como em computacion, pues se usarecursion para definir procedimientos autosimilares.

Definicion 6 Decimos que un objeto es recursivo si en su definicion se nom-bra a sı mismo.

En programacion, una funcion es recursiva si en el ambito de esa funcion hayuna llamada a sı misma, C/C++ permite esta clase de acciones. Los algoritmosrecursivos dan elegancia a las soluciones de los problemas. Un ejemplo clasicoes el factorial de un numero.

Una manera de definir el factorial de un numero n > 1 es:

!n =n∏

i=1

i,

es decir, el producto de todos los numeros enteros menores o guales que el, loque se puede resolver facilmente con una funcion iterativa, esto es, una funcioncon un ciclo que itere suficientes veces, incrementando un valor y entonces iralmacenando en una variable el resultado de esas multiplicaciones.

Una implementacion de esta definicion iterativa es:

(1) int i,n;

(2) long double valorAc;

(4) valorAc=1.0;

(5) std::cout << "Numero entero:";

(6) std::cin>> n;

(7) for(i=1; i<=n; i++) valorAc = valorAc*i;

(8) std::cout<<"El factorial de "<<n<<" es:"<<valorAc;

El ciclo principal es en la lınea (7). No hay ningun truco hasta aquı. Launica observacion importante es en la lınea (2) en donde se declara el tipolong double para el valor del resultado, la razon para tal accion es que elnumero factorial crece muy rapido y aun con entradas en el rango de loscaracteres (hasta 255), el factorial es muy grande. Este procedimiento com-putacional no hace uso de tecnicas especiales empleadas para tratar numerosgrandes.

Sin embargo una solucion mas elegante es usar la definicion recursiva, y estaes:

36

Page 37: e Structur A

!n = n ∗ !(n− 1)

El programa en C/C++ es el que se muestra a continuacion:

( 1) double factorial(double a){

( 2) if (a<=1) return 1.0;

( 3) else return (a *factorial(a-1.0)); }

( 4)

( 5) int main (int argc, char * const argv[]) {

( 6) double n;

( 7) std::cout << "Numero entero:";

( 8) std::cin>> n;

( 9) std::cout<<"El factorial de "<<n<<" es: "<< factorial(n);

(10) return 0; }

Aquı hay varias cosas que senalar, en primer lugar se ha creado una nuevafuncion, a diferencia de la definicion iterativa en donde era suficiente traba-jar en el programa principal. Esta funcion se llama factorial (como era desuponerse), y empieza su encabezado en la lınea (1).

Allı mismo en la misma lınea (1), es de notar que hemos emplado ahora eltipo double tanto para el tipo devuelto como para el tipo del argumento, adiferencia de la version iterativa en donde empleabamos tipos diferentes. Larazon es que al iniciar la recursion el argumento es del tipo devuelto, asi quedeben ser del mismo tipo.

Cada llamada recursiva genera una entrada a una pila, en donde se guardan(como elementos) los estados generales del sistema al momento de hacer lallamada, entonces, cuando se termina la funcion se recupera una entrada de lapila. En la figura 16 ilustra como funciona la recursividad cuando se intentaobtener el factorial(5).

Figura 16. Recursividad cuando se ejecuta factorial(5)

37

Page 38: e Structur A

4.0.1. La serie Fibonacci

Una de las series mas famosas es sin duda alguna la serie de Fibonacci:

1, 1, 2, 3, 5, 8, 13, 21, 34, . . .

Un poco de observacion es sufucuente para encontrar que cualquier numero(a partir del tercero de la serie, osea el segundo 1) es igual a la suma de losdos numeros anteriores.

Daremos en primer lugar la version iterativa. En este algoritmo deseamosencontrar el n-esimo numero de la serie Fibonacci. Ası si n = 4 el resultadodel algoritmo debe ser 3; si n = 6 el resultado debe ser 8. La version iterativaempieza desde los primeros 1’s, sumandolos y encontrando el tercero, luegopara encontrar el cuarto numero se suman el tercero (recien encontrado) y elsegundo, y ası en adelante hasta encontrar el numero buscado.

#include <iostream>

int main (int argc, char * const argv[]) {

int i,n,fib,fib1,fib2,fibx;

std::cout<<"Un numero entero:";

std::cin>>n;

fib1=2; fib2=1; i=3;

if((n==1)||(n==2))

fib=1;

else{

do{

fib = fib1 + fib2;

fibx = fib1; i++;

fib1 = fib; fib2 = fibx;

}while(i<n);

}

std::cout << "\nEl "<<n<<"-esimo numero de

la serie Fibonacci es: "<<fib;

return 0;

}

La definicion recursiva para encontrar todos los n primeros numeros de la serieFibonacci es:

38

Page 39: e Structur A

fib(n) =

1 Si n = 1 o n = 2

fib(n− 1) + fib(n− 2) Si n > 2

En el siguiente codigo, la solucion que propone la recursividad resulta en unaprogramacion elegante, aunque costosa. El codigo que hace esto es:

( 1) #include <iostream>

( 2) //====================

( 3) int fib(int val){

( 4) if ((val==1)||(val==2))

( 5) return 1;

( 6) else

( 7) return (fib(val-1)+fib(val-2));

( 8) }

( 9) //====================

(10) int main (int argc, char * const argv[]) {

(11) int n;

(12) std::cout<<"Numero entero:"; std::cin>>n;

(13) std::cout<<"\nEl "<< n

(14) <<"-esimo numero fibonacci es: "<< fib(n);

(15) return 0;

(16) }

Como regla general, cualquier algoritmo recursivo se puede reescribir en unalgoritmo iterativo. La ventaja de tener un algoritmo iterativo es que no seusa una pila para guardar llamadas a la misma funcion de manera recursiva,esto es una ventaja porque el espacio de memoria destinado al uso de la pila esgeneralmente limitado, de manera que cuando se hacen demasiadas funcionespush seguramente llegara el momento en que la pila “se desborde”, que porcierto es un termino usado en computacion para decir que ya no hay masespacio disponible en la pila.

4.1. Peligros en la recursividad

El principal peligro al usar recursividad, es no tener una manera de salir delpaso recursivo, esto es peligroso porque se hacen llamadas a la misma funcion,lo que significa una entrada en la pila donde se almacenan los estados generalesdel programa.

Para decidir hacer un programa recursivo se deben de tener al menos dos cosasmuy claras:

39

Page 40: e Structur A

1. El paso base: Esta es la clave para terminar la recursion, es cuando dejade hacer llamadas a la funcion recursiva y hace evaluaciones devolviendolos resultados. En el ejemplo de la serie de Fibonacci, el paso base esta enla lınea ( 5). Ademas se debe asegurar de que es posible entrar a este paso.

2. El paso recursivo: Es la parte de la definicion que hace llamadas aesa misma funcion y que es la causante de las inserciones en la pila,almacenando en cada una de las llamadas, informacion del programa, delestado de sus variables locales y globales. En el mismo ejemplo de la serieFibonacci, el paso recursivo se muestra en la lınea ( 7).

Otras cosas que se deben tener claras son por ejemplo si se pasa una variablecomo referencia o por valor, si las variables apuntadores son del tipo adecuadoetc.

Frecuentemente tanto el paso base como el paso recursivo, se encuentran enuna sentencia condicional if, pero porsupuesto que es posible usar cualquierotra sentencia de control, dependiendo de las necesidades particulares del prob-lema.

El siguiente ejemplo ilustra este problema

( 1) #include <iostream>

( 2) int malaFuncion( int n ){

( 3) std::cout << "malaFuncion es una recursion infinita. n="<<n;

( 4) if( n == 0 )

( 5) return 0;

( 6) else

( 7) return malaFuncion( n / 3 + 1 ) + n - 1;

( 8) }

( 9) int main (int argc, char * const argv[]) {

(10) std::cout << malaFuncion(10);

(11) return 0;

(12) }

4.2. Ejercicios de programacion

Los siguientes ejercicios deben de ser programados en C/C++ :

1. Busqueda binaria: Considere un arreglo de elementos (numeros enterosesta bien) en el cual los objetos ya estan ordenados, y se desea encon-trar un elemento dentro de este arreglo. Es decir, se desea realizar una“busqueda”.

La idea general de este metodo de busqueda binaria es:Si el arreglo tiene 1 elemento, se compara con el numero requerido y la

40

Page 41: e Structur A

busqueda termina.Si el arreglo tiene mas de 1 elemento, tendremos que dividir en dos elarreglo y decidir en que parte del arreglo buscar; luego buscarlo usandobusqueda binaria

2. Escriba un programa para calcular la cantidad de maneras diferentes enlas cuales un entero n se puede expresar como la suma de dos enterosmenores p < n y q < n tales que p + q = n

41

Page 42: e Structur A

5. Listas

Hay dos desventajas serias con respecto a las estructuras estaticas de pilas ycolas usando arreglos. Estas desventajas son que tienen un espacio limitadode memoria y la otra desventaja es que es posible no ocupar toda la memoriadisponible, haciendo que se desperdicie espacio.

Una solucion es usar listas. Las listas son estructuras de datos que son dinami-cas, esto significa que adquieren espacio y liberan espacio a medida que senecesita. sin embargo, hay una advertencia. Como regla general siempre hayque tener cuidado al manejar direcciones de espacios de memoria, porque esposible que accedamos a una localidad de memoria de la cual no deseabamoscambiar su contenido.

Antes de estudiar las listas, daremos una breve introduccion a los grafos, pueslas listas son un caso especial de los grafos.

5.1. Grafos

Los grafos son una manera visual de representar las relaciones.

Definicion 7 Si A y B son dos conjuntos, decimos que a ∈ A esta relacionadocon b ∈ B si es verdadera una sentencia R que considere a ambos elementos.Esta sentencia R puede ser cualquier predicado, por ejemplo: “es padre de”,“debe dinero a”, “toma el curso de” etc.; si el predicado es verdadero paraese par de elementos, lo escribimos como aRb, y si el predicado es falso, loescribimos como b 6 .

Ası los ejemplos citados, si a ∈ A, b ∈ B se puede leer:

Si A es el conjunto de alumnos, B es el conjunto de materias y R es “toma elcurso”, entonces pedroRlogica se lee “pedro toma el curso de logica. Enla figura 17 se puede apreciar esto en forma de diagramas de Venn.Si A es el conjunto de personas y B es tambien el conjunto de personas,y R es “debe dinero a”; marisolRrafaelle significa que “marisol debedinero a rafaelle” y de ningun modo es al contrario, es decir “rafaelle nodebe dinero a marisol”.

Los elementos de la figura 17 definen un nuevo conjunto de elementos, elconjunto de pares de elementos que estan relacionados. Ası la relacion “tomael curso de” es el siguiente:

42

Page 43: e Structur A

Figura 17. Relacion “toma el curso de” para los conjuntos A de personas y B dematerias.

R = {(diana, programacion), (carolina, programacion),

(carolina, compiladores), (carolina, lenguajes),

(rafael, compiladores), (gustavo, lenguajes),

(fabiola, lenguajes)}

Graficamente podemos ilustrar el conjunto R de “toma el curso de” con ungrafo como el que se muestra en la figura 18.

Figura 18. Grafo que ilustra la relacion “toma el curso de”.

De manera que podemos definir un grafo como una representacion grafica deuna relacion.

Definicion 8 Para definir formalmente un grafo debemos establecer la sigu-iente tupla:

G = 〈A, N〉

Donde A es un conjunto de aristas y N 6= ∅ un conjunto no vacıo de nodos.En el caso de R, el conjunto A ∪ B es el conjunto de nodos y el conjunto deflechas es el conjunto de aristas.

43

Page 44: e Structur A

Notemos que el conjunto A de aristas puede ser un conjunto vacıo, pero deningun modo hay grafo sin nodos, es decir el conjunto N debe ser diferenteque el conjunto vacıo.

Supongamos ahora A = {1, 2, 3, 4, 5, 6} y la siguiente relacion en A:

R = {(1, 2), (2, 3), (3, 4), (4, 5), (5, 6)}

Esta relacion luce como aparece en la figura 20.

Figura 19. Relacion R de A en A

y en forma de grafo es:

Figura 20. Grafo de la relacion R : A→ A

A esta clase de grafos, en las que cada nodo tiene a lo mas una arista dirigidaque sale y a lo mas una arista dirigida que entra, se le llama lista.

5.2. Listas simplemente encadenadas

Como vimos en la seccion anterior, una lista es una relacion de elementos, talesque cada elemento esta relacionado con unicamente un elemento del conjunto,diferente a sı mismo.

44

Page 45: e Structur A

Como cada elemento puede tener a lo mas una arista dirigida que sale y unaarista dirigida que entra, bien puede tener 0 aristas que salen, o cero aristasque entran. Si el nodo tiene 0 aristas que salen, entonces es el final de la lista.Si el nodo tiene 0 aristas que entran, entonces es el inicio de la lista.

Por razones practicas, se dibujan una flecha que sale de un identificador de lalista y entra al inicio de la lista y otra flecha que sale del final de la lista yapunta a un sımbolo que se llama NULO.

Figura 21. Grafo de la relacion R : A → A con apuntadores del nombre de la listalistaLigada y hacia NULL

En C/C++ el identificador de la lista contiene la direccion del primer elementode la lista, ası como sucede con los arreglos. El valor NULO es util para sabercuando termina la lista, es una constante estandar y no tiene valor.

El contenido de los nodos, como ya hemos visto, son los elementos de unconjunto. Si ese conjunto tiene elementos estructurados, tambien es validousarlos.

Normalmente cada nodo de la lista esta estructurado con dos partes:

1. La parte de informacion.2. La parte de direccion al siguiente nodo de la lista.

El campo de informacion contiene el elemento real de la lista. El campo dedireccion al siguiente contiene un apuntador al elemento con el cual esta rela-cionado, es decir, al elemento siguiente de la lista. La lista completa se accesamediante el identificador de lalista. El campo de la direccion del ultimo nodoapunta a una direccion nula.

La lista que no tiene elementos (solamente tiene un identificador que apunta anulo) se llama lista nula o lista vacıa. Una lista se inicializa a una lista vacıahaciendo lista=null, recordemos que lista es un apuntador a una direccionde memoria que puede albergar una variable del tipo que se hayan definidolos nodos; null es una direccion de cualquier tipo, ası que el compilador asignala direccion null a lista.

45

Page 46: e Structur A

Enseguida vamos a dar una lista de terminos usados para manejar los elemen-tos de una lista simplemente encadenada, aunque no son los que usa C/C++ ,pero sı son bastante claros para hacer algoritmos. Si p es un apuntador a ladireccion de una variable del tipo declarado para los nodos de una lista:

node(p): hace referencia al noso al que se apunta mediante p.info(p): hace referencia a la informacion del nodo al que apunta p.next(p): hace referencia a la parte direccion siguiente y, por tanto, es un

apuntador.

Ası que la expresion info(next(p)) significa que se hace referencia a la seccionde informacion del nodo siguiente al que apunta p.

5.2.1. Insertar y eliminar nodos de una lista

En el uso de las listas ligadas se ven involucradas varias operaciones, entreellas la de insertar un nuevo nodo a la lista y la operacion de eliminar unnodo de la lista. En ambos casos debemos recordar que se trata de manejo dela memoria, ası que insertar un nodo en la lista significa obtener un espaciode memoria disponible y relacionarlo con los elementos de la lista; ası mismo,eliminar un nodo de la lista significa liberar la memoria que ocupa ese nodosin perder la relacion con el resto de los nodos de la lista.

Insertar un elemento al inicio de la lista. La operacion p=getnode();

obtiene un nodo vacıo y establece el contenido de una variable nombrada p enla direccion de este nodo, como se muestra en la figura 22.a. Este nodo aunno pertenece a alguna lista, simplemente se ha logrado dedicar un especio dememoria que es apuntado por p, figura 22.b.

Figura 22. a) Creacion de un nuevo nodo. b) El nuevo nodo debe de ir insertado alfrente, atras o en medio de la lista.

Una vez que se ha creado un nuevo espacio para el nuevo nodo, se debe deestablecer la parte de informacion de ese nodo con la operacion info(p), comose ilustra en el siguiente ejemplo con el dato 6.

46

Page 47: e Structur A

info(p)=6;

Despues de esstablecer la parte de informacion es necesario establecer la partesiguiente de este nodo. Debido a que node(p) va a insertarse en la partedelantera de la lista, el nodo que sigue debe ser el primer nodo actual de lalista. Debido a que la variable lista (el identificador de la lista) contienela direccion de ese primer nodo, node(p) se agrega a la lista ejecutando laoperacion

next(p)=lista;

Esta operacion coloca el valor de lista (la direccion del primer nodo en lalista) en el campo siguiente de node(p). Estos pasos se ilustran en la figura23

Figura 23. Operaciones involucradas en la insercion de un nuevo nodo al inicio deuna lista: c) info(p). d) next(p)=list. e) list=p

Hasta ahora, p apunta a la lista con el elemento adicional incluido. Sin em-bargo, debido a que list es el apuntador externo a la lista deseada, su valordebe modificarse en la direccion del nuevo primer nodo de la lista. Esto sehace ejecutando la operacion

list=p;

En resumen, ya tenemos un algoritmo para insertar un elemento al inicio deuna lista simplemente ligada, al reunir todos los pasos tenemos:

p=getnode();

info(p)=6;

47

Page 48: e Structur A

next(p)=list;

list=p;

Eliminar un elemento de la lista. Para eliminar un elemento del iniciode la lista, se siguen los mismos pasos que se usan para insertar un elemento,pero en un orden diferente:

p=list;

x=info(p);

list=next(p);

Comentaremos cada una de estas tres lıneas, que se pueden apreciar en lafigura 24

Figura 24. Operaciones involucradas en la eliminacion de un nodo al inicio de unalista: c) p=list). d) x=info(p). e) list=next(p)

5.2.2. Listas en C/C++ con arreglos

Vamos a empezar una primera implementacion de listas usando arreglos, cadaelemento del arreglo debe ser un elemento compuesto. Cada elemento debecontener una parte para la informacion y otra parte para apuntar al elementosiguiente:

#include <iostream>

( 1) #define numNodes 500

( 2) struct nodeType{

( 3) int info;

48

Page 49: e Structur A

( 4) int next;

( 5) };

( 6) struct nodeType node[numNodes];

int main (int argc, char * const argv[]) {

std::cout << "Hello, World!\n";

return 0;

}

En el programa anterior, en las lıneas (2) a (5) se crea un nuevo tipo de dato,el tipo nodo. Cada nodo tiene dos partes, su parte de informacion y su partede apuntador al siguiente. Como solamente tenemos 500 nodos (declarados enla lınea (1), el tipo de siguiente es entero y hemos decidido almacenar numerosenteros solamente.

En la lınea (6) se ha declarado una variable global de tipo arreglo de estructurade nodos, es decir, se ha creado un arreglo de 500 nodos.

En este esquema, el ultimo nodo apunta a NULL, que se representa conel valor entero -1. Tenemos tambien los siguientes elementos de cada nodo:node[p] corresponde a next(p), por la notacion propia del lenguaje; tambiennode[p].info para info(p) y finalmente node[p].next hace referencia alnodo siguiente next(p).

Al principio todos los nodos estan sin usar, porque solamente se ha creadoel arreglo. Ası que todos los nodos van a formar parte de una lista de no-dos disponibles. Si se usa la variable global avail para apuntar a la listadisponible, podrıamos organizar inicialmente esta lista como:

void inicializaAvail(void){

int i;

avail = 0;

for(i=0; i<numNodes-1; i++){

node[i].next = i+1;

}

node[numNodes-1].next = -1;

}

Cuando se requiere un nodo para usarlo en la lista, se obtiene de la listadisponible. Cuando ya no es necesario ese nodo, se devuelve a la lista disponible.Estas dos operaciones se implementan mediante las rutinas en C/C++ getnode

y freenode:

49

Page 50: e Structur A

int getNode(void){

int p;

if (avail==-1){

std::cout<<"Overflow\n";

exit(1);

}

p=avail;

avail=node[avail].next;

return p;

}

Si avail es igual a -1 significa que no hay nodos disponibles, es decir, que elarreglo esta completamente lleno. Esto significa que las estructuras de listade un programa particular han desbordado el espacio disponible. La funcionfreeNode acepta un apuntador (numero entero) a un nodo y devuelve esenodo a la lista de disponibles:

void freeNode(int p){

node[p].next=avail;

avail=p;

}

Las operaciones primitivas para listas son versiones directas en C de los al-goritmos correspondientes. La rutina insAfter acepta un apuntador p a unnodo y un elemento x como parametros. Primero se asegura que p no sea nuloy despues se inserta x en el nodo siguiente al indicado por p.

void insAfter(int p, int x){

int q;

if(p==-1){

std::cout<<"void insertion\n";

}

else{

q=getNode();

node[q].info=x;

node[q].next=node[p].next;

node[p].next=q;

}

}

La rutina delAfter(p,px), llamada por el enunciado delAfter(p,&x), suprimeel nodo despues de node(p) y almacena su contenido en x;

void delAfter(int p, int *px){

int q;

if((p==-1)||(node[p].next==-1)){

50

Page 51: e Structur A

std::cout<<"void detection\n";

}

else{

q=node[p].next;

*px = node[q].info;

node[p].next=node[q].next;

freeNode(q);

}

}

Antes de llamar insAfter debemos asegurarnos de que ni p ni node[p].nextsean nulos.

5.3. El uso de memoria dinamica en C/C++

Como sabemos, en lenguaje C/C++ , &x es la direccion donde se almacena enmemoria la variable x. Si p es un apuntador en C/C++ , *p es el contenido dela localidad de memoria p. Si usamos C/C++ para implementar listas ligadas,podemos usar estos apuntadores. Sin embargo, primero analizaremos comoasignar y liberar el almacenamiento en forma dinamica y como se accesa alalmacenamiento dinamico en C/C++ .

En C/C++ , una variable que debe contener la direccion en la memoria quealmacena un numero entero se crea mediante la declaracion

int *p;

Recordemos que esta declaracion se divide en dos partes: la parte de tipoint *, que indica que se trata de un apuntador a un entero; y la parte deidentificador, en este caso p.

Una vez declarada la variable p como un apuntador a un tipo especıfico dedato, debe ser posible crear dinamicamente un objeto de este tipo especıficoy asignar su direccion a p.

Esto se hace en C/C++ mediante la funcion de la biblioteca estandar malloc(size).La fucnion malloc asigna de manera dinamica una parte de memoria detamano especificado en size y devuelve un apuntador a un elemento de tipochar. Consideremos las siguientes declaraciones

extern char *malloc();

int *pi;

float *pr;

51

Page 52: e Structur A

La palabra clave extern especifica que una variable o funcion tiene un en-lace externo. Esto significa que la variable o funcion a la que nos referimosesta definida en algun otro archivo fuente, o mas adelante en el mismo archi-vo. Sin embargo, en C/C++ podemos usar esta palabra clave extern con unacadena. La cadena indica que se esta usando el convenio de enlace de otrolenguaje para los identificadores que se estan definiendo. Para los programasC++ la cadena por defecto es “C++”.

Los enunciados

pi = (int *) malloc(sizeof(int));

pr = (float *) malloc(sizeof(float));

crean directamente la variable entera *pi y la variable real *pr. Estas se de-nominan variables dinamicas. Al ejecutar estos enunciados, el operador sizeofdevuelve el tamano en bytes de su operando. Esto se usa para conservar laindependencia de maquina. Despues, malloc crea un objeto de este tamano.Por tanto, malloc(sizeof(int)) asigna almacenamiento para un entero, entanto que malloc(sizeof(float)) asigna espacio necesario para un real. Deigual manera, malloc devuelve un apuntados al almacenamiento que asigna.Este apuntador es al primer byte de este almacenamiento y es de tipo char *.Para obligar al apuntador a que senale a un entero, usamos el operador decalculo (int *) o (float *).

El operador sizeof, devuelve un valor de tipo int, en tanto que la funcionmalloc espera un parametro de tipo unsigned. Para hacer que correspondan,debemos escribir

pi=(int *)malloc((unsigned)(sizeof(int)));

Como ejemplo, vamos a considerar este breve codigo:

#include <iostream>

int main (int argc, char * const argv[]) {

( 1) int *p, *q;

( 2) int x;

( 3) p = (int *)malloc(sizeof(int));

( 4) *p = 3;

( 5) q = p;

( 6) std::cout<< *p << " " << *q << "\n";

( 7) x = 7;

( 8) *q = x;

( 9) std::cout<< *p << " " << *q << "\n";

(10) p = (int *)malloc(sizeof(int));

(11) *p = 5;

52

Page 53: e Structur A

(12) std::cout<< *p << " " << *q << "\n";

return 0;

}

En la l’inea (3), se crea una variable de tipo entero y su direccion se colocaen p. La lınea (4) establece el valor de esa variable en 3. La lınea (5) haceque la direccion q sea la misma direccion que p. El enunciado de la lınea (5)es perfectamente valido, pues se asigna a una variable de tipo apuntador (q)el valor de otra variable del mismo tipo (p). En este momento *p y *q hacenreferencia a la misma variable. Por tanto, la lınea (6) imprime el contenido deesa variable (que ahora es 3) dos veces.

En la lınea (7), se almacena el valor 7 en la variable entera x. La lınea (8)cambia el valor de *q al valor de x. sin embargo, dado que p y q apuntan ala misma variable, *p y *q tienen el valor 7. Por tanto la lınea (9) imprime elnumero 7 dos veces.

La lınea (10) crea una nueva variable entera y coloca su direccion en p. Ahora*p hace referencia a la variable entera recien creada que todavıa no ha recibidoun valor. q no ha cambiado; por lo que el valor de *q sigue siendo 7. Observe-mos que *p no hace referencia a una variable especıfica unica. Su valor cambiaconforme se modifica el valor de p. La lınea (11) establece el valor de estavariable recien creada en 5 y la lınea 12 imprime los valores 5 y 7. Y ası lasalida del programa es:

3 3

7 7

5 7

mallocEjemplo has exited with status 0.

La funcion free se usa en C para liberar almacenamiento de una variableasignada dinamicamente. La orden

free(p);

invalida cualquier referencia futura a la variable *p (a menos que se asignenuevo espacio de memoraia a esa variable). Llamar free(p) hace que quededisponible para reuso el almacenamiento ocupado por *p, si es necesario.

La funcion free espera un parametro apuntador del tipo char *, para que notengamos problemas de tipos, debemos hacer

free((char *)p);

Consideremos el siguiente ejemplo para ilustrar el uso de free:

53

Page 54: e Structur A

#include <iostream>

int main (int argc, char * const argv[]) {

int *p, *q;

( 1) p=(int *)malloc(sizeof(int));

( 2) *p=5;

( 3) q=(int *)malloc(sizeof(int));

( 4) *q=8;

( 5) free(p);

( 6) p=q;

( 7) q=(int *)malloc(sizeof(int));

( 8) *q=6;

( 9) std::cout<<*p<<" "<<*q<<"\n";

return 0;

}

¿Que se imprime a la salida del programa?

Observemos que si se llama malloc dos veces sucesivas y se asigna su valor ala misma variable como en:

p=(int *)malloc(sizeof(int));

*p=3;

p=(int *)malloc(sizeof(int));

*p=7;

Se pierde la primera copia de *p, dado que su direccion no se guardo.

5.4. Listas ligadas usando memoria dinamica

Para hacer las listas ligadas necesitamos un conjunto de nodos, cada uno delos cuales tiene dos campos: uno de informacion y un apuntador al siguientenodo de la lista. Ademas, un apuntador externo senala el primer nodo de lalista. Usamos variables de apuntador para implementar apuntadores de listas.Ası que definimos el tipo de un apuntador y un nodo mediante

struct node{

int info;

struct node *next;

};

typedef struct node *nodePtr;

54

Page 55: e Structur A

Un nodo de este tipo es igual a los nodos de la implementacion con arreglos,excepto que el campo next es un apuntador y no un entero.

En lugar de declarar un arreglo, para que represente un conjunto acumuladode nodos, estos se asignan y liberan segun es necesario. Se elimina la necesidadde un conjunto de nodos previamente declarado.

Si declaramos

nodePtr p;

la ejecucion de la orden

p=getNode();

debe colocar la direccion de un nodo disponible en p:

nodePtr getNode(void){

nodePtr p;

p=(nodePtr)malloc(sizeof(struct node));

return(p);

}

Para liberar la memoria utilizada usamos freeNode.

void freeNode(nodePtr p){

free(p);

}

Los procedimientos insAfter y delAfter usan la implementacion dinamica deuna lista ligada. Supongamos que list es una variable apuntador que senalaal primer nodo de una lista (si lo hay) y es igual a NULL en el caso de unalista vacıa.

void insAfter(nodePtr p, int x){

nodePtr q;

if(p==NULL){

std::cout<<"Insercion nula\n";

} else{

q=getNode();

q->info=x;

q->next=p->next;

p->next=q;

}

}

void delAfter(nodePtr p, int *px){

55

Page 56: e Structur A

nodePtr q;

if((p==NULL)||(p->next==NULL)){

std::cout<<"Borrado prohibido\n";

} else{

q=p->next;

*px=q->info;

p->next=q->next;

freeNode(q);

}

}

5.5. Ejercicios de programacion

1. Implemente una pila usando memoria dinamica en listas ligadas. Imple-mente las operaciones push, pop, empty y stackTop.

2. Implemente una cola usando memoria dinamica en listas ligadas. Imple-mente las operaciones empty, insert y remove.

3. Desarrolle un programa para buscar un elemento en la lista (de numerosenteros) y borrar la primera ocurrencia de ese elemento.

4. Desarrolle un programa para buscar un elemento en la lista (de numerosenteros) y borrar todas las ocurrencias de ese elemento.

5. Las listas doblemente ligadas tienen nodos que estan divididos en tressegmentos:a) Anterior: Un apuntador a un nodob) Info: La informacion de un nodoc) Siguiente: Un apuntador a un nodo

Implemente las operaciones borrarNodo(p), insertarNodoAntes einsertarNodoDespues.

56

Page 57: e Structur A

6. Arboles

Los arboles son estructuras de datos utiles en muchas aplicaciones. Hay variasformas de arboles y cada una de ellas es practica en situaciones especiales, eneste capıtulo vamos a definir algunas de esas formas y sus aplicaciones.

6.1. Concepto general de arbol

Desde el punto de vista de estructuras de datos, un arbol es un conceptosimple en su definicion, sin embargo es muy ingenioso. Un arbol es un grafocon caracterısticas muy especiales:

Definicion 9 Un arbol es un grafo A que tiene un unico nodo llamado raızque:

Tiene 0 relaciones, en cuyo caso se llama nodo hojatiene un numero finito de relaciones, en cuyo caso, cada una de esas rela-ciones es un subarbol

Para empezar a estudiar los arboles, nos concentraremos en primer lugar enel caso en que el nodo raız tenga 0, 1 o 2 subarboles.

6.2. Arboles binarios

Definicion 10 Un arbol binario es una estructura de datos de tipo arbol endonde cada uno de los nodos del arbol puede tener 0, 1, o 2 subarboles llamadosde acuerdo a su caso como:

Si el nodo raız tiene 0 relaciones se llama hoja.Si el nodo raız tiene 1 relacion a la izquierda, el segundo elemento de larelacion es el subarbol izquierdo.Si el nodo raız tiene 1 relacion a la derecha, el segundo elemento de larelacion es el subarbol derecho.

La figura 25 muestra algunas configuraciones de grafos que sı son arbolesbinarios, y la figura 26 muestra algnas configuraciones de grafos que no sonarboles binarios.

Vamos a dar una lista de teerminos que se usan frecuentemente cuando setrabaja con arboles:

57

Page 58: e Structur A

Figura 25. Grafos que son estructuras tipo arbol binario

Figura 26. Grafos que no son arboles binarios

Si A es la raız de un arbol y B es la raız de su subarbol izquierdo (o dere-cho), se dice que A es el padre de B y se dice que B es el hijo izquierdo (oderecho) de A.

Un nodo que no tiene hijos se denomina hoja

El nodo a es antecesor del nodo b (y recıprocamente el nodo b es descen-diente del nodo a), si a es el padre de b o el padre de algun ancestro de b.

Un nodo b es un descendiente izquierdo del nodo a, si b es el hijo izquierdode a o un descendiente del hijo izquierdo de a. Un descendiente derechose define de la misma forma.

Dos nodos son hermanos si son hijos izquierdo y derecho del mismo padre.

Otros terminos relacionados con arboles, tienen que ver con su funcinoamientoy topologıa:

Si cada nodo que NO es una hoja tiene un subarbol izquierdo y un subarbolderecho, entonces se trata de un arbol binario completo.

El nivel de un nodo es el numero de aristas que se deben recorrer para

58

Page 59: e Structur A

llegar desde ese nodo al nodo raız. De manera que el nivel del nodo raız es0, y el nivel de cualquier otro nodo es el nivel del padre mas uno.

La profundidad de un nodo es el maximo nivel de cualquier hoja en elarbol.

Si un arbol binario tiene m nodos en el nivel l, el maximo numero de nodosen el nivel l + 1 es 2m. Dado que un arbol binario solo tiene un nodo en elnivel 0, puede contener un maximo de 2l nodos en el nivel l. Un arbol binariocompleto de profundidad d es el arbol que contiene exactamente 2l nodos encada nivel l entre 0 y d. La cantidad total de nodos tn en un arbol binariocompleto de profundidad d, es igual a la suma de nodos en cada nivel entre 0y d, por tanto:

tn = 20 + 21 + 22 + · · ·+ 2d =d∑

j=0

2j

Usando induccion matematica se puede demostrar que∑d

j=0 2j = 2d+1 − 1.Dado que todas las hojas en este arbol estan en el nivel d, el arbol contiene2d hojas y, por tanto, 2d − 1 nodos que no son hojas.

Si conocemos el numero total de nodos tn en un arbol binario completo, pode-mos calcular su profundidad d, a partir de la expresion tn = 2d+1−1. Ası sabe-mos que la profundidad d es igual a 1 menos que el numero de veces que 2debe ser multiplicado por sı mismo para llegar a tn + 1. Es decir, que en unarbol binario completo,

d = log2(tn + 1)

Definicion 11 Un arbol binario es un arbol binario casi completo si:

1. Cualquier nodo nd a un nivel menor que d− 1 tiene 2 hijos2. Para cualquier nodo nd en el arbol con un descendiente derecho en el

nivel d debe tener un hijo izquierdo y cada descendiente izquierdo de nd:es una hoja en el nivel d otiene dos hijos

Los nodos en un arbol binario (completo, casi completo o incompleto) sepueden enumerar del siguiente modo. Al nodo raız le corresponde el numero1, al hijo izquierdo le corresponde el doble del numero asignado al padre y alhijo derecho le corresponde el doble mas 1 del numero asignado al padre.

59

Page 60: e Structur A

Figura 27. Comparacion de un arbol binario y un arbol binario casi completo. Elarbol mostrado en (A) descumple la regla 2 de los arboles binarios casi completos.

6.2.1. Operaciones con arboles binarios

Con los arboles binarios es posible definir algunas operaciones primitivas, estasoperaciones son en el sentido de saber la informacion de un nodo y sirven paradesplazarse en el arbol, hacia arriba o hacia abajo.

info(p) que devuelve el contenido del nodo apuntado por p.left(p) devuelve un apuntador al hijo izquierdo del nodo apuntado por p, o

bien, devuelve NULL si el nodo apuntado por p es una hoja.right(p) devuelve un apuntador al hijo derecho del nodo apuntado por p, o

bien, devuelve NULL si el nodo apuntado por p es una hoja.father(p) devuelve un apuntador al padre del nodo apuntado por p, o bien,

devuelve NULL si el nodo apuntado por p es la raız.brother(p) devuelve un apuntador al hermano del nodo apuntado por p, o

bien, devuelve NULL si el nodo apuntado por p no tiene hermano.

Estas otras operaciones son logicas, tienen que ver con la identidad de cadanodo:

isLeft(p) devuelve el valor true si el nodo actual es el hijo izquierdo delnodo apuntado por p, y false en caso contrario.

isRight(p) devuelve el valor true si el nodo actual es el hijo derecho delnodo apuntado por p, y false en caso contrario.

isBrother(p) devuelve el valor true si el nodo actual es el hermano del nodoapuntado por p, y false en caso contrario.

Como ejemplo, un algoritmo para el procedimiento isLeft es como sigue:

q=father(p);

if(q==NULL)

return(false) /* porque p apunta a la raiz */

60

Page 61: e Structur A

if (left(q)==p)

return(true);

return(false);

En la construccion de un arbol binario son utiles las operaciones makeTree,setLeft y setRight. La operacion makeTree(x) crea un nuevo arbol binarioque consta de un unico nodo con un campo de informacion x y devuelve unapuntador a ese nodos. La operacion setLeft(p,x) acepta un apuntador p

a un nodo de arbol binario sin hijo izquierdo. Crea un nuevo hijo izquierdode node(p) con el campo de informacion x. La operacion setRight(p,x) essimilar, excepto que crea un hijo derecho.

6.2.2. Aplicaciones de arboles binarios

Un arbol binario es una estructura de datos util cuando se trata de hacermodelos de procesos en donde se requiere tomar decisiones en uno de dossentidos en cada parte del proceso. Por ejemplo, supongamos que tenemos unarreglo en donde queremos encontrar todos los duplicados. Esta situacion esbastante util en el manejo de las bases de datos, para evitar un problema quese llama redundancia.

Una manera de encontrar los elementos duplicados en un arreglo es recorrertodo el arreglo y comparar con cada uno de los elementos del arreglo. Estoimplica que si el arreglo tiene n elementos, se deben hacer n comparaciones,claro, no es mucho problema si n es un numero pequeno, pero el problema seva complicando mas a medida que n aumenta.

Si usamos un arbol binario, el numero de comparaciones se reduce bastante,veamos como.

El primer numero del arreglo se coloca en la raız del arbol (como en esteejemplo siempre vamos a trabajar con arboles binarios, simplemente diremosarbol, para referirnos a un arbol binario) con sus subarboles izquierdo y dere-cho vacıos. Luego, cada elemento del arreglo se compara son la informaciondel nodo raız y se crean los nuevos hijos con el siguiente criterio:

Si el elemento del arreglo es igual que la informacion del nodo raız, entoncesnotificar duplicidad.Si el elemento del arreglo es menor que la informacion del nodo raız, entoncesse crea un hijo izquierdo.Si el elemento del arreglo es mayor que la informacion del nodo raız, entoncesse crea un hijo derecho.

Una vez que ya esta creado el arbol, se pueden buscar los elementos repetidos.Si x el elemento buscado, se debe recorrer el arbol del siguiente modo:

61

Page 62: e Structur A

Sea k la informacion del nodo actual p. Si x > k entonces cambiar el nodoactual a right(p), en caso contrario, en caso de que x = k informar unaocurrencia duplicada y en caso de que x ≥ k cambiar el nodo actual a left(p).

El siguiente algoritmo

leer numero buscado >> n

tree=makeTree(n)

while(hay numeros en el arreglo){

leeSiguienteNumero >> k

p=q=tree;

while(k!=info(p)&&q!=NULL){

p=q

if(k<info(p))

q=left(p)

else

q=right(p)

}

if(k==info(p))

despliega<<" el numero es duplicado";

else

if (k<info(p))

setLeft(p,k)

else

setRight(p,k)

}

Figura 28. Arbol binario para encontrar numeros duplicados

Para saber el contenido de todos los nodos en un arbol es necesario recorrerel arbol. Esto es debido a que solo tenemos conocimiento del contenido dela direccion de un nodo a la vez. Al recorrer el arbol es necesario tener ladireccion de cada nodo, no necesariamente todos al mismo tiempo, de hechonormalmente se tiene la direccion de uno o dos nodos a la vez; de manera quecuando se tiene la direccion de un nodo, se dice que se visita ese nodo.

62

Page 63: e Structur A

Aunque hay un orden preestablecido (la enumeracion de los nodos) no siemprees bueno recorrer el arbol en ese orden, porque el manejo de los apuntadoresse vuelve mas complejo. En su lugar se han adoptado tres criterios princi-pales para recorrer un arbol binario, sin que de omita cualquier otro criteriodiferente.

Los tres criterios principales para recorrer un arbol binario y visitar todos susnodos son, recorrer el arbol en:

preorden: Se ejecutan las operaciones:1. Visitar la raız2. recorrer el subarbol izquierdo en preorden3. recorrer el subarbol derecho en preorden

entreorden: Se ejecutan las operaciones:1. recorrer el subarbol izquierdo en entreorden2. Visitar la raız3. recorrer el subarbol derecho en entreorden

postorden: Se ejecutan las operaciones:1. recorrer el subarbol izquierdo en postorden2. recorrer el subarbol derecho en postorden3. Visitar la raız

Al considerar el arbol binario que se muestra en la figura 28 usando cada unode los tres criterios para recorrer el arbol se tienen las siguientes secuencias denodos:

En preorden: 〈14, 4, 3, 9, 7, 5, 15, 18, 16, 17, 20〉

En entreorden: 〈3, 4, 5, 7, 9, 14, 15, 16, 17, 18, 20〉

En postorden: 〈3, 5, 7, 9, 4, 17, 16, 20, 18, 15, 14〉

Esto nos lleva a pensar en otra aplicacion, el ordenamiento de los elementosde un arreglo.

Para ordenar los elementos de un arreglo en sentido ascendente, se debe con-struir un arbol similar al arbol binario de busqueda, pero sin omitir las coin-cidencias.

El arreglo usado para crear el arbol binario de busqueda fue

<14,15,4,9,7,18,3,5,16,4,20,17,9,14,5>

El arbol de ordenamiento es el que se muestra en la figura 29

Para ordenar los elementos de este arreglo basta recorrer el arbol en forma deentreorden.

63

Page 64: e Structur A

Figura 29. Arbol binario para ordenar una secuencia de numeros

¿Cual serıa el algoritmo para ordenarlo de manera descendente?

6.3. Representacion en C/C++ de los arboles binarios

Vamos a estudiar estas representaciones por partes, primero los nodos y elarbol; despues las operaciones para el manejo del arbol.

6.3.1. Representacion de los nodos

Los nodos de los arboles binarios son estructuras en C/C++ que estan com-puestas por tres partes:

Un apuntador al subarbol izquierdo, leftUn apuntador al subarbol derecho, rightUna parte de informacion, que puede ser una estructura en sı misma, info.Adicionalmente es muy util poner un apuntador al padre del nodo. father.

Usando una implementacion de arreglos tenemos:

#define numNodes 500

struct nodeType{

int info;

int left;

int right;

int father;

};

struct nodeType node[numNodes];

y usando una representacion con memoria dinamica, los nodos de un arbol sepuede representar tambien con una estructura en C/C++ :

struct nodeType{

64

Page 65: e Structur A

int info;

struct nodeType *left;

struct nodeType *right;

struct nodeType *father;

};

struct nodeType *nodePtr;

La operaciones info(p), left(p), right(p) y father(p) se implementarıanmediante referencias a p->info, p->left, p->right y p->father respectiva-mente. Las rutinas getnode y freenode simplemente asignan y liberan nodosusando las rutinas malloc y free.

nodePtr makeTree(int x){

nodePtr p;

p = getNode();

p->info = x;

p->left = NULL;

p->right = NULL;

return p;

}

La rutina setLeft(p,x) establece un nodo con contenido x como el hijoizquierdo de node(p).

void setLeft(nodePtr p, int x){

if(p == NULL)

std::cout<<"Insercion nula\n";

else

if(p->left != NULL)

std::cout<<"Insercion no valida\n";

else

p->left=maketree(x);

}

La rutina para setRight(p,x) es similar a la rutina anterior.

Cuando se establece la diferencia entre los nodos de hojas y los no-hojas, losnodos que no son hojas se llaman nodos internos y los nodos que sı sonhojas se llaman nodos externos.

6.3.2. Recorridos de arbol binario en C/C++

Aquı usaremos recursividad para hacer estas rutidas de los recorridos dearboles binarios. Las rutinas se llaman preTr, inTr y postTr, que impri-men el contenido de los nodos de un arbol binario en orden previo, en orden

65

Page 66: e Structur A

y en orden posterior, respectivamente.

El recorrido en pre orden se logra con esta rutina:

void preTr(nodePtr tree){

if (tree != NULL){

std::cout<<tree->info;

preTr(tree->left);

preTr(tree->right);

}

}

El recorrido en entre-orden se logra con esta rutina:

void inTr(nodePtr tree){

if (tree != NULL){

inTr(tree->left);

std::cout<<tree->info;

inTr(tree->right);

}

}

y el recorrido en post-orden se logra con esta rutina:

void postTr(nodePtr tree){

if (tree != NULL){

postTr(tree->left);

postTr(tree->right);

std::cout<<tree->info;

}

}

6.4. Arboles

Hasta ahora hemos visto los arboles binarios que son aquellos arboles que susnodos solamente pueden tener un maximo de dos hijos. Cuando ocurre quelos nodos tienen cualquier numero finito de hijos, son arboles (en genreal). Demanera que

Definicion 12 Un arbol es un conjunto finito no vacıo de elementos en elcual un elemento se denomina la raız y los restantes se dividen en m ≥ 0subconjuntos disjuntos, cada uno de los cuales es por sı mismo un arbol. Cadaelemento en un arbol se denomina un nodo del arbol

66

Page 67: e Structur A

Un nodo sin subarboles es una hoja. Usamos los terminos padre, hijo, her-mano, antecesor, descendiente, nivel y profundidad del mismo modoque en los arboles binarios. El grado de un nodo es en numero maximo dehijos que alun nodo tiene.

Un arbol ordenado de define como un arbol en el que los subarboles de cadanodo forman un conjunto ordenado. En un arbol ordenado, podemos hablardel primero, segundo o ultimo hijo de un nodo en particular. El primer hijo deun nodo en un arbol ordenado se denomina con frecuencia el hijo mas viejode este nodo y el ultimo se denomina el hijo mas joven. Vease la figura 30.Un bosque es un conjunto ordenado de arboles ordenados.

Figura 30. El arbol de la izquierda es ordenado y el arbol de la derecha es un arbolno ordenado.

6.4.1. Representacion dinamica en C de los arboles

Al igual que en los arboles binarios, los nodos en un arbol tienen una partede informacion, un apuntador al padre y uno o mas apuntadores a los hijos.De manera que una solucion es crear una estructura que incluya una listadinamica de apuntadores, como lo muestra la figura 31.

Figura 31. Representacion con listas de los nodos de un arbol

struct treeNode{

67

Page 68: e Structur A

int info;

struct treeNode *father;

struct treeNode *son;

struct treeNode *next;

};

typedef struct treeNode *nodePtr;

Si todos los recorridos se realizan de un nodo a sus hijos se omite el campofather. Incluso si es necesario acceder al padre de un nodo, el campo father

se omite colocando un apuntador al padre en el campo next del hijo masjoven, en lugar de dejarlo en null. Se podrıa usar un campo logico adicionalpara indicar si el campo next apunta al siguiente hijo “real” o al padre.

Si consideramos que son corresponde al apuntador left de un nodo de arbolbinario y que next corresponde a su apuntador right, este metodo representaen realidad un arbol ordenado general mediante un arbol binario.

6.4.2. Recorridos de arbol

Los metodos de recorrido para arboles binarios inducen metodos para recorrerlos arboles en general. Si un arbol se representa como un conjunto de nodosde variables dinamicas con apuntadores son y next, una rutina en C/C++ paraimprimir el contenido de sus nodos se escribirıa como:

void inTr(nodePtr tree){

if (tree != NULL){

inTr(tree->left);

std::cout<<tree->info;

inTr(tree->right);

}

}

Las rutinas para recorrer el arbol en los demas ordenes son similares. Estosrecorridos tambien se defininen directamente ası:

Orden previo: similar al caso binario.1. Visitar la raız2. Recorrer en orden previo los subarboles de izquierda a derecha

Las demas rutinas son similares.

Un bosque puede ser representado medianto un arbol binario.

68

Page 69: e Structur A

Para hacer esta representacion, la raız de cada arbol se coloca en una listade apuntadores; luego para cada nodo en la lista (la raız de cada arbol) seprocede del siguiente modo:

1. Se crea una lista de subarboles izquierdos con los apuntadores a cada unode los arboles en el bosque.

2. si un nodo tiene mas de un hijo, entonces se crea un subarbol izquierdoy se forma una lista de subarboles izquierdos con todos los hijos de esenodo.

Figura 32. Arriba: Un bosque de arboles. Abajo: El arbol binario que correspondea ese bosque.

Para recorrer los nodos de un bosque, es preferible convertir todo el bosqueen un arbol binario correspondiente, como se ilustra en la figura 32. Cuandoya se tiene el arbol binario que corresponde a ese bosque, entonces se aplicanlas rutinas ya conocidas.

Si el bosque es un bosque ordenado, es decir, que todos los arboles del bosqueson arboles ordenados; entonces un recorrido en entreorden dara como resul-tado una secuencia de nodos ordenada en sentido ascendente.

6.5. Ejercicios de programacion

1. Escriba un programa que acepte un apuntador a un nodo y devuelva unvalor verdadero si este nodo es la raız de un arbol binario valido y falsoen caso contrario.

2. Escriba un programa que acepte un apuntador a un arbol binario y unapuntador a un nodo del arbol, y devuelva el nivel del nodo en el arbol.

3. Escriba un programa para ejecutar el experimento siguiente: genere 100numeros aleatorios. Conforme se genera cada numero, insertelo en un

69

Page 70: e Structur A

arbol de busqueda binaria inicialmente vacıo. Despues de insertar los 100numeros, imprima el nivel de la hoja que tiene el nivel mas grande yel nivel de la hoja que tiene el nivel mas chico. Repita este proceso 50veces. Imprima una tabla que indique cuantas veces de las 50 ejecucionesprodujeron una diferencia entre el nivel de hoja maximo y mınimo de0,1,2,3, y ası sucesivamente.

4. Implemente los recorridos de los arboles binarios.5. Si un bosque se representa mediante un arbol binario, muestre que el

numero de vınculos derechos nulos es 1 mayor que el numero de no hojasdel bosque.

70

Page 71: e Structur A

7. Grafos

En esta parte del curso vamos a retomar la idea de los grafos. Hasta ahorahomos visto las listas y los arboles como casos especiales de los grafos. Re-sumiendo, las listas son grafos en donde cada nodo tiene una arista que sale yuna arista que llega, excepto un par de nodos, uno de esos nodos es el iniciode la lista que tiene no tiene arista que entra; y el otro nodo es el final de lalista que no tiene arista que sale; En los arboles, los nodos tienen una aristaque llega (la del padre) y una o mas aristas que salen (los hijos).

Como veremos mas adelante con mucho mayor detalle, los nodos en los grafosno tienen lımite de aristas que salen o aristas que lleguen, por eso tanto laslistas como los arboles son casos particulares de los grafos.

7.1. Recordatorio de las definiciones

Un grafo consiste de una tupla G = 〈N, A 〉, en donde N es un conjunto deelementos llamados nodos; y A es una relacion, representada por un conjuntode pares ordenados de nodos.

El conjunto N de nodos debe de ser un conjunto no-vacıo, esto significa quepara que exista un grafo es necesario al menos un nodo. El conjunto A dearistas puede ser el conjunto vaco. En la figura 33 se muestra un grafo y susconjuntos de nodos y de aristas.

Figura 33. Grafo dirigido o digrafo

Si las aristas de un grafo no estan dirigidas se omiten las flechas, y se diceentonces que es un grafo (no un grafo dirigido). Cuando en las aristas no hayflechas, se entiende que hay una relacion reflexiva, es decir, si para un grafo

71

Page 72: e Structur A

G = 〈N, A 〉; a, b ∈ N y se tiene que (a, b), (b, a) ∈ A , entonces no se dibujanlas flechas. Porque la flecha indica el sentido de la relacion.

Si G = 〈N, A〉 es un grafo, los siguientes terminos son frecuentemente usadosal trabajar con G:

Nodo incidente: Si ∃ (a, b) ∈ A entonces tanto el nodo a como el nodo bson nodos incidentes.

Grado de incidencia: Se define para cada nodo, y es su maximo numero deincidencias. Tambien se conoce con el nombre de “valencia”.

Grado interno: Tambien se define para cada nodo y es el numero de aris-tas que llegan a ese nodo. Otro nombre para este termino es “valencia deentrada”.

Grado externo: Para cada nodo es el numero de aristas que salen del nodo.Se conoce tambien con el nombre de “valencia de salida”.

Adyacencia: Si a, b ∈ N , el nodo a es adyacente al nodo b si (a, b) ∈ A .Note que si (b, a) ∈ A , pero (a, b) 6∈ A , entonces el nodo a no es adyacenteal nodo b, pero el nodo b si es adyacente al nodo a.

Sucesor: Si el nodo a es adyacente al nodo b, entonces el nodo b es el sucesordel nodo a.

Antecesor: Si el nodo a es adyacente al nodo b, entonces el nodo a es elantecesor del nodo b.

Es posible asociar una etiqueta a cada arista, como se muestra en la figura 34.La etiqueta asociada con cada arista se denomina peso.

Figura 34. Grafo dirigido con pesos

Los grafos ponderados son relaciones definidas por un conjunto de elementos,en donde cada elemento es un trio ordenado (a, b, c) donde a, b ∈ N y c ∈ W ,para algun conjunto W de pesos. Con los grafos y grafos ponderados (los quetienen pesos) se pueden tener algunas operaciones basicas:

72

Page 73: e Structur A

Con grafos:join(a,b): Agrega una relacion del nodo a al nodo b. Si la relacion no

existe, entonces crea una relacion.removeArc(a,b): Quita un arco del nodo a al nodo b

Con grafos ponderados:joinWt(a,b,w): Agrega una relacion del nodo a al nodo b y le asocia el

peso w. Si la relacion no existe, entonces de crea la relacion y le asocia elpeso indicado.

removeArcWt(a,b): Quita un arco del nodo a al nodo b con peso w.La operacion isAdjacent(a,b) devuelve un valor TRUE si el nodo a esadyacente al nodo b, y devuelve un valor FALSE en caso contrario.

Una trayectoria de longitud k del nodo a al nodo b se define como una se-cuencia de k + 1 nodos 〈n1, n2, . . . , nk, nk+1, 〉 tal que n1 = a, nk+1 = b yisAdjacent(ni,ni+1) para todas las 1 ≤ i < k.

Una trayectoria de longitud 1 un nodo a sı mismo es un autociclo. Si existeuna trayectoria de longitud mayor que 1 de un nodo a sı mismo, entonces esun ciclo. Si el grafo es acıclico y dirigido, entonces se llama dag (directedacyclic graph).

7.2. Aplicacion ejemplo

Supongamos el grafo ponderado de la figura 35, este grafo tiene como conjuntode nodos N = {3, 10, 17, 5, 8, 6} y una relacion

R = {(3, 10, 1), (10, 17, 7), (8, 17, 1), (5, 8, 3), (5, 6, 1), (6, 17, 5)}

Figura 35. Grafo G = 〈N,R〉

Se desea saber si existe un camino entre un par de nodos dado.

73

Page 74: e Structur A

Referencias

[LAT97] Yedidyah Langsam, Moshe J. Augenstein, and Aaron M. Tenenbaum.Estructura de datos con C y C++. Prentice-Hall, Inc., 2a edition,1997.

[MP97] William H. Murray and Chris H. Pappas. Manual de Borland C++,volume ISBN: 0-07-882216-5. Osborne McGraw-Hill, 1997.

[TA83] Aaron M. Tenenbaum and Moshe J. Augenstein. Estructura de datosen Pascal. Phh-PrenticeHall, 1983.

74