Está en la página 1de 74

Apuntes para el curso de Estructuras de datos en C/C++

Dr. Abdiel E. Cceres Gonzlez a a


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 modicaciones. No hay una estructura de datos que sirva para todos los propsitos, y por eso es importante saber sus ventajas y desventajas. Este documeno to es una coleccin de apuntes para el curso de Estructuras de Datos. Los apuntes o se han tomado de algunas fuentes que son detalladas en la seccin de bibliograf o a.

Indice 1. Preliminares de programacin en C/C++ o 3 3 10 15 19 21 21 24 25 26 27 29

1.1. Arreglos 1.2. Apuntadores 1.3. Estructuras C/C++ 1.4. Ejercicios de programacin o 2. La pila

2.1. Denicin y ejemplos o 2.2. Operaciones bsicas a 2.3. Ejemplo: Nmero de parntesis u e 2.4. La estructura de datos Pila en C/C++ 2.5. La representacin en C/C++ de las operaciones de una pila o 2.6. Problemas de programacin o

3.

Colas

31 32 33 34 36 39 40 42 42 44 51 54 56 57 57 57 64 66 69 71 71 73

3.1. Estructura de las colas en C/C++ 3.2. Colas con prioridad 3.3. Ejercicio de programacin o 4. Recursin o

4.1. Peligros en la recursividad 4.2. Ejercicios de programacin o 5. Listas

5.1. Grafos 5.2. Listas simplemente encadenadas 5.3. El uso de memoria dinmica en C/C++ a 5.4. Listas ligadas usando memoria dinmica a 5.5. Ejercicios de programacin o 6. Arboles

6.1. Concepto general de rbol a 6.2. Arboles binarios 6.3. Representacin en C/C++ de los rboles binarios o a 6.4. Arboles 6.5. Ejercicios de programacin o 7. Grafos

7.1. Recordatorio de las deniciones 7.2. Aplicacin ejemplo o

1.

Preliminares de programacin en C/C++ o

En esta seccin recordaremos tres temas de programacin en C/C++ que son o o fundamentales para estudiar estructuras de datos; estos temas son los arreglos, los registros y los punteros. Los tres temas han sido tomados fundamentalmente de [MP97]

1.1.

Arreglos

Denicin 1 Un arreglo se compone de elementos de igual tamao almaceo n 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 es un tipo de datos estndar; es un tipo agregado compuesto de cualquier otro a tipo de datos. Los arreglos se pueden denir usando tipos de datos mixtos debido a que se supone que todos los elementos son del mismo tamao. Puesto que todos los n elementos son del mismo tamao y ya que este hecho se utiliza para ayudar n a determinar cmo localizar un elemento dado, resulta que los elementos son o almacenados en localidades de memoria contiguas. Lo ms importante a tener en cuenta es: El nombre de un arreglo es visto por el a compilador como un puntero-constante al primer elemento del arreglo. Esto es muy importante: a) El nombre del arreglo es visto como un tipo puntero, y ms a espec camente, b) un puntero constante -signica una direccin de memoria o bloqueada para el primer elemento de un arreglo-. Por ejemplo, aunque una declaracin de arreglo toma la frma genrica: o o e Tipo_ElementoArray NombreArray [ NumeroDeElementos ] El compilador ve la declaracin como o Tipo_ElementoArray * const NombreArray = &NombreArray[0]; Por esta razn, un identicador de arreglo no puede ser usado nunca como un o valor-i (valor izquierdo). Los valores izquierdos representan variables que su contenido puede ser alterado por el programa; frecuentemente aparecen a la izquierda de las sentencias de asignacin. o Si los nombres de arreglo fueran variables izquierdos permitidos, el programa podr cambiar sus contenidos. a 3

float SalariosDeEmpleados[Max_empleados]; . . . SalariosDeEmpleados = 45739.0; El efecto har cambiar la direccin inicial del propio arreglo. a o

1.1.1.

Declaraciones de un arreglo

La sintaxis de declaracin de arreglos es: o tipo nombre_arreglo [numero_de_elementos]; Los siguientes son dos ejemplos de declaraciones de arreglos vlidas en C/C++ a : int CoordenadasDePantalla[5]; /*Un arreglo de 5 enteros */ char IDCompania[20]; /*Un arreglo de 20 caracteres */

Figura 1. Arreglo CoordenadasDePantalla con ndices de desplazamiento vlido a

En la gura 1 se muestra el primer arreglo que fue declarado con el tipo de nmeros enteros, llamado CoordenadasDePantalla, ocupa en memoria 5 u localidades de memoria contiguas, cada una de ellas capaz de almacenar un nmero entero. Actualmente es comn que los nmeros enteros sean de 32 u u u bits, esto hace que el arreglo CoordenadasDePantalla ocupe 32 5 = 160 bits No se permite utilizar nombres de variables dentro de los corchetes. Por esto no es posible evitar la especicacin del tamao del arreglo hasta la ejecucin del o n o programa. La expresin debe ser un valor constante, para que el compilador o sepa exactamente cunto espacio de memoria tiene que reservar para el arreglo. a Una buena prctica de programacin es usar constantes predenidas. a o 4

#define Coordenadas_Max 20 #define Tamano_MaX_Compania_Id 15 int CoordenadasDePantalla[Coordenadas_Max]; char IDCompania[Tamano_MaX_Compania_Id]; El uso de constantes predenidas garantiza que futuras referencias al arreglo no excedan el tamao del arreglo denido. n

1.1.2.

Iniciacin del arreglo o

C/C++ proporciona 3 maneras de iniciar elementos del arreglo: Por defecto: Cuando son creados, se aplica solamente a arreglos globales y estticos. a Expl cita: Cuando son creados, suministrando datos de iniciacin o Tiempo de ejecucin: Durante la ejecucin del programa cuando se asigo o 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 error involucra el acceso a los elementos del arreglo, por la simple razn de que o el primer elemento est en una posicin 0, no 1. De manera que el ultimo a o elemento del arreglo lo encontramos en n-1, donde n es el nmero de elementos. u Supongamos la siguiente declaracin: o int Estado[Rango_Maximo_Estado]={-1,0,1}; La siguiente sentencia tiene acceso a -1: Estado[0]; Si escribimos Estado[3] causar un error porque no hay 4 elementos. a

1.1.4.

Clculo del tamao de un arreglo (sizeof()) a n

Es frecuente utilizar el operador sizeof() para calcular la cantidad de espacio que se necesita almacenar para un objeto: /* * exploresz.cpp */ #include<iostream.h> 5

#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 trmino dimensin representa el nmero de e o u ndices utilizados para referirse a un elemento particular en el arreglo. Los arreglos de ms de una dimensin a o se 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

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 los elementos del arraglo a su respectiva distancia relativa desde la base. El arreglo creado tiene 4 las y 5 columnas por la, haciendo un total de 20 elementos enteros. Los arreglos multidimensionales son almacenados de forma lineal en la memoria de la computadora. Los elementos en los arreglos multidimensionales estn a agrupados desde el ndice ms a la derecha hacia el centro. En el ejemplo ana terior, la 1, columna 1 ser el elemento 3 del arreglo almacenado. Aunque el a clculo del desplazamiento aparece un poco dif es referenciado fcilmente a cil, a cada 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

1.1.6.

Arreglos como argumentos de funciones

Es necesario recordar tres cosas al pasar arreglos como parmetros de funa ciones: 1. Todos los arreglos son pasados en llamada-por referencia. 2. Debido a que el arreglo es pasado en llamada por referencia, ser ina correcto para la funcin llamada devolver el arreglo en una sentencia o return();. Esta sentencia est de ms. a a 3. Todos los elementos del arreglo son pasados a las funciones en llamada por valor. lo que signica que se pasa una copia del elemento, no la direccin o del 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

La salida del programa demuestra que el arreglo se pasa en llamada por referencia, ya que el primer ciclo for da como salida los contenidos de minsculas u originales: aeiou, mientras que el segundo ciclo for en main() da como salida los contenidos del arreglo despus del llamado a la funcin ArrayMayuscula(): e o AEIOU. Claramente, dentro del cuerpo de la funcin ArrayMayuscula(), ha cambiado o el arreglo de regreso en la funcin main(). el siguiente ejemplo es una simple o modicacin de este algoritmo, slo que en vez de pasar el arreglo completo, o o 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

aeiou aeiou valarray has exited with status 0.

1.2.

Apuntadores

Denicin 2 Un apuntador es una variable que contiene una direccin de o o memoria. Supongamos una variable de tipo entero que se llama contenidoRAM y otra variable que se llama direccionRAM que puede contener una variable de tipo entero. En C/C++ una variable precedida del operador & devuelve la direccin o de la variable en lugar de su contenido. As que para asignar la direccin de o una variable a otra variable del tipo que contiene direcciones se usan sentencias como esta: direccionRam = &contenidoRAM

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

En la gura 2 se ilustra el nombre de la variable contenidoRAM y se observa que se encuentra en la direccin 7751 de la memoria. El contenido de esta o localidad no se muestra. Una variable que contiene una direccin, tal como o direccionRAM, se llama variable apuntador o simplemente apuntador. Despues que la sentencia anterior se ejecuta, la direccin de contenidoRAM o ser asignada a la variable apuntador direccionRAM. La relacin se expresa a o diciendo que direccionRAM apunta a contenidoRAM. La gura 3 ilustra esta relacin. o El accceso al contenido de una celda cuya direccin est almacenada en la o a variable direccionRAM es tan sencillo como poner al inicio de la variable apuntador un asterisco: *direccionRAM. Lo que se ha hecho es eliminar la referencia directa. Por ejemplo, si se ejecutan las siguientes dos sentencias, el valor de la celda llamada contenidoRAM ser de 20 (vase la gura 4). a e 10

Figura 3. Notacin de echa para los apuntadores o

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 denicin para cada variable. Para denir una variable o apuntador direccionRAM que pueda contener la direccin de una variable o int, se escribe: int *direccionRAM; Realmente existen dos partes separadas en esta declaracin. El tipo de dato o de direccionRAM es: int * y el identicador para la variable es direccionRAM El asterisco que sigue a int signica apuntador a. Esto es, el siguiente tipo de dato es una variable apuntador que puede contener una direccin a un int: o int * 11

