Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Estructura de Datos Cinvestav
Estructura de Datos Cinvestav
ndice General
Preliminares de programacin en C/C++
o
Arreglos
o
Apuntadores
o
Estructuras C/C++
o
Ejercicios de programacin
La pila
o
o
o
o
o
o
Definicin y ejemplos
Operaciones bsicas
Ejemplo: Nmero de parntesis
La estructura de datos Pila en C/C++
La representacin en C/C++ de las operaciones de una pila
Problemas de programacin
Colas
o
o
o
Recursin
o
Peligros en la recursividad
o
Ejercicios de programacin
Listas
o
o
o
o
o
Grafos
Listas simplemente encadenadas
El uso de memoria dinmica en C/C++
Listas ligadas usando memoria dinmica
Ejercicios de programacin
rboles
o
o
o
o
o
Grafos
o
o
Bibliografa
1
Arreglos
Definicin 1 Un arreglo se compone de elementos de igual tamao 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 estndar; 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 tamao. Puesto que todos los elementos son del mismo tamao y ya que este
hecho se utiliza para ayudar a determinar cmo localizar un elemento dado, resulta que los elementos
son almacenados en localidades de memoria contiguas.
Lo ms 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 ms especficamente, b) un puntero constante -significa una direccin de
memoria bloqueada para el primer elemento de un arreglo-. Por ejemplo, aunque una declaracin de
arreglo toma la forma genrica:
Tipo_ElementoArray NombreArray [ NumeroDeElementos ]
Por esta razn, 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 asignacin.
Si los nombres de arreglo fueran variables izquierdos permitidos, el programa podra cambiar sus
contenidos.
float SalariosDeEmpleados[Max_empleados];
.
.
.
SalariosDeEmpleados = 45739.0;
Declaraciones de un arreglo
La sintaxis de declaracin de arreglos es:
tipo nombre_arreglo [numero_de_elementos];
En la figura 1 se muestra el primer arreglo que fue declarado con el tipo de nmeros enteros,
llamado CoordenadasDePantalla, ocupa en memoria 5 localidades de memoria contiguas, cada una de
ellas capaz de almacenar un nmero entero. Actualmente es comn que los nmeros enteros sean de 32
bits
bits, esto hace que el arreglo CoordenadasDePantalla ocupe
No se permite utilizar nombres de variables dentro de los corchetes. Por esto no es posible evitar
la especificacin del tamao del arreglo hasta la ejecucin del programa. La expresin debe ser un valor
constante, para que el compilador sepa exactamente cunto espacio de memoria tiene que reservar para
el arreglo.
Una buena prctica de programacin es usar constantes predefinidas.
#define Coordenadas_Max 20
#define Tamano_MaX_Compania_Id 15
int CoordenadasDePantalla[Coordenadas_Max];
char IDCompania[Tamano_MaX_Compania_Id];
Por defecto:
Cuando son creados, se aplica solamente a arreglos globales y estticos.
Explcita:
Cuando son creados, suministrando datos de iniciacin
Tiempo de ejecucin:
Durante la ejecucin del programa cuando se asignan o copias datos en el arreglo.
Arreglos multidimensionales
El trmino dimensin representa el nmero de ndices utilizados para referirse a un elemento particular
en el arreglo. Los arreglos de ms de una dimensin 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";
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 iniciar cada uno de los elementos del arreglo 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 estn agrupados desde el ndice ms a la
derecha hacia el centro. En el ejemplo anterior, fila 1, columna 1 sera el elemento 3 del arreglo
almacenado. Aunque el clculo del desplazamiento aparece un poco difcil, es referenciado fcilmente
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.
/*
// 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
}
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 originales: aeiou, mientras que el
segundo ciclo for en main() da como salida los contenidos del arreglo despus del llamado a la funcin
ArrayMayuscula(): AEIOU.
Claramente, dentro del cuerpo de la funcin ArrayMayuscula(), ha cambiado el arreglo de
regreso en la funcin main(). el siguiente ejemplo es una simple modificacin de este algoritmo, slo
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);
}
Apuntadores
Definicin 2 Un apuntador es una variable que contiene una direccin de memoria.
Supongamos una variable de tipo entero que se llama contenidoRAM y otra variable que se llama
que puede contener una variable de tipo entero. En C/C++ una variable precedida del
operador & devuelve la direccin de la variable en lugar de su contenido. As que para asignar la
direccin de una variable a otra variable del tipo que contiene direcciones se usan sentencias como esta:
direccionRAM
direccionRam = &contenidoRAM
direccionRAM = &contenidoRAM;
*direccionRAM = 20;
int *direccionRAM;
Realmente existen dos partes separadas en esta declaracin. El tipo de dato de direccionRAM es:
int *
El asterisco que sigue a int significa ``apuntador a''. Esto es, el siguiente tipo de dato es una variable
apuntador que puede contener una direccin a un int: int *
En C/C++ una variable apuntador contiene la direccin de un tipo de dato particular:
char *direccion_char;
char *direccion_int;
En la lnea (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 memoria asignada para la variable A_int es la
direccin 5328, y la direccin 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 figura 5;
La lnea (10) utiliza la expresin *direccion_int para acceder al contenido de la celda a la cual
apunta direccion_int:
Temp_int = *direccion_int;
10
La ltima sentencia en la lnea (12) simplemente copia el contenido de una variable entera,
en otra variable entera B_int (figura 8 )
Temp_int
11
Estructuras
C/C++
Definicin 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.
Un punto y coma finaliza la definicin de una estructura puesto que sta 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 declaracin requiere el uso del
campo etiqueta de la estructura. Si esta sentencia est contenida dentro de una funcin, entonces la
estructura, llamada stbarco_usado, tiene un mbito local a esa funcin. Si la sentencia est contenida
fuera de todas las funciones de programa, la estructura tendr un mbito 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 declaracin de variable va antes del punto y coma final. Cuando se asocia slo una
variable con el tipo estructura, el campo etiqueta puede ser eliminado, por lo que sera posible escribir:
struct {
12
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;
Esta sentencia leer la marca del stbarco_usado en el arreglo de caracteres, mientras la prxima
sentencia imprimir el precio de venta de stbarco_usado en la pantalla.
srd::cout<< stbarco_usado.fprecioventa;
Ejemplo de estructuras:
/*
fractionStruct.cpp -
13
14
Cuando los parmetros son punteros (a fin de que desde dentro de la funcin no puedan ser modificados
los objetos referenciados). Ejemplo: int printf (const char *format, ...);
Cuando el argumento de la funcin sea una referencia, previniendo as que la funcin pueda modificar el
valor referenciado. Ejemplo: int dimen(const X &x2);
Ejercicios de programacin
1. El siguiente algoritmo es el mtodo de insercin para ordenar elementos en un arreglo:
2.
insertionSort(A)
3.
for j:=2 to length[A]
4.
do key:=A[j]
5.
-> Inserta el elemento A[j]
6.
-> en la secuencia ordenada A[1..j-1]
7.
i:=j-1
8.
while i>0 and A[i]>key
9.
do A[i+1]=A[i]
10.
i:=i-1
11.
A[i+1]:=key
1. desarrolle un programa en C/C++ del mtodo de insercin
2. ilustre cmo opera el algoritmo insertionSort(A) usando
A=<31,41,59,26,41,58>
12. Reescriba el programa y nmbrelo insertionSortNondec para que
orden decreciente
13. Considere el siguiente problema de bsqueda:
Input:
Una secuencia de
Output:
nmeros
y un valor
La pila
Uno de los conceptos ms tiles en las ciencias de la computacin es el de pila. En esta seccin
vamos a definir este concepto de manera abstracta y veremos cmo se usa para convertirse en una
herramienta concreta y de gran valor en las soluciones de problemas. La informacin contenida en esta
seccin se ha tomado de (TA83).
Definicin y ejemplos
Definicin 4 Una pila (stack) es una coleccin ordenada de elementos en la cual se pueden
insertar nuevos elementos por un extremo y se pueden retirar otros por el mismo extremo; ese extremo
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 la figura 9 el elemento F es el ms alto de todos
15
los elementos que estn en la pila. El elemento D es el ms alto de los elementos A,B,C, pero es menor
que los elementos E y F.
16
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 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 orden, adems de
insertar finalmente el elemento I que queda en la cima de la pila. Enseguida veremos con ms detalle las
operaciones bsicas de las pilas.
Operaciones bsicas
Las operaciones bsicas de una pila son:
1.
2.
3.
4.
La operacin push
Esta operacin sirve para insertar un elemento e en la pila S, lo vamos a escribir como:
push(S,e)
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 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:
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. El apuntador ``cima'' decrece
en una unidad.
La operacin stackempty
Esta operacin toma como argumento una estructura del tipo stack (pila) y devuelve un valor
booleano, devuelve un true si la pila est vaca y devuelve un false si la pila tiene al menos un
elemento, es decir:
18
La operacin stacktop
La operacin stacktop(S) devuelve el valor del elemento en la cima de la pila S. Para hacer esta
operacin escribiremos:
v=stacktop(S)
19
Empezamos con un contador iniciado en 0, y por cada push aumentamos un contador, y por cada
pop decrementamos el contador. Al final vemos el valor del contador, si el contador=0 entonces
terminamos con xito, de otro modo sealamos el error.
En la figura 13 se muestra la actividad de la pila a medida que se van agregando y quitando elementos.
C/C++
20
La representacin en
C/C++
En esta seccin veremos una implementacin de las cuatro operaciones bsicas 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 a objetos.
La operacin push
En la lnea (1) se observa que la operacin push recibe dos parmetros: la direccin 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 posicin 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.
La operacin pop
La operacin pop se escribe en forma de cdigo en C/C++ con la siguiente secuencia de rdenes:
(1) int pop(struct stack *S){
(2)
int valReturn;
(3)
(4)
valReturn=S->item[S->top];
(5)
S->top--;
(6)
return valReturn;
(7) }
La lnea (1) describe que esta funcin devuelve un tipo entero, el tipo de elementos guardados en
la pila; luego notamos que debemos dar slo la direccin de alguna variable de tipo estructura de pila
(struct stack *). Obtener la direccin se logra con el operador de indireccin (&).
Las lneas (4) y (5) hacen todo el trabajo de esta funcin, se almacena el valor que ser devuelto
en una variable de tipo entero y luego se decrementa el tope de la pila.
La operacin stackempty
21
El encabezado de la funcin que se muestra en la lnea (1) establece que se devuelve un valor
booleano, y que se debe dar un parmetro, que es la direccin de una localidad de memoria que almacena
una estructura de tipo pila. El objetivo de esta funcin es claro:
La lnea (3) establece la veracidad 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 vaco porque el manejo de arreglos en C/C++ empieza en el
ndice 0, que a diferencia de otros lenguajes como Pascal, empiezan en 1.
La operacin stacktop
Problemas de programacin
Los siguientes ejercicios deben ser resueltos en un progrma (en C/C++ ):
1. Expresiones entrefijas y prefijas. Las expresiones aritmticas pueden representarse de varias
maneras, una de ellas, la ms usual es la notacin entrefija.
La notacin entrefija establece que en medio de dos operandos se escribe un operador, como por
ejemplos:
1.
, donde los operandos son y , y el operador es el smbolo ;
Donde el parntesis ms interno establece la mayor prioridad, de
manera que primero se
debe evaluar
, luego
dando como resultado
.
, luego
y fimalmente
2.
No hay nada que hacer, pues es un operador unario.
En las expresiones prefijas se establece que el orden de escritura debe ser, primero el operador y
luego la lista de operandos:
3.
, donde los operandos son y , y el operador es el smbolo ;
22
4.
Lo primero que hay que hacer es tomar el primer 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
. Cada uno de los
operandos debe ser tratado de nuevo como una expresion en prefijo, 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
prefijo:
luego
y finalmente
y evaluar. Los parntesis
cuadrados son para ilustrar el ejemplo y no son necesarios para su evaluacin.
5.
No hay nada que hacer, pues es un operador unario.
Haga un programa en C/C++ que transforme expresiones de entrefijo a prefijo, y de prefijo a
entrefijo. Los caracteres vlidos son: las letras maysculas y minsculas, los nmeros enteros, los
parntesis normales, los cuatro operadores
y el operador unario
Colas
Definicin 5 Las colas son una estructura de datos similar a las pilas. Recordemos que las pilas
funcionan en un depsito 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
23
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: Dinmica 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
En la figura 15 se muestra una actividad tpica 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
es una cola y
Tericamente no hay lmite para el tamao de la cola, asi que siempre se debera poder insertar
elementos a una cola, sin embargo, al igual que las pilas, normalmente se deja un espacio de memoria
para trabajar con esta estructura. Por el contrario, la operacin
solamente se puede hacer si la
cola no est vaca.
C/C++
De manera similar a las pilas, las colas definen una estructura no estndar, de manera que se debe
crear un nuevo tipo de dado, el tipo cola, que debe tener los siguientes elementos:
Un arreglo de elementos de algn tipo especfico, puede incluso ser un tipo estndar o no.
Un nmero que indica el elemento que est en la posicin del frente de la cola.
Un nmero que indica el elemento que est en la posicin trasera de la cola.
Suponiendo que los elementos son nmeros 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.
24
Esta representacin con arreglos es completamente vlida, 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 ms elementos de lo que el arreglo puede almacenar, la operacin insert
podra quedar como:
void insert(struct cola *C, int e){
C->items[++C->rear]=e;
}
y al operacin x=remove(Q)
int remove(struct cola *C){
return C->items[C->front++];
}
y finalmente la operacin empty(Q):
bool empty(struct cola *C){
if(C->front>C->rear)
return true;
else
return false;
}
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 elementos. Esto es porque el elemento a retirar
puede estar en cualquier parte del arreglo.
Cuando se requiere eliminar un dato de una cola de prioridad se necesita verificar cada uno de los
elementos almacenados para saber cul 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 despus es necesario hacer una compactacin, reubicando los elementos en el frente de
la cola.
2. Cada supresin puede compactar el arreglo, cambiando los elementos depus del elemento
eliminado en una posicin y despus decrementando rear en 1. La insercin no cambia. En
promedio, se cambian la mitad de los elementos de una cola de prioridad para cada supresin, por
lo que esta operacin no es eficiente.
Ejercicio de programacin
1. Modifique los procedimientos de insertar, retirar y verificar-cola-vaca 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
un arreglo en C/C++ ? escriba un programa que maneje un deque, y que considere las cuatro
rutinas
o
o
o
o
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 que un deque vaco y que detectan desbordamiento
y subdesbordamiento.
3. Programe las colas de prioridad ascendente y descendente.
4. Existe un estacionamiento que tiene un slo carril que aloja hasta 10 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,
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 estn 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 nmero de placa. Se supone que los carros llegan y salen en el
orden especificado en la entrada. El programa debe imprimir (en la terminal estndar) un mensaje
cada vez que entra o sale un auto. Cuando llega un carro, el mensaje debe especificar si hay
espacio o no para l 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
26
que se movi el auto dentro del estacionamiento, incluyendo la salida misma, pero no la llegada.
Este nmero es 0 si el carro sale de la fila de espera.
Recursin
Un tema fundamental para los prximos temas es el de recursin. La recursin es muy importante
tanto en matemticas como en computacin, pues se usa recursin para definir procedimientos
autosimilares.
Definicin 6 Decimos que un objeto es recursivo si en su definicin se nombra a s mismo.
En programacin, una funcin es recursiva si en el mbito de esa funcin hay 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 es el factorial de un nmero.
es:
es decir, el producto de todos los nmeros enteros menores o guales que l, lo que se puede
resolver fcilmente con una funcin iterativa, esto es, una funcin con un ciclo que itere suficientes
veces, incrementando un valor y entonces ir almacenando en una variable el resultado de esas
multiplicaciones.
Una implementacin de esta definicin iterativa es:
(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 lnea (7). No hay ningn truco hasta aqu. La nica observacin
importante es en la lnea (2) en donde se declara el tipo long double para el valor del resultado, la razn
para tal accin es que el nmero factorial crece muy rpido y an con entradas en el rango de los
caracteres (hasta 255), el factorial es muy grande. Este procedimiento computacional no hace uso de
tcnicas especiales empleadas para tratar nmeros grandes.
Sin embargo una solucin ms elegante es usar la definicin recursiva, y esta es:
27
( 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 funcin, a diferencia
de la definicin iterativa en donde era suficiente trabajar en el programa principal. Esta funcin 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 versin iterativa en donde
emplebamos tipos diferentes. La razn es que al iniciar la recursin el argumento es del tipo devuelto,
as 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 funcin se
recupera una entrada de la pila. En la figura 16 ilustra cmo funciona la recursividad cuando se intenta
obtener el factorial(5).
La serie Fibonacci
Un poco de observacin es suficiente para encontrar que cualquier nmero (a partir del tercero de
la serie, o sea el segundo 1) es igual a la suma de los dos nmeros anteriores.
Daremos en primer lugar la versin iterativa. En este algoritmo deseamos encontrar el -simo
nmero de la serie Fibonacci. As si
el resultado del algoritmo debe ser ; si
el resultado
debe ser . La versin iterativa empieza desde los primeros 1's, sumndolos y encontrando el tercero,
luego para encontrar el cuarto nmero se suman el tercero (recin encontrado) y el segundo, y as en
adelante hasta encontrar el nmero buscado.
#include <iostream>
28
#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, 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 cierto es un trmino usado en computacin
para decir que ya no hay ms espacio disponible en la pila.
29
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, lo que significa 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:
1. El paso base: Esta es la clave para terminar la recursin, es cuando deja de hacer llamadas a la
funcin recursiva y hace evaluaciones devolviendo los resultados. En el ejemplo de la serie de
Fibonacci, el paso base est en la lnea ( 5). Adems se debe asegurar de que es posible entrar a
este paso.
2. El paso recursivo: Es la parte de la definicin que hace llamadas a esa misma funcin y que es la
causante de las inserciones en la pila, almacenando en cada una de las llamadas, informacin del
programa, del estado de sus variables locales y globales. En el mismo ejemplo de la serie
Fibonacci, el paso recursivo se muestra en la lnea ( 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;
}
Ejercicios de programacin
Los siguientes ejercicios deben de ser programados en C/C++ :
1. Bsqueda binaria: Considere un arreglo de elementos (nmeros enteros est bien) en el cual los
objetos ya estan ordenados, y se desea encontrar un elemento dentro de este arreglo. Es decir, se
desea realizar una ``bsqueda''.
La idea general de este mtodo de bsqueda binaria es:
o Si el arreglo tiene 1 elemento, se compara con el numero requerido y la bsqueda termina.
o Si el arreglo tiene ms de 1 elemento, tendremos que dividir en dos el arreglo y decidir en
qu 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 se
puede expresar como la suma de dos enteros menores
tales que
30
Listas
Hay dos desventajas serias con respecto a las estructuras estticas 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 solucin es usar listas. Las listas son estructuras de datos que son dinmicas, 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 desebamos cambiar su
contenido.
Antes de estudiar las listas, daremos una breve introduccin a los grafos, pues las listas son un
caso especial de los grafos.
Grafos
Los grafos son una manera visual de representar las relaciones.
Definicin 7 Si y
son dos conjuntos, decimos que
est relacionado con
si
es verdadera una sentencia
que considere a ambos elementos. Esta sentencia 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
, y si el predicado es falso, lo escribimos
como
es el conjunto de alumnos,
se puede leer:
es el conjunto de materias y
se lee ``
. En la figura 17 se puede
apreciar esto en forma de diagramas de Venn.
Si
es el conjunto de personas y
es tambin el conjunto de personas, y
es ``debe dinero
a'';
significa que ``marisol debe dinero a rafaelle'' y de ningn modo es
al contrario, es decir ``rafaelle no debe dinero a marisol''.
de personas y
de materias.
31
Donde
es un conjunto de aristas y
un conjunto no vaco de nodos. En el caso de
conjunto
es el conjunto de nodos y el conjunto de flechas es el conjunto de aristas.
, el
Supongamos ahora
y la siguiente relacin en
32
Figura: Relacin
de A en A
33
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 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 significa obtener un espacio de
memoria disponible y relacionarlo con los elementos de la lista; as mismo, eliminar un nodo de la lista
significa liberar la memoria que ocupa ese nodo sin perder la relacin con el resto de los nodos de la
lista.
Insertar un elemento al inicio de la lista. La operacin p=getnode(); obtiene un nodo vaco y
establece el contenido de una variable nombrada p en la direccin de este nodo, como se muestra en la
figura 22.a. Este nodo an no pertenece a alguna lista, simplemente se ha logrado dedicar un especio de
memoria que es apuntado por p, figura 22.b.
Figura 22: a) Creacin de un nuevo nodo. b) El nuevo nodo debe de ir insertado al frente, atrs o en medio de la
lista.
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 se ilustra en el siguiente ejemplo con el dato 6.
info(p)=6;
Esta operacin coloca el valor de lista (la direccin del primer nodo en la lista) en el campo
de node(p). Estos pasos se ilustran en la figura 23
siguiente
35
Figura 23: Operaciones involucradas en la insercin de un nuevo nodo al inicio de 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 modificarse en la direccin del nuevo
primer nodo de la lista. Esto se hace ejecutando la operacin
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;
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
36
Figura 24: Operaciones involucradas en la eliminacin de un nodo al inicio de una lista: c) p=list). d)
x=info(p). e) list=next(p)
Vamos a empezar una primera implementacin de listas usando arreglos, cada elemento del
arreglo debe ser un elemento compuesto. Cada elemento debe contener una parte para la informacin y
otra parte para apuntar al elemento siguiente:
#include <iostream>
( 1) #define numNodes 500
(
(
(
(
2) struct nodeType{
3)
int info;
4)
int next;
5) };
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 informacin 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
nmeros 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 ltimo nodo apunta a NULL, que se representa con el valor entero -1.
Tenemos tambin los siguientes elementos de cada nodo: node[p] corresponde a next(p), por la
37
notacin propia del lenguaje; tambin node[p].info para info(p) y finalmente node[p].next hace
referencia al nodo siguiente next(p).
Al principio todos los nodos estn 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:
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 est
completamente lleno. Esto significa que las estructuras de lista de un programa particular han
desbordado el espacio disponible. La funcin freeNode acepta un apuntador (nmero 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
parmetros. Primero se asegura que p no sea nulo y despus 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;
38
}
}
C/C++
Como sabemos, en lenguaje C/C++ , &x es la direccin donde se almacena en 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
asignar y liberar el almacenamiento en forma dinmica y cmo se accede al almacenamiento dinmico
en C/C++.
En C/C++ , una variable que debe contener la direccin en la memoria que almacena un nmero
entero se crea mediante la declaracin
int *p;
Recordemos que esta declaracin se divide en dos partes: la parte de tipo int *, que indica que
se trata de un apuntador a un entero; y la parte de identificador, en este caso p.
Una vez declarada la variable p como un apuntador a un tipo especfico de dato, debe ser posible
crear dinmicamente un objeto de este tipo especfico y asignar su direccin a p.
Esto se hace en C/C++ mediante la funcin de la biblioteca estndar malloc(size). La fucnin
asigna de manera dinmica una parte de memoria de tamao especificado en size y devuelve un
apuntador a un elemento de tipo char. Consideremos las siguientes declaraciones
malloc
La palabra clave extern especifica que una variable o funcin tiene un enlace externo. Esto
significa que la variable o funcin a la que nos referimos est definida en algn otro archivo fuente, o
ms adelante en el mismo archivo. 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 lenguaje para los
identificadores que se estn definiendo. Para los programas C++ la cadena por defecto es ``C++''.
39
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 devuelve el tamao en bytes de su
operando. Esto se usa para conservar la independencia de mquina. Despus, malloc crea un objeto de
este tamao. 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 clculo (int
*) (float *).
El operador sizeof, devuelve un valor de tipo int, en tanto que la funcin malloc espera un
parmetro de tipo unsigned. Para hacer que correspondan, debemos escribir
pi=(int *)malloc((unsigned)(sizeof(int)));
En la lnea (3), se crea una variable de tipo entero y su direccin se coloca en p. La lnea (4)
establece el valor de esa variable en 3. La lnea (5) hace que la direccin q sea la misma direccin que p.
El enunciado de la lnea (5) es perfectamente vlido, 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 nmero 7 dos veces.
La lnea (10) crea una nueva variable entera y coloca su direccin en p. Ahora *p hace referencia
a la variable entera recin 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 nica. Su
valor cambia conforme se modifica el valor de p. La lnea (11) establece el valor de esta variable recin
creada en 5 y la lnea 12 imprime los valores 5 y 7. Y as la salida del programa es:
40
3
7
5
3
7
7
Invalida cualquier referencia futura a la variable *p (a menos que se asigne nuevo espacio de
memoria a esa variable). Llamar free(p) hace que quede disponible para reso el almacenamiento
ocupado por *p, si es necesario.
La funcin free espera un parmetro apuntador del tipo char *, para que no tengamos
problemas de tipos, debemos hacer
free((char *)p);
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;
Un nodo de este tipo es igual a los nodos de la implementacin con arreglos, excepto que el
campo next es un apuntador y no un entero.
En lugar de declarar un arreglo, para que represente un conjunto acumulado de nodos, stos se
asignan y liberan segn es necesario. Se elimina la necesidad de un conjunto de nodos previamente
declarado.
Si declaramos
nodePtr p;
la ejecucin de la orden
p=getNode();
Los procedimientos insAfter y delAfter usan la implementacin dinmica de una lista ligada.
Supongamos que list es una variable apuntador que seala al primer nodo de una lista (si lo
hay) y es igual a NULL en el caso de una lista vaca.
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){
nodePtr q;
if((p==NULL)||(p->next==NULL)){
42
std::cout<<"Borrado prohibido\n";
} else{
q=p->next;
*px=q->info;
p->next=q->next;
freeNode(q);
}
}
Ejercicios de programacin
1. Implemente una pila usando memoria dinmica en listas ligadas. Implemente las operaciones
push, pop, empty y stackTop.
2. Implemente una cola usando memoria dinmica en listas ligadas. Implemente las operaciones
empty, insert y remove.
3. Desarrolle un programa para buscar un elemento en la lista (de nmeros enteros) y borrar la
primera ocurrencia de ese elemento.
4. Desarrolle un programa para buscar un elemento en la lista (de nmeros enteros) y borrar todas
las ocurrencias de ese elemento.
5. Las listas doblemente ligadas tienen nodos que estn divididos en tres segmentos:
1. Anterior: Un apuntador a un nodo
2. Info: La informacin de un nodo
3. Siguiente: Un apuntador a un nodo
Implemente
las
operaciones
borrarNodo(p),
insertarNodoAntes
e
insertarNodoDespues.
rboles
Los rboles son estructuras de datos tiles en muchas aplicaciones. Hay varias formas de rboles
y cada una de ellas es prctica en situaciones especiales, en este captulo vamos a definir algunas de esas
formas y sus aplicaciones.
rboles binarios
Definicin 10 Un rbol binario es una estructura de datos de tipo rbol en donde cada uno de
los nodos del rbol puede tener 0, 1, 2 subrboles llamados de acuerdo a su caso como:
43
El nivel de un nodo es el nmero de aristas que se deben recorrer para 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 ms uno.
La profundidad de un nodo es el mximo nivel de cualquier hoja en el rbol.
es
nodos
nodos
, el rbol contiene
. Dado que
hojas y, por tanto,
nodos
45
Figura 27: Comparacin de un rbol binario y un rbol binario casi completo. El rbol mostrado en (A)
no cumple la regla 2 de los rboles binarios casi completos.
Los nodos en un rbol binario (completo, casi completo o incompleto) se pueden enumerar del
siguiente modo. Al nodo raz le corresponde el nmero 1, al hijo izquierdo le corresponde el doble del
nmero asignado al padre y al hijo derecho le corresponde el doble ms 1 del nmero asignado al padre.
Operaciones con rboles binarios
Con los rboles binarios es posible definir algunas operaciones primitivas, estas operaciones son
en el sentido de saber la informacin de un nodo y sirven para desplazarse en el rbol, hacia arriba o
hacia abajo.
info(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 lgicas, 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.
46
Un rbol binario es una estructura de datos til cuando se trata de hacer 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 bastante til 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 elementos, se
deben hacer
comparaciones, claro, no es mucho problema si
es un nmero pequeo, pero el
problema se va complicando ms a medida que aumenta.
Si usamos un rbol binario, el nmero de comparaciones se reduce bastante, veamos cmo.
El primer nmero del arreglo se coloca en la raz del rbol (como en este ejemplo siempre vamos
a trabajar con rboles binarios, simplemente diremos rbol, para referirnos a un rbol binario) con sus
subrboles izquierdo y derecho vacos. Luego, cada elemento del arreglo se compara son la informacin
del nodo raz y se crean los nuevos hijos con el siguiente criterio:
Si el elemento del arreglo es igual que la informacin del nodo raz, entonces notificar duplicidad.
Si el elemento del arreglo es menor que la informacin del nodo raz, entonces se crea un hijo izquierdo.
Si el elemento del arreglo es mayor que la informacin del nodo raz, entonces se crea un hijo derecho.
Una vez que ya est creado el rbol, se pueden buscar los elementos repetidos. Si x el elemento
buscado, se debe recorrer el rbol del siguiente modo:
Sea k la informacin del nodo actual p. Si
en caso contrario, en caso de que
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;
47
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)
}
Para saber el contenido de todos los nodos en un rbol es necesario recorrer el rbol. Esto es
debido a que solo tenemos conocimiento del contenido de la direccin de un nodo a la vez. Al recorrer el
rbol es necesario tener la direccin de cada nodo, no necesariamente todos al mismo tiempo, de hecho
normalmente se tiene la direccin de uno o dos nodos a la vez; de manera que cuando se tiene la
direccin de un nodo, se dice que se visita ese nodo.
Aunque hay un orden preestablecido (la enumeracin de los nodos) no siempre es bueno recorrer
el rbol en ese orden, porque el manejo de los apuntadores se vuelve ms complejo. En su lugar se han
adoptado tres criterios principales para recorrer un rbol binario, sin que de omita cualquier otro criterio
diferente.
Los tres criterios principales para recorrer un rbol binario y visitar todos sus nodos son, recorrer
el rbol en:
preorden:
Se ejecutan las operaciones:
1. Visitar la raz
2. recorrer el subrbol izquierdo en preorden
3. recorrer el subrbol derecho en preorden
entreorden:
Se ejecutan las operaciones:
1. recorrer el subrbol izquierdo en entreorden
2. Visitar la raz
48
Al considerar el rbol binario que se muestra en la figura 28 usando cada uno de los tres criterios
para recorrer el rbol se tienen las siguientes secuencias de nodos:
En preorden:
En entreorden:
En postorden:
Esto nos lleva a pensar en otra aplicacin, el ordenamiento de los elementos 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 coincidencias.
El arreglo usado para crear el rbol binario de bsqueda fue
<14,15,4,9,7,18,3,5,16,4,20,17,9,14,5>
Para ordenar los elementos de este arreglo basta recorrer el rbol en forma de entreorden.
Cul sera el algoritmo para ordenarlo de manera descendente?
Los nodos de los rboles binarios son estructuras en C/C++ que estn compuestas por tres partes:
Un apuntador al subrbol izquierdo, left
Un apuntador al subrbol derecho, right
Una parte de informacin, que puede ser una estructura en s misma, info.
Adicionalmente es muy til poner un apuntador al padre del nodo. father.
y usando una representacin con memoria dinmica, los nodos de un rbol se puede representar
tambien con una estructura en C/C++ :
struct nodeType{
int info;
struct nodeType *left;
struct nodeType *right;
struct nodeType *father;
};
struct nodeType *nodePtr;
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);
}
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.
Recorridos de rbol binario en C/C++
Aqu usaremos recursividad para hacer estas rutidas de los recorridos de 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 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);
}
}
rboles
Hasta ahora hemos visto los rboles binarios que son aquellos rboles que sus nodos solamente
pueden tener un mximo de dos hijos. Cuando ocurre que los nodos tienen cualquier nmero finito de
hijos, son rboles (en general). De manera que
Definicin 12 Un rbol es un conjunto finito no vaco de elementos en el cual un elemento se
denomina la raz y los restantes se dividen en
subconjuntos disjuntos, cada uno de los cuales es
por s mismo un rbol. Cada elemento en un rbol se denomina un nodo del rbol
Un nodo sin subrboles es una hoja. Usamos los trminos padre, hijo, hermano, antecesor,
descendiente, nivel y profundidad del mismo modo que en los rboles binarios. El grado de un nodo
es en nmero mximo de hijos que algn nodo tiene.
51
Un rbol ordenado de define como un rbol en el que los subrboles de cada nodo forman un
conjunto ordenado. En un rbol ordenado, podemos hablar del primero, segundo o ltimo hijo de un
nodo en particular. El primer hijo de un nodo en un rbol ordenado se denomina con frecuencia el hijo
ms viejo de este nodo y el ltimo se denomina el hijo ms joven. Vase la figura 30. Un bosque es un
conjunto ordenado de rboles ordenados.
Al igual que en los rboles binarios, los nodos en un rbol tienen una parte de informacin, un
apuntador al padre y uno o ms apuntadores a los hijos. De manera que una solucin es crear una
estructura que incluya una lista dinmica de apuntadores, como lo muestra la figura 31.
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 joven, en lugar de dejarlo en null. Se podra usar un campo lgico adicional
para indicar si el campo next apunta al siguiente hijo ``real'' o al padre.
52
Si consideramos que son corresponde al apuntador left de un nodo de rbol binario y que next
corresponde a su apuntador right, este mtodo representa en realidad un rbol ordenado general
mediante un rbol binario.
Recorridos de rbol
Los mtodos de recorrido para rboles binarios inducen mtodos para recorrer los rboles en
general. Si un rbol se representa como un conjunto de nodos de variables dinmicas 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 rbol en los dems ordenes son similares. Estos recorridos tambin se
definen directamente as:
Orden previo:
similar al caso binario.
1. Visitar la raz
2. Recorrer en orden previo los subrboles de izquierda a derecha
Figura 32: Arriba: Un bosque de rboles. Abajo: El rbol binario que corresponde a ese bosque.
53
Para recorrer los nodos de un bosque, es preferible convertir todo el bosque en un rbol binario
correspondiente, como se ilustra en la figura 32. Cuando ya se tiene el rbol binario que corresponde a
ese bosque, entonces se aplican las rutinas ya conocidas.
Si el bosque es un bosque ordenado, es decir, que todos los rboles del bosque son rboles
ordenados; entonces un recorrido en entreorden dar como resultado una secuencia de nodos ordenada en
sentido ascendente.
Ejercicios de programacin
1. Escriba un programa que acepte un apuntador a un nodo y devuelva un valor verdadero si este
nodo es la raz de un rbol binario vlido y falso en caso contrario.
2. Escriba un programa que acepte un apuntador a un rbol binario y un apuntador a un nodo del
rbol, y devuelva el nivel del nodo en el rbol.
3. Escriba un programa para ejecutar el experimento siguiente: genere 100 nmeros aleatorios.
Conforme se genera cada nmero, insrtelo en un rbol de bsqueda binaria inicialmente vaco.
Despus de insertar los 100 nmeros, imprima el nivel de la hoja que tiene el nivel ms grande y
el nivel de la hoja que tiene el nivel ms chico. Repita este proceso 50 veces. Imprima una tabla
que indique cuntas veces de las 50 ejecuciones produjeron una diferencia entre el nivel de hoja
mximo y mnimo de 0,1,2,3, y as sucesivamente.
4. Implemente los recorridos de los rboles binarios.
5. Si un bosque se representa mediante un rbol binario, muestre que el nmero de vnculos
derechos nulos es 1 mayor que el nmero de no hojas del bosque.
Grafos
En esta parte del curso vamos a retomar la idea de los grafos. Hasta ahora hemos visto las listas y
los rboles 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 rboles, los nodos tienen una arista que llega (la del padre) y una o ms aristas que salen (los
hijos).
Como veremos ms 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 rboles son casos particulares
de los grafos.
54
Nodo incidente:
Si
como el nodo
Grado de incidencia:
Se define para cada nodo, y es su mximo numero de incidencias. Tambin se conoce con el
nombre de ``valencia''.
Grado interno:
Tambin se define para cada nodo y es el nmero de aristas que llegan a ese nodo. Otro nombre
para este trmino es ``valencia de entrada''.
Grado externo:
Para cada nodo es el nmero de aristas que salen del nodo. Se conoce tambin con el nombre de
``valencia de salida''.
Adyacencia:
Si
, el nodo
es adyacente al nodo
, entonces el nodo
nodo
si
no es adyacente al nodo
. Note que si
, pero
, pero el nodo
si es adyacente al
Sucesor:
Si el nodo
es adyacente al nodo
, entonces el nodo
.
55
Antecesor:
Si el nodo es adyacente al nodo , entonces el nodo es el antecesor del nodo .
Es posible asociar una etiqueta a cada arista, como se muestra en la figura 34. La etiqueta asociada con
cada arista se denomina peso.
donde
de pesos.
Con los grafos y grafos ponderados (los que tienen pesos) se pueden tener algunas operaciones
bsicas:
Con grafos:
join(a,b):
Agrega una relacin del nodo a al nodo b. Si la relacin no existe, entonces crea una relacin.
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 peso w. Si la relacin no existe, entonces
de crea la relacin y le asocia el 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 adyacente al nodo b, y
devuelve un valor FALSE en caso contrario.
tal que
y isAdjacent(n
,n
para todas
56
Aplicacin ejemplo
Supongamos el grafo ponderado de la figura 35, este grafo tiene como conjunto de nodos
y una relacin
Figura: Grafo
Se desea saber si existe un camino entre un par de nodos dado.
Bibliografa
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.
57