Professional Documents
Culture Documents
2 de junio de 2005
Resumen
Una estructura de datos es una manera de almacenar y organizar datos para facilitar
el acceso y modificaciones. No hay una estructura de datos que sirva para todos los
propositos, y por eso es importante saber sus ventajas y desventajas. Este documento es una coleccion de apuntes para el curso de Estructuras de Datos. Los apuntes
se han tomado de algunas fuentes que son detalladas en la seccion de bibliografa.
Indice
1.
1.1. Arreglos
3
3
1.2. Apuntadores
10
15
19
2.
21
La pila
21
24
2.3. Ejemplo: N
umero de parentesis
25
26
27
29
3.
Colas
31
32
33
34
4.
36
Recursion
39
40
5.
Listas
42
5.1. Grafos
42
44
51
54
56
6.
Arboles
57
57
6.2. Arboles
binarios
57
64
6.4. Arboles
66
69
7.
71
Grafos
71
73
1.
Preliminares de programaci
on en C/C++
1.1.
Arreglos
Definici
on 1 Un arreglo se compone de elementos de igual tama
no almacenados linealmente en posiciones de memoria consecutiva.
Se puede acceder a cada elemento de datos individual utilizando un subndice,
o ndice, para seleccionar uno de los elementos. En C/C++ , un arreglo no es
un tipo de datos estandar; es un tipo agregado compuesto de cualquier otro
tipo de datos.
Los arreglos se pueden definir usando tipos de datos mixtos debido a que se
supone que todos los elementos son del mismo tama
no. Puesto que todos los
elementos son del mismo tama
no y ya que este hecho se utiliza para ayudar
a determinar como localizar un elemento dado, resulta que los elementos son
almacenados en localidades de memoria contiguas.
Lo mas importante a tener en cuenta es: El nombre de un arreglo es visto por el
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 mas
especficamente, b) un puntero constante -significa una direccion de memoria
bloqueada para el primer elemento de un arreglo-. Por ejemplo, aunque una
declaracion de arreglo toma la forma generica:
Tipo_ElementoArray NombreArray [ NumeroDeElementos ]
El compilador ve la declaracion como
Tipo_ElementoArray * const NombreArray = &NombreArray[0];
Por esta razon, un identificador de arreglo no puede ser usado nunca como un
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 asignacion.
Si los nombres de arreglo fueran variables izquierdos permitidos, el programa
podra cambiar sus contenidos.
3
float SalariosDeEmpleados[Max_empleados];
.
.
.
SalariosDeEmpleados = 45739.0;
El efecto hara cambiar la direccion inicial del propio arreglo.
1.1.1.
Declaraciones de un arreglo
#define Coordenadas_Max 20
#define Tamano_MaX_Compania_Id 15
int CoordenadasDePantalla[Coordenadas_Max];
char IDCompania[Tamano_MaX_Compania_Id];
El uso de constantes predefinidas garantiza que futuras referencias al arreglo
no excedan el tama
no del arreglo definido.
1.1.2.
1.1.3.
1.1.4.
#define maxDiasSemana 7
int main(void){
int desplazamiento, maxHorasDiarias[maxDiasSemana];
cout<<"sizeof(int) es"<<(int)sizeof(int)<<"\n\n";
for(desplazamiento=0;desplazamiento<maxDiasSemana;
desplazamiento++)
cout<<"&maxHorasDiarias["
<<desplazamiento
<<"]="
<<&maxHorasDiarias[desplazamiento]<<"\n";
return 0;
}
1.1.5.
Arreglos multidimensionales
El termino dimensi
on representa el n
umero de ndices utilizados para referirse
a un elemento particular en el arreglo. Los arreglos de mas de una dimension
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 filas y 5 columnas por fila, 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 estan
agrupados desde el ndice mas a la derecha hacia el centro. En el ejemplo anterior, fila 1, columna 1 sera el elemento 3 del arreglo almacenado. Aunque el
calculo del desplazamiento aparece un poco difcil, es referenciado facilmente
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.
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 min
usculas
originales: aeiou, mientras que el segundo ciclo for en main() da como salida
los contenidos del arreglo despues del llamado a la funcion ArrayMayuscula():
AEIOU.
Claramente, dentro del cuerpo de la funcion ArrayMayuscula(), ha cambiado
el arreglo de regreso en la funcion main(). el siguiente ejemplo es una simple
modificacion de este algoritmo, solo que en vez de pasar el arreglo completo,
se pasa cada elemento individual:
/*
// ereArray2.xcode
*/
#include <iostream>
#include <ctype.h>
#define maxArray 5
void ElementosArrayMayuscula(char unChar);
int main (int argc, char * const argv[]) {
int desplazamiento;
char Array[maxArray]=
{a,e,i,o,u};
for(desplazamiento=0;desplazamiento<maxArray;desplazamiento++)
std::cout<<Array[desplazamiento];
std::cout<<"\n";
for(desplazamiento=0;desplazamiento<maxArray;desplazamiento++)
ElementosArrayMayuscula(Array[desplazamiento]);
for(desplazamiento=0;desplazamiento<maxArray;desplazamiento++)
std::cout<<Array[desplazamiento];
return 0;
}
void ElementosArrayMayuscula(char unChar)
{
unChar=toupper(unChar);
}
La salida del programa es:
9
aeiou
aeiou
valarray has exited with status 0.
1.2.
Apuntadores
Definici
on 2 Un apuntador es una variable que contiene una direcci
on de
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 direccion
de la variable en lugar de su contenido. As que para asignar la direccion de
una variable a otra variable del tipo que contiene direcciones se usan sentencias
como esta:
direccionRam = &contenidoRAM
direccionRAM = &contenidoRAM;
*direccionRAM = 20;
1.2.1.
C/C++ requiere una definicion para cada variable. Para definir una variable
apuntador direccionRAM que pueda contener la direccion de una variable
int, se escribe:
int *direccionRAM;
Realmente existen dos partes separadas en esta declaracion. El tipo de dato
de direccionRAM es:
int *
y el identificador para la variable es
direccionRAM
El asterisco que sigue a int significa apuntador a. Esto es, el siguiente tipo
de dato es una variable apuntador que puede contener una direccion a un int:
int *
11
1.2.2.
Este puede ser un error muy difcil de localizar puesto que muchos compiladores no emiten ninguna advertencia/error.
Para empeorar el asunto, la mayora de los apuntadores son cercanos, lo que
significa que ocupan 2 bytes (4 bytes para aplicaciones de 32-bits), el mismo
tama
no que un entero en una PC.
La sentencia (11) copia el contenido de la variable B int en la celda apuntada
por la direccion almacenada en direccion int(figura 7):
*direccion_int = B_int;
La u
ltima sentencia en la lnea (12) simplemente copia el contenido de una
variable entera, Temp int en otra variable entera B int (figura 8
1.2.3.
1.3.
Estructuras C/C++
Definici
on 3 Una estructura es un grupo de variables las cuales pueden ser
de diferentes tipos sostenidas o mantenidas juntas en una sola unidad. La
unidad es la estructura.
1.3.1.
tipo_miembro miembro_3;
:
:
tipo_miembro miembro_n;
};
Un punto y coma finaliza la definicion de una estructura puesto que esta es
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 define stbarco_usado de tipo struct stbarco. La declaracion
requiere el uso del campo etiqueta de la estructura. Si esta sentencia esta contenida dentro de una funcion, entonces la estructura, llamada stbarco_usado,
tiene un ambito local a esa funcion. Si la sentencia esta contenida fuera de
todas las funciones de programa, la estructura tendra un ambito global. Es
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 declaracion de variable va antes del punto y coma final. Cuando se
asocia solo una variable con el tipo estructura, el campo etiqueta puede ser
eliminado, por lo que sera posible escribir:
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.
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
1.4.
Ejercicios de programacion
20
2.
La pila
2.1.
Definicion y ejemplos
Definici
on 4 Una pila (stack) es una colecci
on ordenada de elementos en la
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 mas alto en la pila que el otro. En
la figura 9 el elemento F es el mas alto de todos los elementos que estan en la
pila. El elemento D es el mas alto de los elementos A,B,C, pero es menor que
los elementos E y F.
Para describir como funciona esta estructura, debemos agregar un nuevo elemento, el elemento G. Despues de haber agregado el elemento G a la pila, la
nueva configuracion es la que se muestra en la figura 10.
De acuerdo con la definicion, existe solamente un lugar en donde cualquier
elemento puede ser agregado a la pila. Despues de haber insertado el nuevo
elemento, G ahora es el elemento en la cima. Debedos aclarar en que pila
deseamos insertar elementos, puesto que es posible tener mas de una pila al
mismo tiempo.
21
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
esta en la cima de la pila y solamente podemos retirar el elemento que esta en
la cima. Para que la sentencia retira C de la pila tenga sentido, debemos
replantear las ordenes a algo como:
Retira de la pila hasta que el elemento retirado sea C.
Ni siquiera es necesario decir: Retira un elemento de la pila... porque es
sobreentendido que solamente se puede sacar un elemento a la vez.
Siguiendo nuestro ejemplo, ahora deseamos retirar de la pila P. La configuracion global de la pila es como se muestra en la figura 11
2.2.
Operaciones basicas
2.2.1.
La operacion push
2.2.2.
La operacion pop
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 operacion pop escribiendola
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 operacion tiene algunas implicaciones:
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 operacion, e era el elemento en la cima, ahora ya no lo es mas.
El apuntador cima decrece en una unidad.
2.2.3.
La operacion stackempty
Esta operacion toma como argumento una estructura del tipo stack (pila) y
devuelve un valor booleano, devuelve un true si la pila esta vaca y devuelve
24
stackempty(S) =
f alse si S tiene m
as de 0 elementos
2.2.4.
La operacion stacktop
2.3.
Ejemplo: N
umero de parentesis
:
:
:
:
:
:
push(S,()
push(S,()
nada que hacer
nada que hacer
nada que hacer
v=pop(S)
25
* :
4 :
) :
/ :
( :
17:
+ :
9 :
) :
2.4.
2.5.
2.5.1.
La operacion push
El siguiente segmento de codigo ilustra como se puede implementar la operacion insertar un elemento en una pila. Hemos supuesto que la pila ya
esta definida como una estructura stack.
(1) void push(struct stack *S,int e){
(2) S->top++;
(3) S->item[S->top]=e;
(4) }
En la lnea (1) se observa que la operacion push recibe dos parametros: la
direccion de una estructura de tipo pila y un elemento de tipo entero.
La lnea (2) incrementa el tope (cima) de la pila en una unidad, con el fin de
agregar el elemento en una posicion libre de la pila, lo cual se logra en la lnea
(3), asignando el valor e en la casilla S->top del arreglo item de la pila.
27
2.5.2.
La operacion pop
2.5.3.
La operacion stackempty
2.5.4.
La operacion stacktop
2.6.
Problemas de programacion
30
3.
Colas
Definici
on 5 Las colas son una estructura de datos similar a las pilas. Recordemos que las pilas funcionan en un dep
osito en donde se insertan y se retiran
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 fifo (first
in, first out) porque funcionan como una tubera, lo que entra primero por un
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. Dinamica de una cola. a) estado actual con una cola con tres elementos
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
3.1.
De manera similar a las pilas, las colas definen una estructura no estandar, de
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 alg
un tipo especfico, puede incluso ser un
tipo estandar o no.
Un n
umero que indica el elemento que esta en la posicion del frente de la
cola.
Un n
umero que indica el elemento que esta en la posicion trasera de la cola.
Suponiendo que los elementos son n
umeros enteros, una idea para representar
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 representacion con arreglos es completamente valida, pero debemos tener
cuidado con los lmites del arreglo. Suponiendo que no existiera la posibilidad
de caer en un desbordamiento del arreglo, es decir, que se insertaran mas
elementos de lo que el arreglo puede almacenar, la operacion insert podra
quedar como:
void insert(struct cola *C, int e){
C->items[++C->rear]=e;
}
y al operacion x=remove(Q)
int remove(struct cola *C){
return C->items[C->front++];
32
}
y finalmente la operacion empty(Q):
bool empty(struct cola *C){
if(C->front>C->rear)
return true;
else
return false;
}
3.2.
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 operacion insert(CPA,x) inserta
el elemento x en la cola CPA; y la operacion x=minRemove(CPA) asigna a x el
valor del elemento menor (de su prioridad) y lo remueve de la cola.
En las colas de prioridad descendente es similar, pero solo permite la supresion
del elemento mas grande. Las operaciones aplicables a la cola de prioridad
descendente son insert(CPD,x) y x=maxRemove(CPD), cuando CPD es una
cola de prioridad descendente y x es un elemento.
La operacion empty(C) se aplica a cualquier tipo de cola y determina si una
cola de prioridad esta vaca. Las operaciones de insertar y borrar se aplican
solamente si la pila no esta vaca.
Los elementos de la cola de prioridad no necesitan ser n
umeros o caracteres
para que puedan compararse directamente. Pueden ser estructuras complejas
ordenadas en uno o varios campos. Por ejemplo, las agendas telefonicas constan
de apellidos, nombres, direcciones y n
umeros de telefono y estan ordenadas
por apellido.
A diferencia de las pilas y las colas, en las colas de prioridad se pueden sacar
los elementos que no estan en el primer sitio del extremo donde salen los
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
verificar cada uno de los elementos almacenados para saber cual es el menor
(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 vaco en la casilla de un elemento suprimido.
Este enfoque realmente no es muy bueno, porque de cualquier modo se
accesan los elementos para saber si es una localidad vaca o no lo es. Por
otro lado, cuando se remueven elementos, se van creando lugares vacos
y despues es necesario hacer una compactacion, reubicando los elementos
en el frente de la cola.
2. Cada supresion puede compactar el arreglo, cambiando los elementos
depues del elemento eliminado en una posicion y despues decrementando
rear en 1. La insercion no cambia. En promedio, se cambian la mitad de
los elementos de una cola de prioridad para cada supresion, por lo que
esta operacion no es eficiente.
3.3.
Ejercicio de programacion
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 esta en el extremo norte, se sacan todos los automoviles de ese lado,
se retira el auto y los otros coches se restablecen en el mismo orden 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 vacos
esten en la parte sur del estacionamiento. Escriba un programa que lea un
grupo de lineas de ingreso. Cada lnea contiene una A para las llegadas
y una D para las salidas y un n
umero de placa. Se supone que los
carros llegan y salen en el orden especificado en la entrada. El programa
debe imprimir (en la terminal estandar) un mensaje cada vez que entra
o sale un auto. Cuando llega un carro, el mensaje debe especificar si hay
espacio o no para el en el estacionamiento. Si no hay espacio, el carro
espera hasta que hay espacio o hasta que se lee una lnea 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 movio el auto dentro del estacionamiento, incluyendo la salida misma,
pero no la llegada. Este n
umero es 0 si el carro sale de la fila de espera.
35
4.
Recursi
on
!n =
n
Y
i,
i=1
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;
!n = n !(n 1)
El programa en C/C++ es el que se muestra a continuacion:
( 1) double factorial(double a){
( 2) if (a<=1) return 1.0;
( 3)
else
return (a *factorial(a-1.0)); }
( 4)
( 5) int main (int argc, char * const argv[]) {
( 6) double n;
( 7)
std::cout << "Numero entero:";
( 8) std::cin>> n;
( 9) std::cout<<"El factorial de "<<n<<" es: "<< factorial(n);
(10)
return 0; }
Aqu hay varias cosas que se
nalar, en primer lugar se ha creado una nueva
funcion, a diferencia de la definicion iterativa en donde era suficiente trabajar en el programa principal. Esta funcion se llama factorial (como era de
suponerse), y empieza su encabezado en la lnea (1).
All mismo en la misma lnea (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 version iterativa en donde empleabamos tipos diferentes. La
razon es que al iniciar la recursion el argumento es del tipo devuelto, asi que
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 funcion se recupera una entrada de la
pila. En la figura 16 ilustra como funciona la recursividad cuando se intenta
obtener el factorial(5).
37
4.0.1.
La serie Fibonacci
Una de las series mas famosas es sin duda alguna la serie de Fibonacci:
fib(n) =
Si n = 1 o n = 2
#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;
}
4.1.
Peligros en la recursividad
#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 programacion
b
usqueda termina.
Si el arreglo tiene mas de 1 elemento, tendremos que dividir en dos el
arreglo y decidir en que parte del arreglo buscar; luego buscarlo usando
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 estaticas de pilas y
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 solucion es usar listas. Las listas son estructuras de datos que son dinamicas, esto significa 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 introduccion a los grafos, pues
las listas son un caso especial de los grafos.
5.1.
Grafos
(carolina, lenguajes),
(rafael, compiladores),
(gustavo, lenguajes),
(fabiola, lenguajes)}
Graficamente podemos ilustrar el conjunto R de toma el curso de con un
grafo como el que se muestra en la figura 18.
A esta clase de grafos, en las que cada nodo tiene a lo mas una arista dirigida
que sale y a lo mas una arista dirigida que entra, se le llama lista.
5.2.
Como vimos en la seccion anterior, una lista es una relacion de elementos, tales
que cada elemento esta relacionado con u
nicamente un elemento del conjunto,
diferente a s mismo.
44
Como cada elemento puede tener a lo mas una arista dirigida que sale y una
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 final de la lista.
Si el nodo tiene 0 aristas que entran, entonces es el inicio de la lista.
Por razones practicas, se dibujan una flecha que sale de un identificador de la
lista y entra al inicio de la lista y otra flecha que sale del final de la lista y
apunta a un smbolo que se llama NULO.
Enseguida vamos a dar una lista de terminos usados para manejar los elementos 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
direccion de una variable del tipo declarado para los nodos de una lista:
node(p): hace referencia al noso al que se apunta mediante p.
info(p): hace referencia a la informacion del nodo al que apunta p.
next(p): hace referencia a la parte direccion siguiente y, por tanto, es un
apuntador.
As que la expresion info(next(p)) significa que se hace referencia a la seccion
de informacion del nodo siguiente al que apunta p.
5.2.1.
Una vez que se ha creado un nuevo espacio para el nuevo nodo, se debe de
establecer la parte de informacion de ese nodo con la operacion info(p), como
se ilustra en el siguiente ejemplo con el dato 6.
46
info(p)=6;
Despues de esstablecer la parte de informacion es necesario establecer la parte
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 identificador de la lista) contiene
la direccion de ese primer nodo, node(p) se agrega a la lista ejecutando la
operacion
next(p)=lista;
Esta operacion coloca el valor de lista (la direccion del primer nodo en la
lista) en el campo siguiente de node(p). Estos pasos se ilustran en la figura
23
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 modificarse en la direccion del nuevo primer nodo de la lista. Esto se
hace ejecutando la operacion
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 lneas, que se pueden apreciar en la
figura 24
5.2.2.
( 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 lneas (2) a (5) se crea un nuevo tipo de dato,
el tipo nodo. Cada nodo tiene dos partes, su parte de informacion y su parte
de apuntador al siguiente. Como solamente tenemos 500 nodos (declarados en
la lnea (1), el tipo de siguiente es entero y hemos decidido almacenar n
umeros
enteros solamente.
En la lnea (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 u
ltimo nodo apunta a NULL, que se representa con
el valor entero -1. Tenemos tambien los siguientes elementos de cada nodo:
node[p] corresponde a next(p), por la notacion propia del lenguaje; tambien
node[p].info para info(p) y finalmente node[p].next hace referencia al
nodo siguiente next(p).
Al principio todos los nodos estan sin usar, porque solamente se ha creado
el arreglo. As que todos los nodos van a formar parte de una lista de nodos disponibles. Si se usa la variable global avail para apuntar a la lista
disponible, podramos 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 significa que no hay nodos disponibles, es decir, que el
arreglo esta completamente lleno. Esto significa que las estructuras de lista
de un programa particular han desbordado el espacio disponible. La funcion
freeNode acepta un apuntador (n
umero entero) a un nodo y devuelve ese
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 parametros. Primero se asegura que p no sea nulo
y despues se inserta x en el nodo siguiente al indicado por p.
void insAfter(int p, int x){
int q;
if(p==-1){
std::cout<<"void insertion\n";
}
else{
q=getNode();
node[q].info=x;
node[q].next=node[p].next;
node[p].next=q;
}
}
La rutina delAfter(p,px), llamada por el enunciado delAfter(p,&x), suprime
el nodo despues de node(p) y almacena su contenido en x;
void delAfter(int p, int *px){
int q;
if((p==-1)||(node[p].next==-1)){
50
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.
La palabra clave extern especifica que una variable o funcion tiene un enlace externo. Esto significa que la variable o funcion a la que nos referimos
esta definida en alg
un otro archivo fuente, o mas adelante en el mismo archivo. Sin embargo, en C/C++ podemos usar esta palabra clave extern con una
cadena. La cadena indica que se esta usando el convenio de enlace de otro
lenguaje para los identificadores que se estan definiendo. Para los programas
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 dinamicas. Al ejecutar estos enunciados, el operador sizeof
devuelve el tama
no en bytes de su operando. Esto se usa para conservar la
independencia de maquina. Despues, malloc crea un objeto de este tama
no.
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 se
nale a un entero, usamos el operador de
calculo (int *) o (float *).
El operador sizeof, devuelve un valor de tipo int, en tanto que la funcion
malloc espera un parametro de tipo unsigned. Para hacer que correspondan,
debemos escribir
pi=(int *)malloc((unsigned)(sizeof(int)));
Como ejemplo, vamos a considerar este breve codigo:
#include <iostream>
int main (int argc, char * const argv[]) {
( 1) int *p, *q;
( 2) int x;
( 3) p = (int *)malloc(sizeof(int));
( 4) *p = 3;
( 5) q = p;
( 6) std::cout<< *p << " " << *q << "\n";
( 7) x = 7;
( 8) *q = x;
( 9) std::cout<< *p << " " << *q << "\n";
(10) p = (int *)malloc(sizeof(int));
(11) *p = 5;
52
(12)
}
En la linea (3), se crea una variable de tipo entero y su direccion se coloca
en p. La lnea (4) establece el valor de esa variable en 3. La lnea (5) hace
que la direccion q sea la misma direccion que p. El enunciado de la lnea (5)
es perfectamente valido, pues se asigna a una variable de tipo apuntador (q)
el valor de otra variable del mismo tipo (p). En este momento *p y *q hacen
referencia a la misma variable. Por tanto, la lnea (6) imprime el contenido de
esa variable (que ahora es 3) dos veces.
En la lnea (7), se almacena el valor 7 en la variable entera x. La lnea (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 lnea (9) imprime el
n
umero 7 dos veces.
La lnea (10) crea una nueva variable entera y coloca su direccion en p. Ahora
*p hace referencia a la variable entera recien creada que todava no ha recibido
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 especfica u
nica. Su valor cambia
conforme se modifica el valor de p. La lnea (11) establece el valor de esta
variable recien creada en 5 y la lnea 12 imprime los valores 5 y 7. Y as la
salida del programa es:
3
7
5
3
7
7
#include <iostream>
int main (int argc, char * const argv[]) {
int *p, *q;
(
(
(
(
(
(
(
(
(
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;
}
Que se imprime a la salida del programa?
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 direccion no se guardo.
5.4.
Para hacer las listas ligadas necesitamos un conjunto de nodos, cada uno de
los cuales tiene dos campos: uno de informacion y un apuntador al siguiente
nodo de la lista. Ademas, un apuntador externo se
nala el primer nodo de la
lista. Usamos variables de apuntador para implementar apuntadores de listas.
As que definimos el tipo de un apuntador y un nodo mediante
struct node{
int info;
struct node *next;
};
typedef struct node *nodePtr;
54
nodePtr q;
if((p==NULL)||(p->next==NULL)){
std::cout<<"Borrado prohibido\n";
} else{
q=p->next;
*px=q->info;
p->next=q->next;
freeNode(q);
}
}
5.5.
Ejercicios de programacion
1. Implemente una pila usando memoria dinamica en listas ligadas. Implemente las operaciones push, pop, empty y stackTop.
2. Implemente una cola usando memoria dinamica en listas ligadas. Implemente las operaciones empty, insert y remove.
3. Desarrolle un programa para buscar un elemento en la lista (de n
umeros
enteros) y borrar la primera ocurrencia de ese elemento.
4. Desarrolle un programa para buscar un elemento en la lista (de n
umeros
enteros) y borrar todas las ocurrencias de ese elemento.
5. Las listas doblemente ligadas tienen nodos que estan divididos en tres
segmentos:
a) Anterior: Un apuntador a un nodo
b) Info: La informacion de un nodo
c) Siguiente: Un apuntador a un nodo
Implemente las operaciones borrarNodo(p), insertarNodoAntes e
insertarNodoDespues.
56
Arboles
6.
6.1.
6.2.
Arboles
binarios
Definici
on 10 Un arbol binario es una estructura de datos de tipo
arbol en
donde cada uno de los nodos del arbol puede tener 0, 1,
o 2 sub
arboles llamados
de acuerdo a su caso como:
Si el nodo raz tiene 0 relaciones se llama hoja.
Si el nodo raz tiene 1 relacion a la izquierda, el segundo elemento de la
relacion es el subarbol izquierdo.
Si el nodo raz tiene 1 relaci
on a la derecha, el segundo elemento de la
relacion es el subarbol derecho.
La figura 25 muestra algunas configuraciones de grafos que s son arboles
binarios, y la figura 26 muestra algnas configuraciones de grafos que no son
arboles binarios.
Vamos a dar una lista de teerminos que se usan frecuentemente cuando se
trabaja con arboles:
57
Si A es la raz de un arbol y B es la raz de su subarbol izquierdo (o derecho), 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 recprocamente el nodo b es descendiente del nodo a), si a es el padre de b o el padre de alg
un ancestro de b.
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 define de la misma forma.
Dos nodos son hermanos si son hijos izquierdo y derecho del mismo padre.
Otros terminos relacionados con arboles, tienen que ver con su funcinoamiento
y topologa:
Si cada nodo que NO es una hoja tiene un subarbol izquierdo y un subarbol
derecho, entonces se trata de un
arbol binario completo.
El nivel de un nodo es el n
umero de aristas que se deben recorrer para
58
llegar desde ese nodo al nodo raz. De manera que el nivel del nodo raz es
0, y el nivel de cualquier otro nodo es el nivel del padre mas uno.
La profundidad de un nodo es el maximo nivel de cualquier hoja en el
arbol.
Si un arbol binario tiene m nodos en el nivel l, el maximo n
umero de nodos
en el nivel l + 1 es 2m. Dado que un arbol binario solo tiene un nodo en el
nivel 0, puede contener un maximo de 2l nodos en el nivel l. Un arbol binario
completo de profundidad d es el arbol que contiene exactamente 2l nodos en
cada nivel l entre 0 y d. La cantidad total de nodos tn en un arbol binario
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 =
d
X
2j
j=0
Si conocemos el n
umero total de nodos tn en un arbol binario completo, podemos calcular su profundidad d, a partir de la expresion tn = 2d+1 1. As sabemos que la profundidad d es igual a 1 menos que el n
umero de veces que 2
debe ser multiplicado por s mismo para llegar a tn + 1. Es decir, que en un
arbol binario completo,
d = log2 (tn + 1)
Definici
on 11 Un arbol binario es un
arbol binario casi completo si:
1.
2.
6.2.1.
Con los arboles binarios es posible definir algunas operaciones primitivas, estas
operaciones son en el sentido de saber la informacion de un nodo y sirven para
desplazarse en el arbol, hacia arriba o hacia abajo.
info(p) que devuelve el contenido del nodo apuntado por p.
left(p) devuelve un apuntador al hijo izquierdo del nodo apuntado por p, o
bien, devuelve NULL si el nodo apuntado por p es una hoja.
right(p) devuelve un apuntador al hijo derecho del nodo apuntado por p, o
bien, devuelve NULL si el nodo apuntado por p es una hoja.
father(p) devuelve un apuntador al padre del nodo apuntado por p, o bien,
devuelve NULL si el nodo apuntado por p es la raz.
brother(p) devuelve un apuntador al hermano del nodo apuntado por p, o
bien, devuelve NULL si el nodo apuntado por p no tiene hermano.
Estas otras operaciones son logicas, tienen que ver con la identidad de cada
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 construccion de un arbol binario son u
tiles las operaciones makeTree,
setLeft y setRight. La operacion makeTree(x) crea un nuevo arbol binario
que consta de un u
nico nodo con un campo de informacion x y devuelve un
apuntador a ese nodos. La operacion setLeft(p,x) acepta un apuntador p
a un nodo de arbol binario sin hijo izquierdo. Crea un nuevo hijo izquierdo
de node(p) con el campo de informacion x. La operacion setRight(p,x) es
similar, excepto que crea un hijo derecho.
6.2.2.
Figura 29. Arbol
binario para ordenar una secuencia de n
umeros
6.3.
6.3.1.
Los nodos de los arboles binarios son estructuras en C/C++ que estan compuestas por tres partes:
Un apuntador al subarbol izquierdo, left
Un apuntador al subarbol derecho, right
Una parte de informacion, que puede ser una estructura en s misma, info.
Adicionalmente es muy u
til poner un apuntador al padre del nodo. father.
Usando una implementacion de arreglos tenemos:
#define numNodes 500
struct nodeType{
int info;
int left;
int right;
int father;
};
struct nodeType node[numNodes];
y usando una representacion con memoria dinamica, los nodos de un arbol se
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 implementaran
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.
6.4.
Arboles
Hasta ahora hemos visto los arboles binarios que son aquellos arboles que sus
nodos solamente pueden tener un maximo de dos hijos. Cuando ocurre que
los nodos tienen cualquier n
umero finito de hijos, son arboles (en genreal). De
manera que
Definici
on 12 Un arbol es un conjunto finito no vaco de elementos en el
cual un elemento se denomina la raz y los restantes se dividen en m 0
subconjuntos disjuntos, cada uno de los cuales es por s mismo un
arbol. Cada
elemento en un arbol se denomina un nodo del
arbol
66
Un nodo sin subarboles es una hoja. Usamos los terminos padre, hijo, hermano, antecesor, descendiente, nivel y profundidad del mismo modo
que en los arboles binarios. El grado de un nodo es en n
umero maximo de
hijos que al
un nodo tiene.
Un
arbol ordenado de define como un arbol en el que los subarboles de cada
nodo forman un conjunto ordenado. En un arbol ordenado, podemos hablar
del primero, segundo o u
ltimo hijo de un nodo en particular. El primer hijo de
un nodo en un arbol ordenado se denomina con frecuencia el hijo m
as viejo
de este nodo y el u
ltimo se denomina el hijo m
as joven. Vease la figura 30.
Un bosque es un conjunto ordenado de arboles ordenados.
6.4.1.
Al igual que en los arboles binarios, los nodos en un arbol tienen una parte
de informacion, un apuntador al padre y uno o mas apuntadores a los hijos.
De manera que una solucion es crear una estructura que incluya una lista
dinamica de apuntadores, como lo muestra la figura 31.
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 mas
joven, en lugar de dejarlo en null. Se podra usar un campo logico adicional
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 arbol
binario y que next corresponde a su apuntador right, este metodo representa
en realidad un arbol ordenado general mediante un arbol binario.
6.4.2.
Recorridos de arbol
Los metodos de recorrido para arboles binarios inducen metodos para recorrer
los arboles en general. Si un arbol se representa como un conjunto de nodos
de variables dinamicas con apuntadores son y next, una rutina en C/C++ para
imprimir el contenido de sus nodos se escribira como:
void inTr(nodePtr tree){
if (tree != NULL){
inTr(tree->left);
std::cout<<tree->info;
inTr(tree->right);
}
}
Las rutinas para recorrer el arbol en los demas ordenes son similares. Estos
recorridos tambien se defininen directamente as:
Orden previo: similar al caso binario.
1. Visitar la raz
2. Recorrer en orden previo los subarboles de izquierda a derecha
Las demas rutinas son similares.
Un bosque puede ser representado medianto un arbol binario.
68
Para hacer esta representacion, la raz de cada arbol se coloca en una lista
de apuntadores; luego para cada nodo en la lista (la raz de cada arbol) se
procede del siguiente modo:
1. Se crea una lista de subarboles izquierdos con los apuntadores a cada uno
de los arboles en el bosque.
2. si un nodo tiene mas de un hijo, entonces se crea un subarbol izquierdo
y se forma una lista de subarboles izquierdos con todos los hijos de ese
nodo.
Figura 32. Arriba: Un bosque de arboles. Abajo: El arbol binario que corresponde
a ese bosque.
6.5.
Ejercicios de programacion
arbol de b
usqueda binaria inicialmente vaco. Despues de insertar los 100
n
umeros, imprima el nivel de la hoja que tiene el nivel mas grande y
el nivel de la hoja que tiene el nivel mas chico. Repita este proceso 50
veces. Imprima una tabla que indique cuantas veces de las 50 ejecuciones
produjeron una diferencia entre el nivel de hoja maximo y mnimo de
0,1,2,3, y as sucesivamente.
4. Implemente los recorridos de los arboles binarios.
5. Si un bosque se representa mediante un arbol binario, muestre que el
n
umero de vnculos derechos nulos es 1 mayor que el n
umero de no hojas
del bosque.
70
7.
Grafos
En esta parte del curso vamos a retomar la idea de los grafos. Hasta ahora
homos visto las listas y los arboles como casos especiales de los grafos. Resumiendo, 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 final de la
lista que no tiene arista que sale; En los arboles, los nodos tienen una arista
que llega (la del padre) y una o mas aristas que salen (los hijos).
Como veremos mas adelante con mucho mayor detalle, los nodos en los grafos
no tienen lmite de aristas que salen o aristas que lleguen, por eso tanto las
listas como los arboles son casos particulares de los grafos.
7.1.
Con grafos:
join(a,b): Agrega una relacion del nodo a al nodo b. Si la relacion no
existe, entonces crea una relacion.
removeArc(a,b): Quita un arco del nodo a al nodo b
Con grafos ponderados:
joinWt(a,b,w): Agrega una relacion del nodo a al nodo b y le asocia el
peso w. Si la relacion no existe, entonces de crea la relacion y le asocia el
peso indicado.
removeArcWt(a,b): Quita un arco del nodo a al nodo b con peso w.
La operacion isAdjacent(a,b) devuelve un valor TRUE si el nodo a es
adyacente al nodo b, y devuelve un valor FALSE en caso contrario.
Una trayectoria de longitud k del nodo a al nodo b se define como una secuencia de k + 1 nodos hn1 , n2 , . . . , nk , nk+1 , i 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 acclico y dirigido, entonces se llama dag (directed
acyclic graph).
7.2.
Aplicacion ejemplo
Supongamos el grafo ponderado de la figura 35, este grafo tiene como conjunto
de nodos N = {3, 10, 17, 5, 8, 6} y una relacion
R = {(3, 10, 1), (10, 17, 7), (8, 17, 1), (5, 8, 3), (5, 6, 1), (6, 17, 5)}
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