En C/C++ una variable apuntador contiene la direccin de un tipo de dato o particular: char *direccion_char; char *direccion_int; El tipo de dato de direccion char es diferente del tipo de dato de la variable apuntador direccion int. En un programa que dene un apuntador a un tipo de dato y utliza ste para apuntar a otro tipo de dato, pueden ocurrir e errores en tiempo de ejecucin y advertencias en tiempo de compilacin. Una o o prctica de programacin pobre ser denir un apuntador de una forma y a o a luego utilizar ste de alguna otra forma. Por ejemplo: e int *direccion_int; float un_float = 98.34; direccion_int = &un_float;

1.2.2.

Utilizacin de punteros en sentencias sencillas o

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

En la l nea (04) se han declarado tres variables de tipo entero, se da a cada celda un nombre y se inicializan 2 de stas. Supondremos que la direccin de e o o o memoria asignada para la variable A int es la direccin 5328, y la direccin o en memoria RAM asignada para la variable B int es la direccin 7916, y la celda llamada Temp int se le ha asignado la direccin 2385. Vase la gura 5; o e

Figura 5. Descripcin de las tres variables en la memoria o

En la l nea (05) se dene un apuntador a un tipo de dato entero llamado e direccion int. La sentencia asigna la celda y da a sta un nombre. Luego, en la l nea (09), la tercera sentencia asigna a direccion_int la direccin de A_int (gura 6). o

Figura 6. direccion int dada la direccin de A int o

La l nea (10) utiliza la expresin *direccion_int para acceder al contenido o de 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. Si no se pone el * enfrente de direccion_int;, la sentencia de asignacin alo macenar ilegalmente el contenido de direccion_int en la celda nombrada a Temp_int, pero se supone que Temp_int contiene un entero, no una direccin. o 13

Este puede ser un error muy dif de localizar puesto que muchos compicil ladores no emiten ninguna advertencia/error. Para empeorar el asunto, la mayor de los apuntadores son cercanos, lo que a signica que ocupan 2 bytes (4 bytes para aplicaciones de 32-bits), el mismo tamao que un entero en una PC. n La sentencia (11) copia el contenido de la variable B int en la celda apuntada por la direccin almacenada en direccion int(gura 7): o *direccion_int = B_int;

Figura 7. Se copia el contenido de B int usando la notacin de echa de apuntadores o

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

Figura 8. Se copia Temp int en B int utilizando asignacin normal. o

Debemos de asegurarnos de comprender la diferencia entre qu se referencia e cuando una variable puntero est precedida por el operador de indireccin y a o cundo no est precedida por este operador. a a Para este ejemplo, la primera sintaxis es un apuntador a una celda que puede contener un valor entero. La segunda sintaxis referencia la celda que contiene la direccin de otra celda que puede contener un entero. o 14

1.2.3.

Utilizacin incorrecta del operador de direccin o o

No se puede utilizar el operador de direccin sobre toda expresin C/C++ . El o o siguiente ejemplo demuestra aquellas situaciones donde no se puede aplicar el operador de direccin &. o puedeAlmacenarDireccionDeConstante = &37; int RAM_int = 5; puedeAlmacenarDireccionDeExpresionTemp = &(RAM_int +15); puedeAlmacenarDireccionDeRegistro = &varRegistro; La primera sentencia trata de obtener ilegalmente la direccin de un valor o constante integrado. La sentencia no tiene sentido puesto que 37 no tiene una celda de memoria asociada con ste. e La segunda sentencia de asignacin intenta devolver la direccin de la expreo o sin RAM_int+15. No existe direccin asociada con la expresin puesto que la o o o expresin en s misma es realmente un proceso de manipulacin de pila. o o Normalmente, el ultimo ejemplo respeta la demanda del programador para denir varRegistro como un registro ms que como una celda de almacea namiento en la memoria interna. Por consiguiente, no podr devolverse y a almacenarse la direccin de celda de memoria. El compilador C/C++ da la o memoria de variable, no el almacenamiento de registro.

1.3.

Estructuras C/C++

Denicin 3 Una estructura es un grupo de variables las cuales pueden ser o de diferentes tipos sostenidas o mantenidas juntas en una sola unidad. La unidad 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 dentro de la estructura. La etiqueta opcional se utiliza para crear otras variables del tipo particular de la estructura: struct campo_etiqueta{ tipo_miembro miembro_1; tipo_miembro miembro_2; 15

tipo_miembro miembro_3; : : tipo_miembro miembro_n; }; Un punto y coma naliza la denicin de una estructura puesto que sta es o e realmente 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 utilizando una sentencia similar a la siguiente: struct stbarco stbarco_usado; La sentencia dene stbarco_usado de tipo struct stbarco. La declaracin o requiere el uso del campo etiqueta de la estructura. Si esta sentencia est cona tenida dentro de una funcin, entonces la estructura, llamada stbarco_usado, o tiene un mbito local a esa funcin. Si la sentencia est contenida fuera de a o a todas las funciones de programa, la estructura tendr un mbito global. Es a a posible 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 declaracin de variable va antes del punto y coma nal. Cuando se o asocia slo una variable con el tipo estructura, el campo etiqueta puede ser o eliminado, por lo que ser posible escribir: a struct { char sztipo[iString15+iNull_char]; char szmodelo[iString15+iNull_char]; 16

char sztitular[iString20+iNull_char]; int ianio; long int lhoras_motor; float fprecioventa; } stbarco_usado;

1.3.2.

Utilizacin de miembros de estructuras o

Para accesar a los miembros de las estructuras se usa el punto u operador miembro (.). La sintaxis es: estructuraNombre.miembroNombre Por ejemplo en: gets(stbarco_usado.szmodelo); Aqu stbarco_usado es el nombre asociado con la estructura, y szmodelo es , una variable miembro de la estructura, otro ejemplo: std::cin>> stbarco_usado.sztipo; Esta sentencia leer la marca del stbarco_usado en el arreglo de caracteres, a mientras la prxima sentencia imprimir el precio de venta de stbarco_usado o a en 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

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

<< 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 tipos diferentes que int. Aunque las funciones que devuelven valores enteris no necesitan prototipos, se recomienda tener prototipos. Sin prototipos completos, se hacen las conversiones estndares, pero no se a checan los tipos o los nmeros de argumentos con el nmero de parmetros. u u a Los prototipos se usan para inicializar apuntadores a funciones, antes de que las funciones sean denidas. La lista de parmetros se usa para checar la correspondencia de los argua mentos en la llamada a la funcin con los parmetros en la denicin de la o a o funcin o const en parmetros de funciones El especicador const puede ser utilizado en la denicin de parmetros de o a funciones. Esto resulta de especial utilidad en tres casos. En los tres el n que se persigue es el mismo: indicar que la funcin no podr cambiar dichos o a argumentos: Con parmetros de funciones que sean de tipo matriz (que se pasan por a referencia). Ejemplo: int strlen(const char[]); Cuando los parmetros son punteros (a n de que desde dentro de la funcin a o no puedan ser modicados los objetos referenciados). Ejemplo: int printf (const char *format, ...); Cuando el argumento de la funcin sea una referencia, previniendo as que la o funcin pueda modicar el valor referenciado. Ejemplo: int dimen(const o X &x2);

1.4.

Ejercicios de programacin o

1. El siguiente algoritmo es el mtodo de insercin para ordenar elementos e o en 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

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 mtodo de insercin e o b) ilustre cmo opera el algoritmo insertionSort(A) usando como eno trada el arreglo A=<31,41,59,26,41,58> 2. Reescriba el programa y nmbrelo insertionSortNondec para que oro dene los elementos en orden decreciente 3. Considere el siguiente problema de bsqueda: u Input: Una secuencia de n nmeros A = a1 , a2 , . . . , an y un valor v. u Output: Un ndice i tal que v = A[i] o el valor espacial N IL si v no ocurre en A. Escriba un programa que resuelva este problema de bsqueda. u 4. Considere el problema de sumar dos nmeros binarios de longitud n. u Cada nmero se almacena en uno de los arreglos A y B de tamao n. La u n suma se almacena en un arreglo C de tamao n + 1, tambin como un n e nmero binario. Escriba un programa que resuelva este problema. u

20

2.

La pila

Uno de los conceptos ms utiles en las ciencias de la computacin es el de pila. a o En esta seccin vamos a denir este concepto de manera abstracta y veremos o cmo se usa para convertirse en una herramienta concreta y de gran valor en o las soluciones de problemas. La informacin contenida en esta seccin se ha o o tomado de [TA83].

2.1.

Denicin y ejemplos o

Denicin 4 Una pila (stack) es una coleccin ordenada de elementos en la o o cual se pueden insertar nuevos elementos por un extremo y se pueden retirar otros por el mismo extremo; ese estremos se llama la parte superior de la pila. Si tenemos un par de elementos en la pila, uno de ellos debe estar en la parte superior de la pila, que se considera el ms alto en la pila que el otro. En a la gura 9 el elemento F es el ms alto de todos los elementos que estn en la a a pila. El elemento D es el ms alto de los elementos A,B,C, pero es menor que a los elementos E y F.

Figura 9. Pila con 6 elementos

Para describir cmo funciona esta estructura, debemos agregar un nuevo eleo mento, el elemento G. Despus de haber agregado el elemento G a la pila, la e nueva conguracin es la que se muestra en la gura 10. o De acuerdo con la denicin, existe solamente un lugar en donde cualquier o elemento puede ser agregado a la pila. Despus de haber insertado el nuevo e elemento, G ahora es el elemento en la cima. Debedos aclarar en qu pila e deseamos insertar elementos, puesto que es posible tener ms de una pila al a mismo tiempo. 21

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

Cuando se desea retirar un elemento de la pila, solo basta ordenar que sea retirado un elemento; no podemos decir retira C de la pila, porque C no est en la cima de la pila y solamente podemos retirar el elemento que est en a a la cima. Para que la sentencia retira C de la pila tenga sentido, debemos replantear las rdenes a algo como: o Retira de la pila hasta que el elemento retirado sea C. Ni siquiera es necesario decir: Retira un elemento de la pila... porque es sobreentendido que solamente se puede sacar un elemento a la vez. Siguiendo nuestro ejemplo, ahora deseamos retirar de la pila P. La conguracin global de la pila es como se muestra en la gura 11 o

Figura 11. Operacin de retirar de la pila P o

El concepto de pila es muy importante en computacin y en especial en teor o a de lenguajes de programacin. En lenguajes procedurales como Pascal o C, la o pila es una estructura indispensable, debido a las llamadas a funcin. o Resulta que el ujo de instrucciones va de arriba hacia abajo, y cuando ocurre una llamada a alguna funcin, el estado global del sistema se almacena en un o registro y ste en una pila. As que la pila va a contenr todas las llamadas a e procedimientos que se hagan. 22

Cuando se termina de ejecutar algn procedimiento, se recupera el registro que u est en la cima de la pila. En ese registro estn los valores de las variables como a a estaban antes de la llamada a la funcin, o algunas pueden haber cambiado si o valor, dependiendo del mbito de las variables. a Cada elemento en la pila que es retirado, signica que se ha terminado de ejecutar alguna funcin. Cuando se termina de ejecutar el programa, la pila o de llamadas a subprogramas debe haber quedado en 0 tambin, de otro modo e podr causar algun tipo de error. a Esto nos lleva a pensar en otras utilidades de la pila. La pila sirve para encontrar errores. La dinmica de la pila, es decir, la manera en cmo entran los datos a la a o estructura de datos y cmo salen, se denomina fo, que viene del ings rst o e in rst out (primero en entrar, primero en salir).

Figura 12. Dinmica de la pila P a

En la gura 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 cima G y F para luego insertar H, que quedar posteriormente debajo de F. a Lo que sucede es que, cuando se retira el elemento G se debe hacer una evaluacin para determinar si el elemento retirado es el elemento objetivo, en este o caso el elemento objetivo es F, puesto que se desea insertar un elemento debajo de F. Despus de haber insertado F, insertamos de nuevo los elementos F y G en ese e orden, adems de insertar nalmente el elemento I que queda en la cima de la a pila. Enseguida veremos con ms detalle las operaciones bsicas de las pilas. a a 23

2.2.

Operaciones bsicas a

Las operaciones bsicas de una pila son: a 1. 2. 3. 4. En la pila S, insertar un elemento e: push(S,e), Retirar un elemento de la pila S: pop(S), Vericar si la pila S est vac stackempty(S) y a a: Saber cul es el elemento en la cima de la pila S: stacktop(S). a

enseguida cada una de estas operaciones:

2.2.1.

La operacin push o

Esta operacin sirve para insertar un elemento e en la pila S, lo vamos a o escribir como: push(S,e) Despus de hacer esta operacin sucede que: e o El elemento en la cima de la pila S ahora es e

2.2.2.

La operacin pop o

Para retirar un elemento de la pila S y asignarlo a una variable del mismo tipo que el tipo de los elementos de la pila, usaremos la operacin pop escribindola o e como: v=pop(S); En donde v es una variable que almacena el valor del elemento que estaba en la cima de S. Hacer esta operacin tiene algunas implicaciones: o La variable v debe ser del mismo tipo que los elementos almacenados en la pila. Solamente se puede retirar un elemento de la pila a la vez. Antes de la operacin, e era el elemento en la cima, ahora ya no lo es ms. o a El apuntador cima decrece en una unidad.

2.2.3.

La operacin stackempty o

Esta operacin toma como argumento una estructura del tipo stack (pila) y o devuelve un valor booleano, devuelve un true si la pila est vac y devuelve a a 24

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


true si S tiene 0 elementos f alse si S tiene ms de 0 elementos a

stackempty(S) =

2.2.4.

La operacin stacktop o

La operacin stacktop(S) devuelve el valor del elemento en la cima de la pila o S. Para hacer esta operacin escribiremos: o v=stacktop(S) las implicaciones de usar esta operacin son: o Se hace una copia del elemento que est en la cima a En realidad se hacen dos operaciones, primero se hace v=pop(S), luego un push(S,v), porque despus de la operacin stacktop, la pila S queda sin e o cambio alguno.

2.3.

Ejemplo: Nmero de parntesis u e

Supongamos ahora la expresin ((5+6)*4)/(17+9), una de las condiciones o para que sea una expresin aritmtica correcta en que tengas sus parntesis o e e balanceados, as que deseamos saber si el nmero de parntesis que abres es u e el mismo nmero de parntesis que cierran. u e Para resolver este problema usaremos el concepto de pila. La idea es simple. Vamos a leer cada elemento de la expresin, si se trata de un parntesis que o e abre, entonces lo insertaremos en una pila; si se trata de un parntesis que e cierra, entonces sacamos un elemento de la pila. Al terminar de leer la expresin revisaremos si la pila est vac en cuyo caso habremos conclu que el o a a, do nmero de parntesis que abre es el mismo que el nmero de parntesis que u e u e cierra y la expresin tiene parntesis balanceados. o e Veamos cmo funciona: o ( ( 5 + 6 ) : : : : : : push(S,() push(S,() nada que hacer nada que hacer nada que hacer v=pop(S) 25

* : 4 : ) : / : ( : 17: + : 9 : ) :

nada que hacer nada que hacer v=pop(S) nada que hacer push(S,() nada que hacer nada que hacer nada que hacer v=pop(S)

Empezamos con un contador iniciado en 0, y por cada push aumentamos un contador, y por cada pop decrementamos el contador. Al nal vemos el valor del contador, si el contador=0 entonces terminamos con xito, de otro mod e sealamos el error. n En la gura 13 se muestra la actividad de la pila a medida que se van agregando y quitando elementos.

Figura 13. Evaluacin del balance de parntesis en una expresin aritmtica o e o e

2.4.

La estructura de datos Pila en C/C++

Una pila est conformada por dos elementos: a Un espacio sucientemente grande para almacenar los elementos insertados en la pila Una parte que nos seale cul es el elemento en la cima de la pila. n a 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

fin de la nueva estructura Fcilmente podemos describir un cdigo en C/C++ que represente lo anteriora o 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 representacin en C/C++ de las operaciones de una pila o

En esta seccin veremos una implementacin de las cuatro operaciones bsicas o o a de las pilas. Todas estas operaciones se han hecho desde un punto de vista de programacin funcional, sin duda se pueden describir en un modelo orientado o a objetos.

2.5.1.

La operacin push o

El siguiente segmento de cdigo ilustra cmo se puede implementar la opo o eracin insertar un elemento en una pila. Hemos supuesto que la pila ya o est denida como una estructura stack. a (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 operacin push recibe dos parmetros: la o a direccin de una estructura de tipo pila y un elemento de tipo entero. o La l nea (2) incrementa el tope (cima) de la pila en una unidad, con el n de agregar el elemento en una posicin libre de la pila, lo cual se logra en la l o nea (3), asignando el valor e en la casilla S->top del arreglo item de la pila. 27

2.5.2.

La operacin pop o

La operacin pop se escribe en forma de cdigo en C/C++ con la siguiente o o secuencia de rdenes: o (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 funcin devuelve un tipo entero, el tipo de o elementos guardados en la pila; luego notamos que debemos dar slo la direco cin de alguna variable de tipo estructura de pila (struct stack *). Obtener la o direccin se logra con el operador de indireccin (&). o o Las l neas (4) y (5) hacen todo el trabajo de esta funcin, se almacena el valor o que ser devuelto en una variable de tipo entero y luego se decrementa el tope a de la pila.

2.5.3.

La operacin stackempty o

La operacin stackempty se describe en el siguiente segmento de cdigo: o o (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 funcin que se muestra en la l o nea (1) establece que se devuelve un valor booleano, y que se debe dar un parmetro, que es la a direccin de una localidad de memoria que almacena una estructura de tipo o pila. El objetivo de esta funcin es claro: o 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 un verdadero (4), de otro modo ha de devolver un valor falso (6). Se ha establecido un -1 como vac porque el manejo de arreglos en C/C++ empieza en el o ndice 0, que a diferencia de otros lenguajes como Pascal, empiezan en 1. 28

2.5.4.

La operacin stacktop o

Este es un caso especial porque no se requiere hacer ningn cdigo. u o Esta funcin debe devolver un nmero entero y dejar la pila sin cambio. Para o u lograr esto se debe hacer un pop(&A), mostrar el elemento y luego insertar de nuevo el elemento en la pila haciendo un push(&A,elemento), notemos que se han usado los operadores de direccin para dar la direccin de la variable que o o alberga una estructura de tipo pila. El siguiente segmento de cdigo ilustra o cmo se han usado las funciones antes creadas, por supuesto que se pueden o separar y crear una nueva funcin que haga lo mismo: o ... (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 programacin o

Los siguientes ejercicios deben ser resueltos en un progrma (en C/C++ ): a 1. Expresiones entrejas y prejas. Las expresiones aritmticas pueden e representarse de varias maneras, una de ellas, la ms usual es la notacin a o entreja. La notacin entreja establece que en medio de dos operandos se escribe o un 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 parntesis ms interno establece la maye a or prioridad, de manera que primero se debe evaluar (5 + 7), luego (12/4), luego 2 + (5 3) y malmente (2 + 15), dando como resultado 17. c) 1 No hay nada que hacer, pues es un operador unario. En las expresiones prejas se establece que el orden de escritura debe ser, 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

operador y tomar los operandos necesarios siguientes (dos si se trata de un operador binario y uno si es un operador unario). En este caso se trata de evaluar 2 + [5/ + 574]. Cada uno de los operandos debe ser tratado de nuevo como una expresion en prejo, de manera que se repite lo anterior, tomar el operador y la lista de sus operandos y tratar cada uno de sus operandos como expresiones en prejo: 2 + [5 [/ + 574]], luego 2 + [5 [[+57]/4]] y nalmente 2 + [5 [[5 + 7]/4]] y evaluar. Los parntesis cuadrados son para ilustrar el ejemplo y no e son necesarios para su evaluacin. o c) 1 No hay nada que hacer, pues es un operador unario. Haga un programa en C/C++ que transforme expresiones de entrejo a prejo, y de prejo a entrejo. Los caracteres vlidos son: las letras a maysculas y minsculas, los nmeros enteros, los parntesis normales, u u u e los cuatro operadores (+, , , /, ) y el operador unario ().

Figura 14. Ilustracin del estacionamiento mencionado en el problema 2 o

2. en cierto punto de la ciudad hay un estacionamiento como el que se muestra en la gura 14, en donde hay lugar para 9 veh culos. haga un programa que muestre el manejo de este estacionamiento, considerando los siguientes requisitos: a) Los veh culos proporcionan la siguiente informacin: Placas (6 digio tos), Estado (2-3 caracteres, p.e. SON, DF, CHI, YUC), Marca, Modelo, Ao-Modelo, Nombre del propietario. n 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 algn veh u culo del estacionamiento, regresando los veh culos en el orden en que estaban. e) Toda la corrida del programa debe hacerse hacia/desde la terminal estndar. a 3. Haga un programa que implemente 2 pilas en 1 arreglo A[1..n] de manera que ninguna pila se desborde a menos que el nmero de elementos en u ambas pilas sea n

30

3.

Colas

Denicin 5 Las colas son una estructura de datos similar a las pilas. Recordeo mos que las pilas funcionan en un depsito en donde se insertan y se retiran o elementos por el mismo extremo. En las colas sucede algo diferente, se insertan elementos por un extremo y se retiran elementos por el otro extremo. De hecho a este tipo de dispositivos se les conoce como dispositivos fo (rst in, rst out) porque funcionan como una tuber lo que entra primero por un a, extremo, sale primero por el otro extremo. En una cola hay dos extremos, uno es llamado la parte delantera y el otro extremo se llama la parte trasera de la cola. En una cola, los elementos se retiran por la parte delantera y se agregan por la parte trasera.

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

En la gura 15 se muestra una actividad t pica de la cola, en donde se muestra que se agregan datos por la parte trasera de la cola y se eliminana datos por el frente de la cola. Si Q es una cola y x es un elemento, se pueden hacer tres operaciones bsicas a con 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 parte frontal de la cola Q. 3. empty(Q), que es un predicado de valor booleano, y es verdadero cuando la cola Q tiene 0 elementos, y es f also cuando la cola Q tiene al menos un elemento, en cuyo caso, ese unico elemento es la parte frontal y la parte trasera de la cola al mismo tiempo. 31

Tericamente no hay l o mite para el tamao de la cola, asi que siempre se n deber poder insertar elementos a una cola, sin embargo, al igual que las a pilas, normalmente se deja un espacio de memoria para trabajar con esta estructura. Por el contrario, la operacin remove slamente se puede hacer si o o la cola no est vac a a.

3.1.

Estructura de las colas en C/C++

De manera similar a las pilas, las colas denen una estructura no estndar, de a manera que se debe crear un nuevo tipo de dado, el tipo cola, que debe tener los siguientes elementos: Un arreglo de n elementos de algn tipo espec u co, puede incluso ser un tipo estndar o no. a Un nmero que indica el elemento que est en la posicin del frente de la u a o cola. Un nmero que indica el elemento que est en la posicin trasera de la cola. u a o Suponiendo que los elementos son nmeros enteros, una idea para representar u una cola en C/C++ es usar un arreglo para contener los elementos y emplear otras 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 representacin con arreglos es completamente vlida, pero debemos tener o a cuidado con los l mites del arreglo. Suponiendo que no existiera la posibilidad de caer en un desbordamiento del arreglo, es decir, que se insertaran ms a elementos de lo que el arreglo puede almacenar, la operacin insert podr o a quedar como: void insert(struct cola *C, int e){ C->items[++C->rear]=e; } y al operacin x=remove(Q) o int remove(struct cola *C){ return C->items[C->front++]; 32

} y nalmente la operacin empty(Q): o 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 los datos almacenados de acuerdo a un criterio de prioridad. Hay dos tipos de colas 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 forma arbitraria y solamente se puede remover el elemento con menor prioridad. Si CPA es una cola de prioridad ascendente, la operacin insert(CPA,x) inserta o el elemento x en la cola CPA; y la operacin x=minRemove(CPA) asigna a x el o valor del elemento menor (de su prioridad) y lo remueve de la cola. En las colas de prioridad descendente es similar, pero slo permite la supresin o o del elemento ms grande. Las operaciones aplicables a la cola de prioridad a descendente son insert(CPD,x) y x=maxRemove(CPD), cuando CPD es una cola de prioridad descendente y x es un elemento. La operacin empty(C) se aplica a cualquier tipo de cola y determina si una o cola de prioridad est vac Las operaciones de insertar y borrar se aplican a a. solamente si la pila no est vac a a. Los elementos de la cola de prioridad no necesitan ser nmeros o caracteres u para que puedan compararse directamente. Pueden ser estructuras complejas ordenadas en uno o varios campos. Por ejemplo, las agendas telefnicas constan o de apellidos, nombres, direcciones y nmeros de telfono y estn ordenadas u e a por apellido. A diferencia de las pilas y las colas, en las colas de prioridad se pueden sacar los elementos que no estn en el primer sitio del extremo donde salen los a elementos. Esto es porque el elemento a retirar puede estar en cualquier parte 33

del arreglo. Cuando se requiere eliminar un dato de una cola de prioridad se necesita vericar cada uno de los elementos almacenados para saber cul es el menor a (o el mayor). Esto conlleva algunos problemas, el principal problema es que el tiempo necesario para eliminar un elemento puede crecer tanto como elementos tenga la cola. Para resolver este problema hay varias soluciones: 1. Se coloca una marca de vac en la casilla de un elemento suprimido. o Este enfoque realmente no es muy bueno, porque de cualquier modo se accesan los elementos para saber si es una localidad vac o no lo es. Por a otro lado, cuando se remueven elementos, se van creando lugares vac os y despus es necesario hacer una compactacin, reubicando los elementos e o en el frente de la cola. 2. Cada supresin puede compactar el arreglo, cambiando los elementos o depus del elemento eliminado en una posicin y despus decrementando e o e rear en 1. La insercin no cambia. En promedio, se cambian la mitad de o los elementos de una cola de prioridad para cada supresin, por lo que o esta operacin no es eciente. o

3.3.

Ejercicio de programacin o

1. Modique los procedimientos de insertar, retirar y vericar-cola-vac a para que considere aprovechar los espacios dejados al retirar elementos. 2. Un deque es un conjunto ordenado de elementos del cual pueden eliminarse elementos en cualquier extremo y en el cual pueden insertarse elementos en cualquier extremo. Llamemos a los dos extremos de un deque left (izquierdo) y right (derecho). ?cmo se representa un deque en o un arreglo en C/C++ ? escriba un programa que maneje un deque, y que considere las cuatro rutinas removeLeft removeRight insertLeft insertRight para remover e insertar elementos en los extemos izquierdo y derecho de un deque. Asegrese de que las rutinas funcionan adecuadamente para u que un deque vac y que detectan desbordamiento y subdesbordamiento. o 3. Programe las colas de prioridad ascendente y descendente. 4. Existe un estacionamiento que tiene un slo carril que aloja hasta 10 o 34

carros. Los autos llegan por el extremo sur del estacionamiento y salen por el extremo norte del mismo. Si llega un cliente para recoger un carro que no est en el extremo norte, se sacan todos los automviles de ese lado, a o se retira el auto y los otros coches se restablecen en el mismo orden que estaban. Cada vez que sale un auto, todos los autos del lado sur se mueven hacia adelante para que en todas las ocasiones todos los espacios vac os estn en la parte sur del estacionamiento. Escriba un programa que lea un e grupo de lineas de ingreso. Cada l nea contiene una A para las llegadas y una D para las salidas y un nmero de placa. Se supone que los u carros llegan y salen en el orden especicado en la entrada. El programa debe imprimir (en la terminal estndar) un mensaje cada vez que entra a o sale un auto. Cuando llega un carro, el mensaje debe especicar si hay espacio o no para l en el estacionamiento. Si no hay espacio, el carro e espera hasta que hay espacio o hasta que se lee una l nea de salida para el auto. Cuando queda disponible espacio, debe imprimirse otro mensaje. Cuando salga un coche, el mensaje debe incluir la cantidad de veces que se movi el auto dentro del estacionamiento, incluyendo la salida misma, o pero no la llegada. Este nmero es 0 si el carro sale de la la de espera. u

35

4.

Recursin o

Un tema fundamental para los prximos temas es el de recusrin. La recursin o o o es muy importante tanto en mateticas como em computacin, pues se usa a o recursin para denir procedimientos autosimilares. o Denicin 6 Decimos que un objeto es recursivo si en su denicin se nomo o bra a s mismo. En programacin, una funcin es recursiva si en el mbito de esa funcin hay o o a o una llamada a s misma, C/C++ permite esta clase de acciones. Los algoritmos recursivos dan elegancia a las soluciones de los problemas. Un ejemplo clsico a es el factorial de un nmero. u Una manera de denir el factorial de un nmero n > 1 es: u
n

!n =
i=1

i,

es decir, el producto de todos los nmeros enteros menores o guales que l, lo u e que se puede resolver fcilmente con una funcin iterativa, esto es, una funcin a o o con un ciclo que itere sucientes veces, incrementando un valor y entonces ir almacenando en una variable el resultado de esas multiplicaciones. Una implementacin de esta denicin iterativa es: o o (1) (2) (4) (5) (6) (7) (8) int i,n; long double valorAc; valorAc=1.0; std::cout << "Numero entero:"; std::cin>> n; for(i=1; i<=n; i++) valorAc = valorAc*i; std::cout<<"El factorial de "<<n<<" es:"<<valorAc;

El ciclo principal es en la l nea (7). No hay ningn truco hasta aqu La u . unica observacin importante es en la l o nea (2) en donde se declara el tipo long double para el valor del resultado, la razn para tal accin es que el o o nmero factorial crece muy rpido y an con entradas en el rango de los u a u caracteres (hasta 255), el factorial es muy grande. Este procedimiento computacional no hace uso de tcnicas especiales empleadas para tratar nmeros e u grandes. Sin embargo una solucin ms elegante es usar la denicin recursiva, y esta o a o es: 36

!n = n !(n 1) El programa en C/C++ es el que se muestra a continuacin: o ( 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 sealar, en primer lugar se ha creado una nueva n funcin, a diferencia de la denicin iterativa en donde era suciente trabao o jar en el programa principal. Esta funcin se llama factorial (como era de o suponerse), y empieza su encabezado en la l nea (1). All mismo en la misma l nea (1), es de notar que hemos emplado ahora el tipo double tanto para el tipo devuelto como para el tipo del argumento, a diferencia de la versin iterativa en donde emplebamos tipos diferentes. La o a razn es que al iniciar la recursin el argumento es del tipo devuelto, asi que o o deben 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 la llamada, entonces, cuando se termina la funcin se recupera una entrada de la o pila. En la gura 16 ilustra cmo funciona la recursividad cuando se intenta o obtener el factorial(5).

Figura 16. Recursividad cuando se ejecuta factorial(5)

37

4.0.1.

La serie Fibonacci

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

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

Un poco de observacin es sufucuente para encontrar que cualquier nmero o u (a partir del tercero de la serie, osea el segundo 1) es igual a la suma de los dos nmeros anteriores. u Daremos en primer lugar la versin iterativa. En este algoritmo deseamos o encontrar el n-simo nmero de la serie Fibonacci. As si n = 4 el resultado e u del algoritmo debe ser 3; si n = 6 el resultado debe ser 8. La versin iterativa o empieza desde los primeros 1s, sumndolos y encontrando el tercero, luego a para encontrar el cuarto nmero se suman el tercero (recin encontrado) y el u e segundo, y as en adelante hasta encontrar el nmero buscado. u #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 denicin recursiva para encontrar todos los n primeros nmeros de la serie o u Fibonacci es: 38

fib(n) =

Si n = 1 n = 2 o

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

En el siguiente cdigo, la solucin que propone la recursividad resulta en una o o programacin elegante, aunque costosa. El cdigo que hace esto es: o o ( 1) ( 2) ( 3) ( 4) ( 5) ( 6) ( 7) ( 8) ( 9) (10) (11) (12) (13) (14) (15) (16) #include <iostream> //==================== int fib(int val){ if ((val==1)||(val==2)) return 1; else return (fib(val-1)+fib(val-2)); } //==================== int main (int argc, char * const argv[]) { int n; std::cout<<"Numero entero:"; std::cin>>n; std::cout<<"\nEl "<< n <<"-esimo numero fibonacci es: "<< fib(n); return 0; }

Como regla general, cualquier algoritmo recursivo se puede reescribir en un algoritmo iterativo. La ventaja de tener un algoritmo iterativo es que no se usa una pila para guardar llamadas a la misma funcin de manera recursiva, o esto es una ventaja porque el espacio de memoria destinado al uso de la pila es generalmente limitado, de manera que cuando se hacen demasiadas funciones push seguramente llegar el momento en que la pila se desborde, que por a cierto es un trmino usado en computacin para decir que ya no hay ms e o a espacio disponible en la pila.

4.1.

Peligros en la recursividad

El principal peligro al usar recursividad, es no tener una manera de salir del paso recursivo, esto es peligroso porque se hacen llamadas a la misma funcin, o lo que signica una entrada en la pila donde se almacenan los estados generales del programa. Para decidir hacer un programa recursivo se deben de tener al menos dos cosas muy claras: 39

1. El paso base: Esta es la clave para terminar la recursin, es cuando deja o de hacer llamadas a la funcin recursiva y hace evaluaciones devolviendo o los resultados. En el ejemplo de la serie de Fibonacci, el paso base est en a la l nea ( 5). Adems se debe asegurar de que es posible entrar a este paso. a 2. El paso recursivo: Es la parte de la denicin que hace llamadas a o esa misma funcin y que es la causante de las inserciones en la pila, o almacenando en cada una de las llamadas, informacin del programa, del o estado de sus variables locales y globales. En el mismo ejemplo de la serie Fibonacci, el paso recursivo se muestra en la l nea ( 7). Otras cosas que se deben tener claras son por ejemplo si se pasa una variable como referencia o por valor, si las variables apuntadores son del tipo adecuado etc. Frecuentemente tanto el paso base como el paso recursivo, se encuentran en una sentencia condicional if, pero porsupuesto que es posible usar cualquier otra sentencia de control, dependiendo de las necesidades particulares del problema. El siguiente ejemplo ilustra este problema ( 1) ( 2) ( 3) ( 4) ( 5) ( 6) ( 7) ( 8) ( 9) (10) (11) (12) #include <iostream> int malaFuncion( int n ){ std::cout << "malaFuncion es una recursion infinita. n="<<n; if( n == 0 ) return 0; else return malaFuncion( n / 3 + 1 ) + n - 1; } int main (int argc, char * const argv[]) { std::cout << malaFuncion(10); return 0; }

4.2.

Ejercicios de programacin o

Los siguientes ejercicios deben de ser programados en C/C++ : 1. B squeda binaria: Considere un arreglo de elementos (nmeros enteros u u est bien) en el cual los objetos ya estan ordenados, y se desea encona trar un elemento dentro de este arreglo. Es decir, se desea realizar una bsqueda. u La idea general de este mtodo de bsqueda binaria es: e u Si el arreglo tiene 1 elemento, se compara con el numero requerido y la 40

bsqueda termina. u Si el arreglo tiene ms de 1 elemento, tendremos que dividir en dos el a arreglo y decidir en qu parte del arreglo buscar; luego buscarlo usando e busqueda binaria 2. Escriba un programa para calcular la cantidad de maneras diferentes en las cuales un entero n se puede expresar como la suma de dos enteros menores p < n y q < n tales que p + q = n

41

5.

Listas

Hay dos desventajas serias con respecto a las estructuras estticas de pilas y a colas usando arreglos. Estas desventajas son que tienen un espacio limitado de memoria y la otra desventaja es que es posible no ocupar toda la memoria disponible, haciendo que se desperdicie espacio. Una solucin es usar listas. Las listas son estructuras de datos que son dinmio a cas, esto signica que adquieren espacio y liberan espacio a medida que se necesita. sin embargo, hay una advertencia. Como regla general siempre hay que tener cuidado al manejar direcciones de espacios de memoria, porque es posible que accedamos a una localidad de memoria de la cual no deseabamos cambiar su contenido. Antes de estudiar las listas, daremos una breve introduccin a los grafos, pues o las listas son un caso especial de los grafos.

5.1.

Grafos

Los grafos son una manera visual de representar las relaciones. Denicin 7 Si A y B son dos conjuntos, decimos que a A est relacionado o a con 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 para ese par de elementos, lo escribimos como aRb, y si el predicado es falso, lo escribimos como b . 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 el curso, entonces pedroRlogica se lee pedro toma el curso de logica. En la gura 17 se puede apreciar esto en forma de diagramas de Venn. Si A es el conjunto de personas y B es tambin el conjunto de personas, e y R es debe dinero a; marisolRrafaelle signica que marisol debe dinero a rafaelle y de ningn modo es al contrario, es decir rafaelle no u debe dinero a marisol. Los elementos de la gura 17 denen un nuevo conjunto de elementos, el conjunto de pares de elementos que estan relacionados. As la relacin toma o el curso de es el siguiente: 42

Figura 17. Relacin toma el curso de para los conjuntos A de personas y B de o materias.

R = {(diana, programacion), (carolina, programacion), (carolina, compiladores), (rafael, compiladores), (fabiola, lenguajes)} Grcamente podemos ilustrar el conjunto R de toma el curso de con un a grafo como el que se muestra en la gura 18. (carolina, lenguajes), (gustavo, lenguajes),

Figura 18. Grafo que ilustra la relacin toma el curso de. o

De manera que podemos denir un grafo como una representacin grca de o a una relacin. o Denicin 8 Para denir formalmente un grafo debemos establecer la siguo iente tupla: G = A, N Donde A es un conjunto de aristas y N = un conjunto no vac de nodos. o En el caso de R, el conjunto A B es el conjunto de nodos y el conjunto de echas es el conjunto de aristas. 43

Notemos que el conjunto A de aristas puede ser un conjunto vac pero de o, ningn modo hay grafo sin nodos, es decir el conjunto N debe ser diferente u que el conjunto vac o. Supongamos ahora A = {1, 2, 3, 4, 5, 6} y la siguiente relacin en A: o R = {(1, 2), (2, 3), (3, 4), (4, 5), (5, 6)} Esta relacin luce como aparece en la gura 20. o

Figura 19. Relacin R de A en A o

y en forma de grafo es:

Figura 20. Grafo de la relacin R : A A o

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

5.2.

Listas simplemente encadenadas

Como vimos en la seccin anterior, una lista es una relacin de elementos, tales o o que cada elemento est relacionado con unicamente un elemento del conjunto, a diferente a s mismo. 44

Como cada elemento puede tener a lo ms una arista dirigida que sale y una a arista dirigida que entra, bien puede tener 0 aristas que salen, o cero aristas que entran. Si el nodo tiene 0 aristas que salen, entonces es el nal de la lista. Si el nodo tiene 0 aristas que entran, entonces es el inicio de la lista. Por razones prcticas, se dibujan una echa que sale de un identicador de la a lista y entra al inicio de la lista y otra echa que sale del nal de la lista y apunta a un s mbolo que se llama NULO.

Figura 21. Grafo de la relacin R : A A con apuntadores del nombre de la lista o listaLigada y hacia NULL

En C/C++ el identicador de la lista contiene la direccin del primer elemento o de la lista, as como sucede con los arreglos. El valor NULO es util para saber cundo termina la lista, es una constante estndar y no tiene valor. a a El contenido de los nodos, como ya hemos visto, son los elementos de un conjunto. Si ese conjunto tiene elementos estructurados, tambin es vlido e a usarlos. Normalmente cada nodo de la lista est estructurado con dos partes: a 1. La parte de informacin. o 2. La parte de direccin al siguiente nodo de la lista. o El campo de informacin contiene el elemento real de la lista. El campo de o direccin al siguiente contiene un apuntador al elemento con el cul est relao a a cionado, es decir, al elemento siguiente de la lista. La lista completa se accesa mediante el identicador de lalista. El campo de la direccin del ultimo nodo o apunta a una direccion nula. La lista que no tiene elementos (solamente tiene un identicador que apunta a nulo) se llama lista nula o lista vac Una lista se inicializa a una lista vac a. a haciendo lista=null, recordemos que lista es un apuntador a una direccin o de memoria que puede albergar una variable del tipo que se hayan denido los nodos; null es una direccin de cualquier tipo, as que el compilador asigna o la direccin null a lista. o 45

Enseguida vamos a dar una lista de trminos usados para manejar los elemene 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 la direccin de una variable del tipo declarado para los nodos de una lista: o node(p): hace referencia al noso al que se apunta mediante p. info(p): hace referencia a la informacin del nodo al que apunta p. o next(p): hace referencia a la parte direccin siguiente y, por tanto, es un o apuntador. As que la expresin info(next(p)) signica que se hace referencia a la seccin o o de informacin del nodo siguiente al que apunta p. o

5.2.1.

Insertar y eliminar nodos de una lista

En el uso de las listas ligadas se ven involucradas varias operaciones, entre ellas la de insertar un nuevo nodo a la lista y la operacin de eliminar un o nodo de la lista. En ambos casos debemos recordar que se trata de manejo de la memoria, as que insertar un nodo en la lista signica obtener un espacio de memoria disponible y relacionarlo con los elementos de la lista; as mismo, eliminar un nodo de la lista signica liberar la memoria que ocupa ese nodo sin perder la relacin con el resto de los nodos de la lista. o Insertar un elemento al inicio de la lista. La operacin p=getnode(); o obtiene un nodo vac y establece el contenido de una variable nombrada p en o la direccin de este nodo, como se muestra en la gura 22.a. Este nodo an o u no pertenece a alguna lista, simplemente se ha logrado dedicar un especio de memoria que es apuntado por p, gura 22.b.

Figura 22. a) Creacin de un nuevo nodo. b) El nuevo nodo debe de ir insertado al o frente, atrs o en medio de la lista. a

Una vez que se ha creado un nuevo espacio para el nuevo nodo, se debe de establecer la parte de informacin de ese nodo con la operacin info(p), como o o se ilustra en el siguiente ejemplo con el dato 6. 46

info(p)=6; Despus de esstablecer la parte de informacin es necesario establecer la parte e o siguiente de este nodo. Debido a que node(p) va a insertarse en la parte delantera de la lista, el nodo que sigue debe ser el primer nodo actual de la lista. Debido a que la variable lista (el identicador de la lista) contiene la direccin de ese primer nodo, node(p) se agrega a la lista ejecutando la o operacin o next(p)=lista; Esta operacin coloca el valor de lista (la direccin del primer nodo en la o o lista) en el campo siguiente de node(p). Estos pasos se ilustran en la gura 23

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

Hasta ahora, p apunta a la lista con el elemento adicional incluido. Sin embargo, debido a que list es el apuntador externo a la lista deseada, su valor debe modicarse en la direccin del nuevo primer nodo de la lista. Esto se o hace ejecutando la operacin o list=p; En resumen, ya tenemos un algoritmo para insertar un elemento al inicio de una lista simplemente ligada, al reunir todos los pasos tenemos: p=getnode(); info(p)=6; 47

next(p)=list; list=p; Eliminar un elemento de la lista. Para eliminar un elemento del inicio de 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 la gura 24

Figura 24. Operaciones involucradas en la eliminacin de un nodo al inicio de una o lista: 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 implementacin de listas usando arreglos, cada o elemento del arreglo debe ser un elemento compuesto. Cada elemento debe contener una parte para la informacin y otra parte para apuntar al elemento o siguiente: #include <iostream> ( 1) #define numNodes 500 ( 2) struct nodeType{ ( 3) int info; 48

( 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 informacin y su parte o de apuntador al siguiente. Como solamente tenemos 500 nodos (declarados en la l nea (1), el tipo de siguiente es entero y hemos decidido almacenar nmeros u enteros solamente. En la l nea (6) se ha declarado una variable global de tipo arreglo de estructura de nodos, es decir, se ha creado un arreglo de 500 nodos. En este esquema, el ultimo nodo apunta a NULL, que se representa con el valor entero -1. Tenemos tambin los siguientes elementos de cada nodo: e node[p] corresponde a next(p), por la notacin propia del lenguaje; tambin o e node[p].info para info(p) y nalmente node[p].next hace referencia al nodo siguiente next(p). Al principio todos los nodos estn sin usar, porque solamente se ha creado a el 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 lista disponible, 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 lista disponible. 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

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 signica que no hay nodos disponibles, es decir, que el arreglo est completamente lleno. Esto signica que las estructuras de lista a de un programa particular han desbordado el espacio disponible. La funcin o freeNode acepta un apuntador (nmero entero) a un nodo y devuelve ese u nodo 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 algoritmos correspondientes. La rutina insAfter acepta un apuntador p a un nodo y un elemento x como parmetros. Primero se asegura que p no sea nulo a y despus se inserta x en el nodo siguiente al indicado por p. e 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), suprime el nodo despus de node(p) y almacena su contenido en x; e void delAfter(int p, int *px){ int q; if((p==-1)||(node[p].next==-1)){ 50

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].next sean nulos.

5.3.

El uso de memoria dinmica en C/C++ a

Como sabemos, en lenguaje C/C++ , &x es la direccin donde se almacena en o memoria la variable x. Si p es un apuntador en C/C++ , *p es el contenido de la localidad de memoria p. Si usamos C/C++ para implementar listas ligadas, podemos usar estos apuntadores. Sin embargo, primero analizaremos cmo o asignar y liberar el almacenamiento en forma dinmica y cmo se accesa al a o almacenamiento dinmico en C/C++ . a En C/C++ , una variable que debe contener la direccin en la memoria que o almacena un nmero entero se crea mediante la declaracin u o int *p; Recordemos que esta declaracin se divide en dos partes: la parte de tipo o int *, que indica que se trata de un apuntador a un entero; y la parte de identicador, en este caso p. Una vez declarada la variable p como un apuntador a un tipo espec co de dato, debe ser posible crear dinmicamente un objeto de este tipo espec a co y asignar su direccin a p. o Esto se hace en C/C++ mediante la funcin de la biblioteca estndar malloc(size). o a La fucnin malloc asigna de manera dinmica una parte de memoria de o a tamao especicado en size y devuelve un apuntador a un elemento de tipo n char. Consideremos las siguientes declaraciones extern char *malloc(); int *pi; float *pr; 51

La palabra clave extern especica que una variable o funcin tiene un eno lace externo. Esto signica que la variable o funcin a la que nos referimos o est denida en algn otro archivo fuente, o ms adelante en el mismo archia u a vo. Sin embargo, en C/C++ podemos usar esta palabra clave extern con una cadena. La cadena indica que se est usando el convenio de enlace de otro a lenguaje para los identicadores que se estn deniendo. Para los programas a C++ 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 denominan variables dinmicas. Al ejecutar estos enunciados, el operador sizeof a devuelve el tamao en bytes de su operando. Esto se usa para conservar la n independencia de mquina. Despus, malloc crea un objeto de este tamao. a e n Por tanto, malloc(sizeof(int)) asigna almacenamiento para un entero, en tanto que malloc(sizeof(float)) asigna espacio necesario para un real. De igual 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 seale a un entero, usamos el operador de n clculo (int *) (float *). a o El operador sizeof, devuelve un valor de tipo int, en tanto que la funcin o malloc espera un parmetro de tipo unsigned. Para hacer que correspondan, a debemos escribir pi=(int *)malloc((unsigned)(sizeof(int))); Como ejemplo, vamos a considerar este breve cdigo: o #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

(12) }

std::cout<< *p << " return 0;

" << *q << "\n";

En la linea (3), se crea una variable de tipo entero y su direccin se coloca o en p. La l nea (4) establece el valor de esa variable en 3. La l nea (5) hace que la direccin q sea la misma direccin que p. El enunciado de la l o o nea (5) es perfectamente vlido, pues se asigna a una variable de tipo apuntador (q) a el valor de otra variable del mismo tipo (p). En este momento *p y *q hacen referencia a la misma variable. Por tanto, la l nea (6) imprime el contenido de esa 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 a la misma variable, *p y *q tienen el valor 7. Por tanto la l nea (9) imprime el nmero 7 dos veces. u La l nea (10) crea una nueva variable entera y coloca su direccin en p. Ahora o *p hace referencia a la variable entera recin creada que todav no ha recibido e a un valor. q no ha cambiado; por lo que el valor de *q sigue siendo 7. Observemos que *p no hace referencia a una variable espec ca unica. Su valor cambia conforme se modica el valor de p. La l nea (11) establece el valor de esta variable recin creada en 5 y la l e nea 12 imprime los valores 5 y 7. Y as la salida del programa es: 3 7 5 3 7 7

mallocEjemplo has exited with status 0. La funcin free se usa en C para liberar almacenamiento de una variable o asignada dinmicamente. La orden a free(p); invalida cualquier referencia futura a la variable *p (a menos que se asigne nuevo espacio de memoraia a esa variable). Llamar free(p) hace que quede disponible para reso el almacenamiento ocupado por *p, si es necesario. u La funcin free espera un parmetro apuntador del tipo char *, para que no o a tengamos problemas de tipos, debemos hacer free((char *)p); Consideremos el siguiente ejemplo para ilustrar el uso de free: 53

#include <iostream> int main (int argc, char * const argv[]) { int *p, *q; ( ( ( ( ( ( ( ( ( } Qu se imprime a la salida del programa? e Observemos que si se llama malloc dos veces sucesivas y se asigna su valor a la 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 direccin no se guard. o o 1) 2) 3) 4) 5) 6) 7) 8) 9) p=(int *)malloc(sizeof(int)); *p=5; q=(int *)malloc(sizeof(int)); *q=8; free(p); p=q; q=(int *)malloc(sizeof(int)); *q=6; std::cout<<*p<<" "<<*q<<"\n"; return 0;

5.4.

Listas ligadas usando memoria dinmica a

Para hacer las listas ligadas necesitamos un conjunto de nodos, cada uno de los cuales tiene dos campos: uno de informacin y un apuntador al siguiente o nodo de la lista. Adems, un apuntador externo seala el primer nodo de la a n lista. Usamos variables de apuntador para implementar apuntadores de listas. As que denimos el tipo de un apuntador y un nodo mediante struct node{ int info; struct node *next; }; typedef struct node *nodePtr; 54

Un nodo de este tipo es igual a los nodos de la implementacin con arreglos, o excepto que el campo next es un apuntador y no un entero. En lugar de declarar un arreglo, para que represente un conjunto acumulado de nodos, stos se asignan y liberan segn es necesario. Se elimina la necesidad e u de un conjunto de nodos previamente declarado. Si declaramos nodePtr p; la ejecucin de la orden o p=getNode(); debe colocar la direccin de un nodo disponible en p: o 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 implementacin dinmica de o a una lista ligada. Supongamos que list es una variable apuntador que seala n al primer nodo de una lista (si lo hay) y es igual a NULL en el caso de una lista 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

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 programacin o

1. Implemente una pila usando memoria dinmica en listas ligadas. Implea mente las operaciones push, pop, empty y stackTop. 2. Implemente una cola usando memoria dinmica en listas ligadas. Implea mente las operaciones empty, insert y remove. 3. Desarrolle un programa para buscar un elemento en la lista (de nmeros u enteros) y borrar la primera ocurrencia de ese elemento. 4. Desarrolle un programa para buscar un elemento en la lista (de nmeros u enteros) y borrar todas las ocurrencias de ese elemento. 5. Las listas doblemente ligadas tienen nodos que estn divididos en tres a segmentos: a) Anterior: Un apuntador a un nodo b) Info: La informacin de un nodo o c) Siguiente: Un apuntador a un nodo Implemente las operaciones borrarNodo(p), insertarNodoAntes e insertarNodoDespues.

56

6.

Arboles

Los rboles son estructuras de datos utiles en muchas aplicaciones. Hay varias a formas de rboles y cada una de ellas es prctica en situaciones especiales, en a a este cap tulo vamos a denir algunas de esas formas y sus aplicaciones.

6.1.

Concepto general de rbol a

Desde el punto de vista de estructuras de datos, un rbol es un concepto a simple en su denicin, sin embargo es muy ingenioso. Un rbol es un grafo o a con caracter sticas muy especiales: Denicin 9 Un rbol es un grafo A que tiene un unico nodo llamado ra o a z que: Tiene 0 relaciones, en cuyo caso se llama nodo hoja tiene un nmero nito de relaciones, en cuyo caso, cada una de esas relau ciones es un subrbol a Para empezar a estudiar los rboles, nos concentraremos en primer lugar en a el caso en que el nodo ra tenga 0, 1 2 subrboles. z o a

6.2.

Arboles binarios

Denicin 10 Un rbol binario es una estructura de datos de tipo rbol en o a a donde cada uno de los nodos del rbol puede tener 0, 1, 2 subrboles llamados a o a de acuerdo a su caso como: Si el nodo raz tiene 0 relaciones se llama hoja. Si el nodo raz tiene 1 relacin a la izquierda, el segundo elemento de la o relacin es el subrbol izquierdo. o a Si el nodo raz tiene 1 relacin a la derecha, el segundo elemento de la o relacin es el subrbol derecho. o a La gura 25 muestra algunas conguraciones de grafos que s son rboles a binarios, y la gura 26 muestra algnas conguraciones de grafos que no son a rboles binarios. Vamos a dar una lista de terminos que se usan frecuentemente cuando se e trabaja con rboles: a 57

Figura 25. Grafos que son estructuras tipo rbol binario a

Figura 26. Grafos que no son rboles binarios a

Si A es la ra de un rbol y B es la ra de su subrbol izquierdo (o derez a z a cho), se dice que A es el padre de B y se dice que B es el hijo izquierdo (o derecho) 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 descendiente del nodo a), si a es el padre de b o el padre de algn ancestro de b. u Un nodo b es un descendiente izquierdo del nodo a, si b es el hijo izquierdo de a o un descendiente del hijo izquierdo de a. Un descendiente derecho se dene de la misma forma. Dos nodos son hermanos si son hijos izquierdo y derecho del mismo padre. Otros trminos relacionados con rboles, tienen que ver con su funcinoamiento e a y topolog a: Si cada nodo que NO es una hoja tiene un subrbol izquierdo y un subrbol a a derecho, entonces se trata de un rbol binario completo. a El nivel de un nodo es el nmero de aristas que se deben recorrer para u 58

llegar desde ese nodo al nodo ra De manera que el nivel del nodo ra es z. z 0, y el nivel de cualquier otro nodo es el nivel del padre ms uno. a La profundidad de un nodo es el mximo nivel de cualquier hoja en el a a rbol. Si un arbol binario tiene m nodos en el nivel l, el mximo nmero de nodos a u en el nivel l + 1 es 2m. Dado que un rbol binario slo tiene un nodo en el a o nivel 0, puede contener un mximo de 2l nodos en el nivel l. Un rbol binario a a completo de profundidad d es el rbol que contiene exactamente 2l nodos en a cada nivel l entre 0 y d. La cantidad total de nodos tn en un rbol binario a completo de profundidad d, es igual a la suma de nodos en cada nivel entre 0 y d, por tanto:

tn = 20 + 21 + 22 + + 2d =
j=0

2j

Usando induccin matemtica se puede demostrar que d 2j = 2d+1 1. o a j=0 Dado que todas las hojas en este rbol estn en el nivel d, el rbol contiene a a a 2d hojas y, por tanto, 2d 1 nodos que no son hojas. Si conocemos el nmero total de nodos tn en un rbol binario completo, podeu a mos calcular su profundidad d, a partir de la expresin tn = 2d+1 1. As sabeo mos que la profundidad d es igual a 1 menos que el nmero de veces que 2 u debe ser multiplicado por s mismo para llegar a tn + 1. Es decir, que en un a rbol binario completo,

d = log2 (tn + 1) Denicin 11 Un rbol binario es un rbol binario casi completo si: o a a 1. 2. Cualquier nodo nd a un nivel menor que d 1 tiene 2 hijos Para cualquier nodo nd en el rbol con un descendiente derecho en el a nivel d debe tener un hijo izquierdo y cada descendiente izquierdo de nd: es una hoja en el nivel d o tiene dos hijos

Los nodos en un rbol binario (completo, casi completo o incompleto) se a pueden enumerar del siguiente modo. Al nodo ra le corresponde el nmero z u 1, al hijo izquierdo le corresponde el doble del nmero asignado al padre y al u hijo derecho le corresponde el doble ms 1 del nmero asignado al padre. a u 59

Figura 27. Comparacin de un rbol binario y un rbol binario casi completo. El o a a a rbol mostrado en (A) descumple la regla 2 de los rboles binarios casi completos. a

6.2.1.

Operaciones con rboles binarios a

Con los rboles binarios es posible denir algunas operaciones primitivas, estas a operaciones son en el sentido de saber la informacin de un nodo y sirven para o desplazarse en el rbol, hacia arriba o hacia abajo. a 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 lgicas, tienen que ver con la identidad de cada o nodo: isLeft(p) devuelve el valor true si el nodo actual es el hijo izquierdo del nodo apuntado por p, y false en caso contrario. isRight(p) devuelve el valor true si el nodo actual es el hijo derecho del nodo apuntado por p, y false en caso contrario. isBrother(p) devuelve el valor true si el nodo actual es el hermano del nodo apuntado 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

if (left(q)==p) return(true); return(false); En la construccin de un rbol binario son utiles las operaciones makeTree, o a setLeft y setRight. La operacin makeTree(x) crea un nuevo rbol binario o a que consta de un unico nodo con un campo de informacin x y devuelve un o apuntador a ese nodos. La operacin setLeft(p,x) acepta un apuntador p o a un nodo de rbol binario sin hijo izquierdo. Crea un nuevo hijo izquierdo a de node(p) con el campo de informacin x. La operacin setRight(p,x) es o o similar, excepto que crea un hijo derecho.

6.2.2.

Aplicaciones de rboles binarios a

Un rbol binario es una estructura de datos util cuando se trata de hacer a modelos de procesos en donde se requiere tomar decisiones en uno de dos sentidos en cada parte del proceso. Por ejemplo, supongamos que tenemos un arreglo en donde queremos encontrar todos los duplicados. Esta situacin es o bastante util en el manejo de las bases de datos, para evitar un problema que se llama redundancia. Una manera de encontrar los elementos duplicados en un arreglo es recorrer todo el arreglo y comparar con cada uno de los elementos del arreglo. Esto implica que si el arreglo tiene n elementos, se deben hacer n comparaciones, claro, no es mucho problema si n es un nmero pequeo, pero el problema se u n va complicando ms a medida que n aumenta. a Si usamos un rbol binario, el nmero de comparaciones se reduce bastante, a u veamos cmo. o El primer nmero del arreglo se coloca en la ra del rbol (como en este u z a ejemplo siempre vamos a trabajar con rboles binarios, simplemente diremos a a rbol, para referirnos a un rbol binario) con sus subrboles izquierdo y derea a cho vac Luego, cada elemento del arreglo se compara son la informacin os. o del nodo ra y se crean los nuevos hijos con el siguiente criterio: z Si el elemento del arreglo es igual que la informacin del nodo ra entonces o z, noticar duplicidad. Si el elemento del arreglo es menor que la informacin del nodo ra entonces o z, se crea un hijo izquierdo. Si el elemento del arreglo es mayor que la informacin del nodo ra entonces o z, se crea un hijo derecho. Una vez que ya est creado el rbol, se pueden buscar los elementos repetidos. a a Si x el elemento buscado, se debe recorrer el rbol del siguiente modo: a 61

Sea k la informacin del nodo actual p. Si x > k entonces cambiar el nodo o actual a right(p), en caso contrario, en caso de que x = k informar una ocurrencia 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 nmeros duplicados u

Para saber el contenido de todos los nodos en un rbol es necesario recorrer a el rbol. Esto es debido a que solo tenemos conocimiento del contenido de a la direccin de un nodo a la vez. Al recorrer el rbol es necesario tener la o a direccin de cada nodo, no necesariamente todos al mismo tiempo, de hecho o normalmente se tiene la direccin de uno o dos nodos a la vez; de manera que o cuando se tiene la direccin de un nodo, se dice que se visita ese nodo. o 62

Aunque hay un orden preestablecido (la enumeracin de los nodos) no siempre o es bueno recorrer el rbol en ese orden, porque el manejo de los apuntadores a se vuelve ms complejo. En su lugar se han adoptado tres criterios princia pales para recorrer un rbol binario, sin que de omita cualquier otro criterio a diferente. Los tres criterios principales para recorrer un rbol binario y visitar todos sus a nodos son, recorrer el rbol en: a preorden: Se ejecutan las operaciones: 1. Visitar la ra z 2. recorrer el subrbol izquierdo en preorden a 3. recorrer el subrbol derecho en preorden a entreorden: Se ejecutan las operaciones: 1. recorrer el subrbol izquierdo en entreorden a 2. Visitar la ra z 3. recorrer el subrbol derecho en entreorden a postorden: Se ejecutan las operaciones: 1. recorrer el subrbol izquierdo en postorden a 2. recorrer el subrbol derecho en postorden a 3. Visitar la ra z Al considerar el rbol binario que se muestra en la gura 28 usando cada uno a de los tres criterios para recorrer el rbol se tienen las siguientes secuencias de a nodos: 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 aplicacin, el ordenamiento de los elementos o de un arreglo. Para ordenar los elementos de un arreglo en sentido ascendente, se debe construir un rbol similar al rbol binario de bsqueda, pero sin omitir las coina a u cidencias. El arreglo usado para crear el rbol binario de bsqueda fue a u <14,15,4,9,7,18,3,5,16,4,20,17,9,14,5> El rbol de ordenamiento es el que se muestra en la gura 29 a Para ordenar los elementos de este arreglo basta recorrer el rbol en forma de a entreorden. 63

Figura 29. Arbol binario para ordenar una secuencia de nmeros u

Cul ser el algoritmo para ordenarlo de manera descendente? a a

6.3.

Representacin en C/C++ de los rboles binarios o a

Vamos a estudiar estas representaciones por partes, primero los nodos y el a rbol; despus las operaciones para el manejo del rbol. e a

6.3.1.

Representacin de los nodos o

Los nodos de los rboles binarios son estructuras en C/C++ que estan coma puestas por tres partes: Un apuntador al subrbol izquierdo, left a Un apuntador al subrbol derecho, right a Una parte de informacin, que puede ser una estructura en s misma, info. o Adicionalmente es muy util poner un apuntador al padre del nodo. father. Usando una implementacin de arreglos tenemos: o #define numNodes 500 struct nodeType{ int info; int left; int right; int father; }; struct nodeType node[numNodes]; y usando una representacin con memoria dinmica, los nodos de un rbol se o a a puede representar tambien con una estructura en C/C++ : struct nodeType{ 64

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 an mediante referencias a p->info, p->left, p->right y p->father respectivamente. Las rutinas getnode y freenode simplemente asignan y liberan nodos usando 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 hijo izquierdo 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, los nodos que no son hojas se llaman nodos internos y los nodos que s son hojas se llaman nodos externos.

6.3.2.

Recorridos de rbol binario en C/C++ a

Aqu usaremos recursividad para hacer estas rutidas de los recorridos de a rboles binarios. Las rutinas se llaman preTr, inTr y postTr, que imprimen el contenido de los nodos de un rbol binario en orden previo, en orden a 65

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 rboles binarios que son aquellos rboles que sus a a nodos solamente pueden tener un mximo de dos hijos. Cuando ocurre que a los nodos tienen cualquier nmero nito de hijos, son rboles (en genreal). De u a manera que Denicin 12 Un rbol es un conjunto nito no vac de elementos en el o a o cual un elemento se denomina la ra y los restantes se dividen en m 0 z subconjuntos disjuntos, cada uno de los cuales es por s mismo un rbol. Cada a elemento en un rbol se denomina un nodo del rbol a a 66

Un nodo sin subrboles es una hoja. Usamos los trminos padre, hijo, hera e mano, antecesor, descendiente, nivel y profundidad del mismo modo que en los rboles binarios. El grado de un nodo es en nmero mximo de a u a hijos que aln nodo tiene. u Un rbol ordenado de dene como un rbol en el que los subrboles de cada a a a nodo forman un conjunto ordenado. En un rbol ordenado, podemos hablar a del primero, segundo o ultimo hijo de un nodo en particular. El primer hijo de un nodo en un rbol ordenado se denomina con frecuencia el hijo ms viejo a a de este nodo y el ultimo se denomina el hijo ms joven. Vase la gura 30. a e Un bosque es un conjunto ordenado de rboles ordenados. a

Figura 30. El rbol de la izquierda es ordenado y el rbol de la derecha es un rbol a a a no ordenado.

6.4.1.

Representacin dinmica en C de los rboles o a a

Al igual que en los rboles binarios, los nodos en un rbol tienen una parte a a de informacin, un apuntador al padre y uno o ms apuntadores a los hijos. o a De manera que una solucin es crear una estructura que incluya una lista o dinmica de apuntadores, como lo muestra la gura 31. a

Figura 31. Representacin con listas de los nodos de un rbol o a

struct treeNode{ 67

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 campo father. 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 ms a joven, en lugar de dejarlo en null. Se podr usar un campo lgico adicional a o para 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 rbol a binario y que next corresponde a su apuntador right, este mtodo representa e en realidad un rbol ordenado general mediante un rbol binario. a a

6.4.2.

Recorridos de rbol a

Los mtodos de recorrido para rboles binarios inducen mtodos para recorrer e a e los rboles en general. Si un rbol se representa como un conjunto de nodos a a de variables dinmicas con apuntadores son y next, una rutina en C/C++ para a imprimir el contenido de sus nodos se escribir como: a void inTr(nodePtr tree){ if (tree != NULL){ inTr(tree->left); std::cout<<tree->info; inTr(tree->right); } } Las rutinas para recorrer el rbol en los dems ordenes son similares. Estos a a recorridos tambin se deninen directamente as e : Orden previo: similar al caso binario. 1. Visitar la ra z 2. Recorrer en orden previo los subrboles de izquierda a derecha a Las demas rutinas son similares. Un bosque puede ser representado medianto un rbol binario. a 68

Para hacer esta representacin, la ra de cada rbol se coloca en una lista o z a de apuntadores; luego para cada nodo en la lista (la ra de cada rbol) se z a procede del siguiente modo: 1. Se crea una lista de subrboles izquierdos con los apuntadores a cada uno a de los rboles en el bosque. a 2. si un nodo tiene ms de un hijo, entonces se crea un subrbol izquierdo a a y se forma una lista de subrboles izquierdos con todos los hijos de ese a nodo.

Figura 32. Arriba: Un bosque de rboles. Abajo: El rbol binario que corresponde a a a ese bosque.

Para recorrer los nodos de un bosque, es preferible convertir todo el bosque en un rbol binario correspondiente, como se ilustra en la gura 32. Cuando a ya se tiene el rbol binario que corresponde a ese bosque, entonces se aplican a las rutinas ya conocidas. Si el bosque es un bosque ordenado, es decir, que todos los rboles del bosque a son rboles ordenados; entonces un recorrido en entreorden dar como resula a tado una secuencia de nodos ordenada en sentido ascendente.

6.5.

Ejercicios de programacin o

1. Escriba un programa que acepte un apuntador a un nodo y devuelva un valor verdadero si este nodo es la ra de un rbol binario vlido y falso z a a en caso contrario. 2. Escriba un programa que acepte un apuntador a un rbol binario y un a apuntador a un nodo del rbol, y devuelva el nivel del nodo en el rbol. a a 3. Escriba un programa para ejecutar el experimento siguiente: genere 100 nmeros aleatorios. Conforme se genera cada nmero, insrtelo en un u u e 69

a rbol de bsqueda binaria inicialmente vac Despus de insertar los 100 u o. e nmeros, imprima el nivel de la hoja que tiene el nivel ms grande y u a el nivel de la hoja que tiene el nivel ms chico. Repita este proceso 50 a veces. Imprima una tabla que indique cuntas veces de las 50 ejecuciones a produjeron una diferencia entre el nivel de hoja mximo y m a nimo de 0,1,2,3, y as sucesivamente. 4. Implemente los recorridos de los rboles binarios. a 5. Si un bosque se representa mediante un rbol binario, muestre que el a nmero de v u nculos derechos nulos es 1 mayor que el nmero de no hojas u del bosque.

70

7.

Grafos

En esta parte del curso vamos a retomar la idea de los grfos. Hasta ahora a homos visto las listas y los rboles como casos especiales de los grafos. Rea sumiendo, las listas son grafos en donde cada nodo tiene una arista que sale y una arista que llega, excepto un par de nodos, uno de esos nodos es el inicio de la lista que tiene no tiene arista que entra; y el otro nodo es el nal de la lista que no tiene arista que sale; En los rboles, los nodos tienen una arista a que llega (la del padre) y una o ms aristas que salen (los hijos). a Como veremos ms adelante con mucho mayor detalle, los nodos en los grafos a no tienen l mite de aristas que salen o aristas que lleguen, por eso tanto las listas como los rboles son casos particulares de los grafos. a

7.1.

Recordatorio de las deniciones

Un grafo consiste de una tupla G = N, A , en donde N es un conjunto de elementos llamados nodos; y A es una relacin, representada por un conjunto o de pares ordenados de nodos. El conjunto N de nodos debe de ser un conjunto no-vac esto signica que o, para que exista un grafo es necesario al menos un nodo. El conjunto A de aristas puede ser el conjunto vac. En la gura 33 se muestra un grafo y sus o conjuntos de nodos y de aristas.

Figura 33. Grafo dirigido o digrafo

Si las aristas de un grafo no estn dirigidas se omiten las echas, y se dice a entonces que es un grafo (no un grafo dirigido). Cuando en las aristas no hay echas, se entiende que hay una relacin reexiva, es decir, si para un grafo o 71

G = N, A ; a, b N y se tiene que (a, b), (b, a) A , entonces no se dibujan las echas. Porque la echa indica el sentido de la relacin. o Si G = N, A es un grafo, los siguientes trminos son frecuentemente usados e al trabajar con G: Nodo incidente: Si (a, b) A entonces tanto el nodo a como el nodo b son nodos incidentes. Grado de incidencia: Se dene para cada nodo, y es su mximo numero de a incidencias. Tambin se conoce con el nombre de valencia. e Grado interno: Tambin se dene para cada nodo y es el nmero de arise u tas que llegan a ese nodo. Otro nombre para este trmino es valencia de e entrada. Grado externo: Para cada nodo es el nmero de aristas que salen del nodo. u Se conoce tambin con el nombre de valencia de salida. e 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) A , entonces el nodo a no es adyacente al 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 sucesor del nodo a. Antecesor: Si el nodo a es adyacente al nodo b, entonces el nodo a es el antecesor del nodo b. Es posible asociar una etiqueta a cada arista, como se muestra en la gura 34. La etiqueta asociada con cada arista se denomina peso.

Figura 34. Grafo dirigido con pesos

Los grafos ponderados son relaciones denidas por un conjunto de elementos, en donde cada elemento es un trio ordenado (a, b, c) donde a, b N y c W , para algn conjunto W de pesos. Con los grafos y grafos ponderados (los que u tienen pesos) se pueden tener algunas operaciones bsicas: a 72

Con grafos: join(a,b): Agrega una relacin del nodo a al nodo b. Si la relacin no o o existe, entonces crea una relacin. o removeArc(a,b): Quita un arco del nodo a al nodo b Con grafos ponderados: joinWt(a,b,w): Agrega una relacin del nodo a al nodo b y le asocia el o peso w. Si la relacin no existe, entonces de crea la relacin y le asocia el o o peso indicado. removeArcWt(a,b): Quita un arco del nodo a al nodo b con peso w. La operacin isAdjacent(a,b) devuelve un valor TRUE si el nodo a es o adyacente al nodo b, y devuelve un valor FALSE en caso contrario. Una trayectoria de longitud k del nodo a al nodo b se dene como una secuencia de k + 1 nodos n1 , n2 , . . . , nk , nk+1 , tal que n1 = a, nk+1 = b y isAdjacent(ni ,ni+1 ) para todas las 1 i < k. Una trayectoria de longitud 1 un nodo a s mismo es un autociclo. Si existe una trayectoria de longitud mayor que 1 de un nodo a s mismo, entonces es un ciclo. Si el grafo es ac clico y dirigido, entonces se llama dag (directed acyclic graph). 7.2. Aplicacin ejemplo o

Supongamos el grafo ponderado de la gura 35, este grafo tiene como conjunto de nodos N = {3, 10, 17, 5, 8, 6} y una relacin o 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

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 datos en Pascal. Phh-PrenticeHall, 1983.

74

También podría gustarte