Está en la página 1de 74

Apuntes para el curso de

Estructuras de datos en C/C++


Dr. Abdiel E. Caceres Gonzalez
ITESM-CCM
2 de junio de 2005
Resumen
Una estructura de datos es una manera de almacenar y organizar datos para facilitar
el acceso y modicaciones. No hay una estructura de datos que sirva para todos los
propositos, y por eso es importante saber sus ventajas y desventajas. Este documen-
to es una coleccion de apuntes para el curso de Estructuras de Datos. Los apuntes
se han tomado de algunas fuentes que son detalladas en la seccion de bibliografa.

Indice
1. Preliminares de programacion en C/C++ 3
1.1. Arreglos 3
1.2. Apuntadores 10
1.3. Estructuras C/C++ 15
1.4. Ejercicios de programacion 19
2. La pila 21
2.1. Denicion y ejemplos 21
2.2. Operaciones basicas 24
2.3. Ejemplo: N umero de parentesis 25
2.4. La estructura de datos Pila en C/C++ 26
2.5. La representacion en C/C++ de las operaciones de una pila 27
2.6. Problemas de programacion 29
1
3. Colas 31
3.1. Estructura de las colas en C/C++ 32
3.2. Colas con prioridad 33
3.3. Ejercicio de programacion 34
4. Recursion 36
4.1. Peligros en la recursividad 39
4.2. Ejercicios de programacion 40
5. Listas 42
5.1. Grafos 42
5.2. Listas simplemente encadenadas 44
5.3. El uso de memoria dinamica en C/C++ 51
5.4. Listas ligadas usando memoria dinamica 54
5.5. Ejercicios de programacion 56
6.

Arboles 57
6.1. Concepto general de arbol 57
6.2.

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

Arboles 66
6.5. Ejercicios de programacion 69
7. Grafos 71
7.1. Recordatorio de las deniciones 71
7.2. Aplicacion ejemplo 73
2
1. Preliminares de programacion en C/C++
En esta seccion recordaremos tres temas de programacion en C/C++ que son
fundamentales para estudiar estructuras de datos; estos temas son los arreg-
los, los registros y los punteros. Los tres temas han sido tomados fundamen-
talmente de [MP97]
1.1. Arreglos
Denicion 1 Un arreglo se compone de elementos de igual tama no almace-
nados 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 denir 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
especcamente, b) un puntero constante -signica 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 identicador 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
La sintaxis de declaracion de arreglos es:
tipo nombre_arreglo [numero_de_elementos];
Los siguientes son dos ejemplos de declaraciones de arreglos validas en C/C++
:
int CoordenadasDePantalla[5]; /*Un arreglo de 5 enteros */
char IDCompania[20]; /*Un arreglo de 20 caracteres */
Figura 1. Arreglo CoordenadasDePantalla con ndices de desplazamiento valido
En la gura 1 se muestra el primer arreglo que fue declarado con el tipo
de n umeros enteros, llamado CoordenadasDePantalla, ocupa en memoria 5
localidades de memoria contiguas, cada una de ellas capaz de almacenar un
n umero entero. Actualmente es com un que los n umeros enteros sean de 32
bits, esto hace que el arreglo CoordenadasDePantalla ocupe 32 5 = 160
bits
No se permite utilizar nombres de variables dentro de los corchetes. Por esto no
es posible evitar la especicacion del tama no del arreglo hasta la ejecucion del
programa. La expresion debe ser un valor constante, para que el compilador
sepa exactamente cuanto espacio de memoria tiene que reservar para el arreglo.
Una buena practica de programacion es usar constantes predenidas.
4
#define Coordenadas_Max 20
#define Tamano_MaX_Compania_Id 15
int CoordenadasDePantalla[Coordenadas_Max];
char IDCompania[Tamano_MaX_Compania_Id];
El uso de constantes predenidas garantiza que futuras referencias al arreglo
no excedan el tama no del arreglo denido.
1.1.2. Iniciacion del arreglo
C/C++ proporciona 3 maneras de iniciar elementos del arreglo:
Por defecto: Cuando son creados, se aplica solamente a arreglos globales y
estaticos.
Explcita: Cuando son creados, suministrando datos de iniciacion
Tiempo de ejecucion: Durante la ejecucion del programa cuando se asig-
nan o copias datos en el arreglo.
1.1.3. Acceso a los elementos de un arreglo
Si se tiene un error cuando se utilizan arreglos en C/C++ , de seguro el error
involucra el acceso a los elementos del arreglo, por la simple razon de que
el primer elemento esta en una posicion 0, no 1. De manera que el ultimo
elemento del arreglo lo encontramos en n-1, donde n es el n umero de elementos.
Supongamos la siguiente declaracion:
int Estado[Rango_Maximo_Estado]={-1,0,1};
La siguiente sentencia tiene acceso a -1:
Estado[0];
Si escribimos Estado[3] causara un error porque no hay 4 elementos.
1.1.4. Calculo del tama no de un arreglo (sizeof())
Es frecuente utilizar el operador sizeof() para calcular la cantidad de espacio
que se necesita almacenar para un objeto:
/*
* exploresz.cpp
*/
#include<iostream.h>
5
#define maxDiasSemana 7
int main(void){
int desplazamiento, maxHorasDiarias[maxDiasSemana];
cout<<"sizeof(int) es"<<(int)sizeof(int)<<"\n\n";
for(desplazamiento=0;desplazamiento<maxDiasSemana;
desplazamiento++)
cout<<"&maxHorasDiarias["
<<desplazamiento
<<"]="
<<&maxHorasDiarias[desplazamiento]<<"\n";
return 0;
}
1.1.5. Arreglos multidimensionales
El termino dimension representa el n umero dendices 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 las y 5 columnas por la, haciendo un total de 20 elementos
enteros.
Los arreglos multidimensionales son almacenados de forma lineal en la memo-
ria de la computadora. Los elementos en los arreglos multidimensionales estan
agrupados desde el ndice mas a la derecha hacia el centro. En el ejemplo an-
terior, la 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. Arreglos como argumentos de funciones
Es necesario recordar tres cosas al pasar arreglos como parametros de fun-
ciones:
1. Todos los arreglos son pasados en llamada-por referencia.
2. Debido a que el arreglo es pasado en llamada por referencia, sera in-
correcto para la funcion llamada devolver el arreglo en una sentencia
return();. Esta sentencia esta de mas.
3. Todos los elementos del arreglo son pasados a las funciones en llamada por
valor. lo que signica que se pasa una copia del elemento, no la direccion
del elemento.
/*
// ereArray.xcode
*/
#include <iostream>
#include <ctype.h>
#define maxArray 5
void ArrayMayuscula(char Array[maxArray]);
int main (int argc, char * const argv[]) {
int desplazamiento;
char Array[maxArray]=
{a,e,i,o,u};
for(desplazamiento=0;desplazamiento<maxArray;
desplazamiento++)
std::cout<<Array[desplazamiento];
std::cout<<"\n";
ArrayMayuscula(Array);
for(desplazamiento=0;desplazamiento<maxArray;
desplazamiento++)
std::cout<<Array[desplazamiento];
return 0;
}
void ArrayMayuscula(char Array[maxArray])
{
for(int desplazamiento=0;desplazamiento<maxArray;
desplazamiento++)
Array[desplazamiento]=toupper(Array[desplazamiento]);
//Aqui return array seria incorrecto
}
8
La salida del programa demuestra que el arreglo se pasa en llamada por refer-
encia, 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
modicacion 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
Denicion 2 Un apuntador es una variable que contiene una direccion 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
Figura 2. contenidoRAM se asigna a la localidad de memoria con direccion 7751
En la gura 2 se ilustra el nombre de la variable contenidoRAM y se observa
que se encuentra en la direccion 7751 de la memoria. El contenido de esta
localidad no se muestra. Una variable que contiene una direccion, tal como
direccionRAM, se llama variable apuntador o simplemente apuntador.
Despues que la sentencia anterior se ejecuta, la direccion de contenidoRAM
sera asignada a la variable apuntador direccionRAM. La relacion se expresa
diciendo que direccionRAM apunta a contenidoRAM. La gura 3 ilustra esta
relacion.
El accceso al contenido de una celda cuya direccion esta almacenada en la
variable direccionRAM es tan sencillo como poner al inicio de la variable
apuntador un asterisco: *direccionRAM. Lo que se ha hecho es eliminar la
referencia directa. Por ejemplo, si se ejecutan las siguientes dos sentencias, el
valor de la celda llamada contenidoRAM sera de 20 (vease la gura 4).
10
Figura 3. Notacion de echa para los apuntadores
direccionRAM = &contenidoRAM;
*direccionRAM = 20;
Figura 4. A contenidoRAM se le asigna el valor entero 20
1.2.1. Declaraciones de variables apuntador
C/C++ requiere una denicion para cada variable. Para denir 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 identicador para la variable es
direccionRAM
El asterisco que sigue a int signica apuntador a. Esto es, el siguiente tipo
de dato es una variable apuntador que puede contener una direccion a un int:
int *
11
En C/C++ una variable apuntador contiene la direccion de un tipo de dato
particular:
char *direccion_char;
char *direccion_int;
El tipo de dato de direccion char es diferente del tipo de dato de la variable
apuntador direccion int. En un programa que dene un apuntador a un
tipo de dato y utliza este para apuntar a otro tipo de dato, pueden ocurrir
errores en tiempo de ejecucion y advertencias en tiempo de compilacion. Una
practica de programacion pobre sera denir un apuntador de una forma y
luego utilizar este de alguna otra forma. Por ejemplo:
int *direccion_int;
float un_float = 98.34;
direccion_int = &un_float;
1.2.2. Utilizacion de punteros en sentencias sencillas
Veamos el siguiente ejemplo:
/*
// changeVals.xcode
*/
(01) #include <iostream>
(02)
(03) int main (int argc, char * const argv[]) {
(04) int A_int=15, B_int=37, Temp_int;
(05) int *direccion_int;
(06)
(07) std::cout<<"El contenido de A_int es:"<<A_int<<"\n";
(08) std::cout<<"El contenido de B_int es:"<<B_int<<"\n";
(09) direccion_int = &A_int;
(10) Temp_int = *direccion_int;
(11) *direccion_int = B_int;
(12) B_int = Temp_int;
(13) std::cout<<"Despues del intercambio:"<<"\n\n";
(14)
(15) std::cout<<"El contenido de A_int es:"<<A_int<<"\n";
(16) std::cout<<"El contenido de B_int es:"<<B_int<<"\n";
(17) return 0;
(18) }
12
En la lnea (04) se han declarado tres variables de tipo entero, se da a cada
celda un nombre y se inicializan 2 de estas. Supondremos que la direccion de
memoria asignada para la variable A int es la direccion 5328, y la direccion
en memoria RAM asignada para la variable B int es la direccion 7916, y la
celda llamada Temp int se le ha asignado la direccion 2385. Vease la gura 5;
Figura 5. Descripcion de las tres variables en la memoria
En la lnea (05) se dene un apuntador a un tipo de dato entero llamado
direccion int. La sentencia asigna la celda y da a esta un nombre.
Luego, en la lnea (09), la tercera sentencia asigna a direccion_int la direc-
cion de A_int (gura 6).
Figura 6. direccion int dada la direccion de A int
La lnea (10) utiliza la expresion *direccion_int para acceder al contenido
de la celda a la cual apunta direccion_int:
Temp_int = *direccion_int;
Por consiguiente, el valor entero 15 se almacena en la variable Temp_int. Si
no se pone el * enfrente de direccion_int;, la sentencia de asignacion al-
macenara ilegalmente el contenido de direccion_int en la celda nombrada
Temp_int, pero se supone que Temp_int contiene un entero, no una direccion.
13
Este puede ser un error muy difcil de localizar puesto que muchos compi-
ladores no emiten ninguna advertencia/error.
Para empeorar el asunto, la mayora de los apuntadores son cercanos, lo que
signica 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(gura 7):
*direccion_int = B_int;
Figura 7. Se copia el contenido de B int usando la notacion de echa de apuntadores
La ultima sentencia en la lnea (12) simplemente copia el contenido de una
variable entera, Temp int en otra variable entera B int (gura 8
Figura 8. Se copia Temp int en B int utilizando asignacion normal.
Debemos de asegurarnos de comprender la diferencia entre que se referencia
cuando una variable puntero esta precedida por el operador de indireccion y
cuando no esta precedida por este operador.
Para este ejemplo, la primera sintaxis es un apuntador a una celda que puede
contener un valor entero. La segunda sintaxis referencia la celda que contiene
la direccion de otra celda que puede contener un entero.
14
1.2.3. Utilizacion incorrecta del operador de direccion
No se puede utilizar el operador de direccion sobre toda expresion C/C++ . El
siguiente ejemplo demuestra aquellas situaciones donde no se puede aplicar el
operador de direccion &.
puedeAlmacenarDireccionDeConstante = &37;
int RAM_int = 5;
puedeAlmacenarDireccionDeExpresionTemp = &(RAM_int +15);
puedeAlmacenarDireccionDeRegistro = &varRegistro;
La primera sentencia trata de obtener ilegalmente la direccion de un valor
constante integrado. La sentencia no tiene sentido puesto que 37 no tiene una
celda de memoria asociada con este.
La segunda sentencia de asignacion intenta devolver la direccion de la expre-
sion RAM_int+15. No existe direccion asociada con la expresion puesto que la
expresion en s misma es realmente un proceso de manipulacion de pila.
Normalmente, el ultimo ejemplo respeta la demanda del programador para
denir varRegistro como un registro mas que como una celda de almace-
namiento en la memoria interna. Por consiguiente, no podra devolverse y
almacenarse la direccion de celda de memoria. El compilador C/C++ da la
memoria de variable, no el almacenamiento de registro.
1.3. Estructuras C/C++
Denicion 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. Sintaxis y reglas para estructuras en C/C++
En C/C++ se forma una estructura utilizando la palabra reservada struct,
seguida por un campo etiqueta opcional, y luego una lista de miembros dentro
de la estructura. La etiqueta opcional se utiliza para crear otras variables del
tipo particular de la estructura:
struct campo_etiqueta{
tipo_miembro miembro_1;
tipo_miembro miembro_2;
15
tipo_miembro miembro_3;
:
:
tipo_miembro miembro_n;
};
Un punto y coma naliza la denicion 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 dene stbarco_usado de tipo struct stbarco. La declaracion
requiere el uso del campo etiqueta de la estructura. Si esta sentencia esta con-
tenida dentro de una funcion, entonces la estructura, llamada stbarco_usado,
tiene un ambito local a esa funcion. Si la sentencia esta contenida fuera 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 nal. 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. Utilizacion de miembros de estructuras
Para accesar a los miembros de las estructuras se usa el punto u operador
miembro (.). La sintaxis es:
estructuraNombre.miembroNombre
Por ejemplo en:
gets(stbarco_usado.szmodelo);
Aqu, stbarco_usado es el nombre asociado con la estructura, y szmodelo es
una variable miembro de la estructura, otro ejemplo:
std::cin>> stbarco_usado.sztipo;
Esta sentencia leera la marca del stbarco_usado en el arreglo de caracteres,
mientras la proxima sentencia imprimira el precio de venta de stbarco_usado
en la pantalla.
srd::cout<< stbarco_usado.fprecioventa;
Ejemplo de estructuras:
/* fractionStruct.cpp -
Programa para demostrar el uso de los
tipos Struct en C++, este tipo de datos
es util para los programadores para crear
sus propias estructuras de tipos.
*/
#include <iostream>
using namespace std;
// Definimos un nuevo tipo de estructura llamada Fraction
// como la definicion se puso antes del "main"
// los tipos Fraction se pueden usar como prototipos
17
struct Fraction {
// declaramos sus dos miembros
int numerator;
int denominator;
}; // Note el punto y coma al final
// funciones prototipos
void getFraction(Fraction &f);
void printFraction(const Fraction &f);
int main (int argc, char * const argv[])
{
// declaramos variables de tipo Fraction
Fraction f1, f2;
// obtenemos dos fracciones y las desplegamos
getFraction(f1);
cout << "\nf1 = ";
printFraction(f1);
getFraction(f2);
cout << "\nf2 = ";
printFraction(f2);
cout << endl;
return 0;
}
// pedimos al usuario los valores del denominador y numerador
// los almacenamos en su adecuado lugar en la estrcututra; checamos si
// el valor del denominador es valido y lo ponemos en 1 si no lo es.
void getFraction(Fraction &f) {
cout << "\nEnter the numerator: ";
cin >> f.numerator;
cout << "Enter the denominator: ";
cin >> f.denominator;
if (f.denominator == 0) {
cout << "\nIllegal denominator! Denominator is being set to 1.\n";
f.denominator = 1;
}
}
// imprimimos la fraccion
void printFraction(const Fraction &f) {
cout << f.numerator << "/"
18
<< f.denominator << "\n";
}
Nota sobre las funciones prototipos:
Las funciones prototipo tienen los siguientes usos importantes:
Establecen el tipo devuelto para las funciones que devuelven otros tipos
diferentes que int. Aunque las funciones que devuelven valores enteris no
necesitan prototipos, se recomienda tener prototipos.
Sin prototipos completos, se hacen las conversiones estandares, pero no se
checan los tipos o los n umeros de argumentos con el n umero de parametros.
Los prototipos se usan para inicializar apuntadores a funciones, antes de
que las funciones sean denidas.
La lista de parametros se usa para checar la correspondencia de los argu-
mentos en la llamada a la funcion con los parametros en la denicion de la
funcion
const en parmetros de funciones
El especicador const puede ser utilizado en la denicion de parametros de
funciones. Esto resulta de especial utilidad en tres casos. En los tres el n
que se persigue es el mismo: indicar que la funcion no podra cambiar dichos
argumentos:
Con parametros de funciones que sean de tipo matriz (que se pasan por
referencia). Ejemplo: int strlen(const char[]);
Cuando los parametros son punteros (a n de que desde dentro de la funcion
no puedan ser modicados los objetos referenciados). Ejemplo: int printf
(const char *format, ...);
Cuando el argumento de la funcion sea una referencia, previniendo as que la
funcion pueda modicar el valor referenciado. Ejemplo: int dimen(const
X &x2);
1.4. Ejercicios de programacion
1. El siguiente algoritmo es el metodo de insercion para ordenar elementos
en un arreglo:
insertionSort(A)
for j:=2 to length[A]
do key:=A[j]
-> Inserta el elemento A[j]
-> en la secuencia ordenada A[1..j-1]
i:=j-1
19
while i>0 and A[i]>key
do A[i+1]=A[i]
i:=i-1
A[i+1]:=key
a) desarrolle un programa en C/C++ del metodo de insercion
b) ilustre como opera el algoritmo insertionSort(A) usando como en-
trada el arreglo A=<31,41,59,26,41,58>
2. Reescriba el programa y nombrelo insertionSortNondec para que or-
dene los elementos en orden decreciente
3. Considere el siguiente problema de b usqueda:
Input: Una secuencia de n n umeros A = a
1
, a
2
, . . . , a
n
y un valor v.
Output: Un ndice i tal que v = A[i] o el valor espacial NIL si v no
ocurre en A.
Escriba un programa que resuelva este problema de b usqueda.
4. Considere el problema de sumar dos n umeros binarios de longitud n.
Cada n umero se almacena en uno de los arreglos A y B de tama no n. La
suma se almacena en un arreglo C de tama no n + 1, tambien como un
n umero binario. Escriba un programa que resuelva este problema.
20
2. La pila
Uno de los conceptos mas utiles en las ciencias de la computacion es el de pila.
En esta seccion vamos a denir este concepto de manera abstracta y veremos
como se usa para convertirse en una herramienta concreta y de gran valor en
las soluciones de problemas. La informacion contenida en esta seccion se ha
tomado de [TA83].
2.1. Denicion y ejemplos
Denicion 4 Una pila (stack) es una coleccion 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 gura 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.
Figura 9. Pila con 6 elementos
Para describir como funciona esta estructura, debemos agregar un nuevo ele-
mento, el elemento G. Despues de haber agregado el elemento G a la pila, la
nueva conguracion es la que se muestra en la gura 10.
De acuerdo con la denicion, 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
Figura 10. Operacion de insertar el elemento G en la pila P
Cuando se desea retirar un elemento de la pila, solo basta ordenar que 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 congu-
racion global de la pila es como se muestra en la gura 11
Figura 11. Operacion de retirar de la pila P
El concepto de pila es muy importante en computacion y en especial en teora
de lenguajes de programacion. En lenguajes procedurales como Pascal o C, la
pila es una estructura indispensable, debido a las llamadas a funcion.
Resulta que el ujo de instrucciones va de arriba hacia abajo, y cuando ocurre
una llamada a alguna funcion, el estado global del sistema se almacena en un
registro y este en una pila. As que la pila va a contenr todas las llamadas a
procedimientos que se hagan.
22
Cuando se termina de ejecutar alg un procedimiento, se recupera el registro que
esta en la cima de la pila. En ese registro estan los valores de las variables como
estaban antes de la llamada a la funcion, o algunas pueden haber cambiado si
valor, dependiendo del ambito de las variables.
Cada elemento en la pila que es retirado, signica que se ha terminado de
ejecutar alguna funcion. Cuando se termina de ejecutar el programa, la pila
de llamadas a subprogramas debe haber quedado en 0 tambien, de otro modo
podra causar algun tipo de error.
Esto nos lleva a pensar en otras utilidades de la pila. La pila sirve para en-
contrar errores.
La din amica de la pila, es decir, la manera en como entran los datos a la
estructura de datos y como salen, se denomina fo, que viene del inges rst
in rst out (primero en entrar, primero en salir).
Figura 12. Dinamica de la pila P
En la gura 12 se muestran fotografas en distintos momentos de la pila,
cuando se desea insertar H justo debajo de F. Para hacer esto se requiere,
retirar tantos elementos como sean necesarios, aqu se han retirado de la cima
G y F para luego insertar H, que quedara posteriormente debajo de F.
Lo que sucede es que, cuando se retira el elemento G se debe hacer una evalu-
acion para determinar si el elemento retirado es el elemento objetivo, en este
caso el elemento objetivo es F, puesto que se desea insertar un elemento debajo
de F.
Despues de haber insertado F, insertamos de nuevo los elementos F y G en ese
orden, ademas de insertar nalmente el elemento I que queda en la cima de la
pila. Enseguida veremos con mas detalle las operaciones basicas de las pilas.
23
2.2. Operaciones basicas
Las operaciones basicas de una pila son:
1. En la pila S, insertar un elemento e: push(S,e),
2. Retirar un elemento de la pila S: pop(S),
3. Vericar si la pila S esta vaca: stackempty(S) y
4. Saber cual es el elemento en la cima de la pila S: stacktop(S).
enseguida cada una de estas operaciones:
2.2.1. La operacion push
Esta operacion sirve para insertar un elemento e en la pila S, lo vamos a
escribir como:
push(S,e)
Despues de hacer esta operacion sucede que:
El elemento en la cima de la pila S ahora es e
2.2.2. La operacion pop
Para retirar un elemento de la pila S y asignarlo a una variable del mismo 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
un false si la pila tiene al menos un elemento, es decir:
stackempty(S) =

true si S tiene 0 elementos


false si S tiene mas de 0 elementos
2.2.4. La operacion stacktop
La operacion stacktop(S) devuelve el valor del elemento en la cima de la pila
S. Para hacer esta operacion escribiremos:
v=stacktop(S)
las implicaciones de usar esta operacion son:
Se hace una copia del elemento que esta en la cima
En realidad se hacen dos operaciones, primero se hace v=pop(S), luego un
push(S,v), porque despues de la operacion stacktop, la pila S queda sin
cambio alguno.
2.3. Ejemplo: N umero de parentesis
Supongamos ahora la expresion ((5+6)*4)/(17+9), una de las condiciones
para que sea una expresion aritmetica correcta en que tengas sus parentesis
balanceados, as que deseamos saber si el n umero de parentesis que abres es
el mismo n umero de parentesis que cierran.
Para resolver este problema usaremos el concepto de pila. La idea es simple.
Vamos a leer cada elemento de la expresion, si se trata de un parentesis que
abre, entonces lo insertaremos en una pila; si se trata de un parentesis que
cierra, entonces sacamos un elemento de la pila. Al terminar de leer la expre-
sion revisaremos si la pila esta vaca, en cuyo caso habremos concludo que el
n umero de parentesis que abre es el mismo que el n umero de parentesis que
cierra y la expresion tiene parentesis balanceados.
Veamos como funciona:
( : push(S,()
( : push(S,()
5 : nada que hacer
+ : nada que hacer
6 : nada que hacer
) : v=pop(S)
25
* : nada que hacer
4 : nada que hacer
) : v=pop(S)
/ : nada que hacer
( : push(S,()
17: nada que hacer
+ : nada que hacer
9 : nada que hacer
) : v=pop(S)
Empezamos con un contador iniciado en 0, y por cada push aumentamos un
contador, y por cada pop decrementamos el contador. Al nal vemos el valor
del contador, si el contador=0 entonces terminamos con exito, de otro mod
se nalamos el error.
En la gura 13 se muestra la actividad de la pila a medida que se van agregando
y quitando elementos.
Figura 13. Evaluacion del balance de parentesis en una expresion aritmetica
2.4. La estructura de datos Pila en C/C++
Una pila esta conformada por dos elementos:
Un espacio sucientemente grande para almacenar los elementos insertados
en la pila
Una parte que nos se nale cual es el elemento en la cima de la pila.
Estas partes las conformamos en una estructura, descrita como sigue:
definir numero maximo de elementos en la pila
definir nuevo tipo estructura llamado "stack" con
item : un arreglo de 1 a maximos elementos enteros
top : un numero de 0 a maximos elementos
26
fin de la nueva estructura
Facilmente podemos describir un codigo en C/C++ que represente lo anterior-
mente propuesto.
// En la parte de definiciones
#define maxElem 100
// En la parte de tipos
struct stack {
int item[maxElem];
int top;
};
// En la parte de variables
struct stack A;
2.5. La representacion en C/C++ de las operaciones de una pila
En esta seccion veremos una implementacion de las cuatro operaciones basicas
de las pilas. Todas estas operaciones se han hecho desde un punto de vista de
programacion funcional, sin duda se pueden describir en un modelo orientado
a objetos.
2.5.1. La operacion push
El siguiente segmento de codigo ilustra como se puede implementar la op-
eracion insertar un elemento en una pila. Hemos supuesto que la pila ya
esta denida 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
direcci on 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 n 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
La operacion pop se escribe en forma de codigo en C/C++ con la siguiente
secuencia de ordenes:
(1) int pop(struct stack *S){
(2) int valReturn;
(3)
(4) valReturn=S->item[S->top];
(5) S->top--;
(6) return valReturn;
(7) }
La lnea (1) describe que esta funcion devuelve un tipo entero, el tipo de
elementos guardados en la pila; luego notamos que debemos dar solo la direc-
cion de alguna variable de tipo estructura de pila (struct stack *). Obtener la
direcci on se logra con el operador de indireccion (&).
Las lneas (4) y (5) hacen todo el trabajo de esta funcion, se almacena el valor
que ser a devuelto en una variable de tipo entero y luego se decrementa el tope
de la pila.
2.5.3. La operacion stackempty
La operacion stackempty se describe en el siguiente segmento de codigo:
(1) bool stackempty(struct stack *S){
(2) bool valorDevuelto;
(3) if(S->top== -1)
(4) valorDevuelto=true;
(5) else
(6) valorDevuelto=false;
(7) return valorDevuelto;
(8) }
El encabezado de la funcion que se muestra en la lnea (1) establece que
se devuelve un valor booleano, y que se debe dar un parametro, que es la
direcci on de una localidad de memoria que almacena una estructura de tipo
pila. El objetivo de esta funcion es claro:
La lnea (3) establece la verdacidad o falsedad del predicado (S->top==-1),
determinando si el nivel del tope es igual que -1, en cuyo caso devuelve un
verdadero (4), de otro modo ha de devolver un valor falso (6). Se ha establecido
un -1 como 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.
28
2.5.4. La operacion stacktop
Este es un caso especial porque no se requiere hacer ning un codigo.
Esta funcion debe devolver un n umero entero y dejar la pila sin cambio. Para
lograr esto se debe hacer un pop(&A), mostrar el elemento y luego insertar de
nuevo el elemento en la pila haciendo un push(&A,elemento), notemos que se
han usado los operadores de direccion para dar la direccion de la variable que
alberga una estructura de tipo pila. El siguiente segmento de codigo ilustra
como se han usado las funciones antes creadas, por supuesto que se pueden
separar y crear una nueva funcion que haga lo mismo:
...
(1) case 4:{
(2) if(not stackempty(&A)){
(3) valor=pop(&A);
(4) std::cout<<"La cima de la pila es: "<<valor<<"\n";
(5) push(&A,valor);
(6) } else
(7) std::cout<<"La pila esta vacia";
(8) break;
(9) }
...
2.6. Problemas de programacion
Los siguientes ejercicios deben ser resueltos en un programa (en C/C++ ):
1. Expresiones entrejas y prejas. Las expresiones aritmeticas pueden
representarse de varias maneras, una de ellas, la mas usual es la notacion
entreja.
La notacion entreja establece que en medio de dos operandos se escribe
un operador, como por ejemplos:
a) a b, donde los operandos son a y b, y el operador es el smbolo ;
b) 2 +5 ((5 +7)/4) Donde el parentesis mas interno establece la may-
or prioridad, de manera que primero se debe evaluar (5 + 7), luego
(12/4), luego 2+(53) y malmente (2+15), dando como resultado
17.
c) 1 No hay nada que hacer, pues es un operador unario.
En las expresiones prejas se establece que el orden de escritura debe
ser, primero el operador y luego la lista de operandos:
a) ab, donde los operandos son a y b, y el operador es el smbolo ;
b) +2 5/ + 574 Lo primero que hay que hacer es tomar el primer
29
operador y tomar los operandos necesarios siguientes (dos si se trata
de un operador binario y uno si es un operador unario). En este caso
se trata de evaluar 2 + [5/ + 574]. Cada uno de los operandos debe
ser tratado de nuevo como una expresion en prejo, de manera que
se repite lo anterior, tomar el operador y la lista de sus operandos y
tratar cada uno de sus operandos como expresiones en prejo: 2 +
[5 [/ +574]], luego 2 +[5 [[+57]/4]] y nalmente 2 +[5 [[5 +7]/4]]
y evaluar. Los parentesis cuadrados son para ilustrar el ejemplo y no
son necesarios para su evaluacion.
c) 1 No hay nada que hacer, pues es un operador unario.
Haga un programa en C/C++ que transforme expresiones de entrejo
a prejo, y de prejo a entrejo. Los caracteres validos son: las letras
may usculas y min usculas, los n umeros enteros, los parentesis normales,
los cuatro operadores (+, , , /, ) y el operador unario ().
Figura 14. Ilustracion del estacionamiento mencionado en el problema 2
2. en cierto punto de la ciudad hay un estacionamiento como el que se
muestra en la gura 14, en donde hay lugar para 9 vehculos. haga un
programa que muestre el manejo de este estacionamiento, considerando
los siguientes requisitos:
a) Los vehculos proporcionan la siguiente informacion: Placas (6 digi-
tos), Estado (2-3 caracteres, p.e. SON, DF, CHI, YUC), Marca, Mod-
elo, A no-Modelo, Nombre del propietario.
b) Al llegar un veculo se acepta solamente si hay lugar disponible.
c) Validar todas las operaciones de la pila.
d) En cualquier momento se puede sacar alg un vehculo del estacionamien-
to, regresando los vehculos en el orden en que estaban.
e) Toda la corrida del programa debe hacerse hacia/desde la terminal
estandar.
3. Haga un programa que implemente 2 pilas en 1 arreglo A[1..n] de man-
era que ninguna pila se desborde a menos que el n umero de elementos en
ambas pilas sea n
30
3. Colas
Denicion 5 Las colas son una estructura de datos similar a las pilas. Recorde-
mos que las pilas funcionan en un deposito en donde se insertan y se retiran
elementos por el mismo extremo. En las colas sucede algo diferente, se inser-
tan elementos por un extremo y se retiran elementos por el otro extremo. De
hecho a este tipo de dispositivos se les conoce como dispositivos fo (rst
in, rst 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
En la gura 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 Q es una cola y x es un elemento, se pueden hacer tres operaciones basicas
con las colas:
1. insert(Q,x), que inserta el elemento x en la parte trasera de la cola Q.
2. x=remove(Q), que almacena en x el valor del elemento retirado de la parte
frontal de la cola Q.
3. empty(Q), que es un predicado de valor booleano, y es verdadero cuando
la cola Q tiene 0 elementos, y es falso cuando la cola Q tiene al menos un
elemento, en cuyo caso, ese unico elemento es la parte frontal y la parte
trasera de la cola al mismo tiempo.
31
Teoricamente no hay lmite para el tama no 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 operacion remove solamente se puede hacer si
la cola no esta vaca.
3.1. Estructura de las colas en C/C++
De manera similar a las pilas, las colas denen 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 especco, 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 nalmente la operacion empty(Q):
bool empty(struct cola *C){
if(C->front>C->rear)
return true;
else
return false;
}
3.2. Colas con prioridad
Una cola con prioridad es una estructura de datos en la que se ordenan 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
vericar 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 eciente.
3.3. Ejercicio de programacion
1. Modique los procedimientos de insertar, retirar y vericar-cola-vaca
para que considere aprovechar los espacios dejados al retirar elementos.
2. Un deque es un conjunto ordenado de elementos del cual pueden elimi-
narse elementos en cualquier extremo y en el cual pueden insertarse ele-
mentos en cualquier extremo. Llamemos a los dos extremos de un deque
left (izquierdo) y right (derecho). ?como se representa un deque en
un arreglo en C/C++ ? escriba un programa que maneje un deque, y que
considere las cuatro rutinas
removeLeft
removeRight
insertLeft
insertRight
para remover e insertar elementos en los extemos izquierdo y derecho de
un deque. Aseg urese 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 solo carril que aloja hasta 10
34
carros. Los autos llegan por el extremo sur del estacionamiento y salen por
el extremo norte del mismo. Si llega un cliente para recoger un carro que
no 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 especicado 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 especicar 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 la de espera.
35
4. Recursion
Un tema fundamental para los proximos temas es el de recusrion. La recursion
es muy importante tanto en mateaticas como em computacion, pues se usa
recursion para denir procedimientos autosimilares.
Denicion 6 Decimos que un objeto es recursivo si en su denicion se nom-
bra a s mismo.
En programacion, una funcion es recursiva si en el ambito de esa funcion 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 clasico
es el factorial de un n umero.
Una manera de denir el factorial de un n umero n > 1 es:
!n =
n

i=1
i,
es decir, el producto de todos los n umeros enteros menores o guales que el, lo
que se puede resolver facilmente con una funcion iterativa, esto es, una funcion
con un ciclo que itere sucientes veces, incrementando un valor y entonces ir
almacenando en una variable el resultado de esas multiplicaciones.
Una implementacion de esta denicion iterativa es:
(1) int i,n;
(2) long double valorAc;
(4) valorAc=1.0;
(5) std::cout << "Numero entero:";
(6) std::cin>> n;
(7) for(i=1; i<=n; i++) valorAc = valorAc*i;
(8) std::cout<<"El factorial de "<<n<<" es:"<<valorAc;
El ciclo principal es en la lnea (7). No hay ning un truco hasta aqu. La
unica observacion importante es en la lnea (2) en donde se declara el tipo
long double para el valor del resultado, la razon para tal accion es que el
n umero factorial crece muy rapido y a un con entradas en el rango de los
caracteres (hasta 255), el factorial es muy grande. Este procedimiento com-
putacional no hace uso de tecnicas especiales empleadas para tratar n umeros
grandes.
Sin embargo una solucion mas elegante es usar la denicion recursiva, y esta
es:
36
!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 denicion iterativa en donde era suciente traba-
jar 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 gura 16 ilustra como funciona la recursividad cuando se intenta
obtener el factorial(5).
Figura 16. Recursividad cuando se ejecuta factorial(5)
37
4.0.1. La serie Fibonacci
Una de las series mas famosas es sin duda alguna la serie de Fibonacci:
1, 1, 2, 3, 5, 8, 13, 21, 34, . . .
Un poco de observacion es sufucuente para encontrar que cualquier n umero
(a partir del tercero de la serie, osea el segundo 1) es igual a la suma de los
dos n umeros anteriores.
Daremos en primer lugar la version iterativa. En este algoritmo deseamos
encontrar el n-esimo n umero de la serie Fibonacci. As si n = 4 el resultado
del algoritmo debe ser 3; si n = 6 el resultado debe ser 8. La version iterativa
empieza desde los primeros 1s, sumandolos y encontrando el tercero, luego
para encontrar el cuarto n umero se suman el tercero (recien encontrado) y el
segundo, y as en adelante hasta encontrar el n umero buscado.
#include <iostream>
int main (int argc, char * const argv[]) {
int i,n,fib,fib1,fib2,fibx;
std::cout<<"Un numero entero:";
std::cin>>n;
fib1=2; fib2=1; i=3;
if((n==1)||(n==2))
fib=1;
else{
do{
fib = fib1 + fib2;
fibx = fib1; i++;
fib1 = fib; fib2 = fibx;
}while(i<n);
}
std::cout << "\nEl "<<n<<"-esimo numero de
la serie Fibonacci es: "<<fib;
return 0;
}
La denicion recursiva para encontrar todos los n primeros n umeros de la serie
Fibonacci es:
38
fib(n) =

1 Si n = 1 o n = 2
fib(n 1) + fib(n 2) Si n > 2
En el siguiente codigo, la solucion que propone la recursividad resulta en una
programacion elegante, aunque costosa. El codigo que hace esto es:
( 1) #include <iostream>
( 2) //====================
( 3) int fib(int val){
( 4) if ((val==1)||(val==2))
( 5) return 1;
( 6) else
( 7) return (fib(val-1)+fib(val-2));
( 8) }
( 9) //====================
(10) int main (int argc, char * const argv[]) {
(11) int n;
(12) std::cout<<"Numero entero:"; std::cin>>n;
(13) std::cout<<"\nEl "<< n
(14) <<"-esimo numero fibonacci es: "<< fib(n);
(15) return 0;
(16) }
Como regla general, cualquier algoritmo recursivo se puede reescribir en un
algoritmo iterativo. La ventaja de tener un algoritmo iterativo es que no se
usa una pila para guardar llamadas a la misma funcion de manera recursiva,
esto es una ventaja porque el espacio de memoria destinado al uso de la pila es
generalmente limitado, de manera que cuando se hacen demasiadas funciones
push seguramente llegara el momento en que la pila se desborde, que por
cierto es un termino usado en computacion para decir que ya no hay mas
espacio disponible en la pila.
4.1. Peligros en la recursividad
El principal peligro al usar recursividad, es no tener una manera de salir del
paso recursivo, esto es peligroso porque se hacen llamadas a la misma funcion,
lo que signica una entrada en la pila donde se almacenan los estados generales
del programa.
Para decidir hacer un programa recursivo se deben de tener al menos dos cosas
muy claras:
39
1. El paso base: Esta es la clave para terminar la recursion, es cuando deja
de hacer llamadas a la funcion recursiva y hace evaluaciones devolviendo
los resultados. En el ejemplo de la serie de Fibonacci, el paso base esta en
la lnea ( 5). Ademas se debe asegurar de que es posible entrar a este paso.
2. El paso recursivo: Es la parte de la denicion que hace llamadas a
esa misma funcion y que es la causante de las inserciones en la pila,
almacenando en cada una de las llamadas, informacion 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 prob-
lema.
El siguiente ejemplo ilustra este problema
( 1) #include <iostream>
( 2) int malaFuncion( int n ){
( 3) std::cout << "malaFuncion es una recursion infinita. n="<<n;
( 4) if( n == 0 )
( 5) return 0;
( 6) else
( 7) return malaFuncion( n / 3 + 1 ) + n - 1;
( 8) }
( 9) int main (int argc, char * const argv[]) {
(10) std::cout << malaFuncion(10);
(11) return 0;
(12) }
4.2. Ejercicios de programacion
Los siguientes ejercicios deben de ser programados en C/C++ :
1. B usqueda binaria: Considere un arreglo de elementos (n umeros enteros
esta bien) en el cual los objetos ya estan ordenados, y se desea encon-
trar un elemento dentro de este arreglo. Es decir, se desea realizar una
b usqueda.
La idea general de este metodo de b usqueda binaria es:
Si el arreglo tiene 1 elemento, se compara con el numero requerido y la
40
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 dinami-
cas, esto signica que adquieren espacio y liberan espacio a medida que se
necesita. sin embargo, hay una advertencia. Como regla general siempre hay
que tener cuidado al manejar direcciones de espacios de memoria, porque es
posible que accedamos a una localidad de memoria de la cual no deseabamos
cambiar su contenido.
Antes de estudiar las listas, daremos una breve introduccion a los grafos, pues
las listas son un caso especial de los grafos.
5.1. Grafos
Los grafos son una manera visual de representar las relaciones.
Denicion 7 Si A y B son dos conjuntos, decimos que a A esta relacionado
con b B si es verdadera una sentencia R que considere a ambos elementos.
Esta sentencia R puede ser cualquier predicado, por ejemplo: es padre de,
debe dinero a, toma el curso de etc.; si el predicado es verdadero para
ese par de elementos, lo escribimos como aRb, y si el predicado es falso, lo
escribimos como b .
As los ejemplos citados, si a A, b B se puede leer:
Si A es el conjunto de alumnos, B es el conjunto de materias y R es toma el
curso, entonces pedroRlogica se lee pedro toma el curso de logica. En
la gura 17 se puede apreciar esto en forma de diagramas de Venn.
Si A es el conjunto de personas y B es tambien el conjunto de personas,
y R es debe dinero a; marisolRrafaelle signica que marisol debe
dinero a rafaelle y de ning un modo es al contrario, es decir rafaelle no
debe dinero a marisol.
Los elementos de la gura 17 denen un nuevo conjunto de elementos, el
conjunto de pares de elementos que estan relacionados. As la relacion toma
el curso de es el siguiente:
42
Figura 17. Relacion toma el curso de para los conjuntos A de personas y B de
materias.
R = {(diana, programacion), (carolina, programacion),
(carolina, compiladores), (carolina, lenguajes),
(rafael, compiladores), (gustavo, lenguajes),
(fabiola, lenguajes)}
Gracamente podemos ilustrar el conjunto R de toma el curso de con un
grafo como el que se muestra en la gura 18.
Figura 18. Grafo que ilustra la relacion toma el curso de.
De manera que podemos denir un grafo como una representacion graca de
una relacion.
Denicion 8 Para denir formalmente un grafo debemos establecer la sigu-
iente tupla:
G = A, N
Donde A es un conjunto de aristas y N = un conjunto no vaco de nodos.
En el caso de R, el conjunto A B es el conjunto de nodos y el conjunto de
echas es el conjunto de aristas.
43
Notemos que el conjunto A de aristas puede ser un conjunto vaco, pero de
ning un modo hay grafo sin nodos, es decir el conjunto N debe ser diferente
que el conjunto vaco.
Supongamos ahora A = {1, 2, 3, 4, 5, 6} y la siguiente relacion en A:
R = {(1, 2), (2, 3), (3, 4), (4, 5), (5, 6)}
Esta relacion luce como aparece en la gura 20.
Figura 19. Relacion R de A en A
y en forma de grafo es:
Figura 20. Grafo de la relacion R : A A
A esta clase de grafos, en las que cada nodo tiene a lo mas una arista dirigida
que sale y a lo mas una arista dirigida que entra, se le llama lista.
5.2. Listas simplemente encadenadas
Como vimos en la seccion anterior, una lista es una relacion de elementos, tales
que cada elemento esta relacionado con unicamente 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 nal de la lista.
Si el nodo tiene 0 aristas que entran, entonces es el inicio de la lista.
Por razones practicas, se dibujan una echa que sale de un identicador de la
lista y entra al inicio de la lista y otra echa que sale del nal de la lista y
apunta a un smbolo que se llama NULO.
Figura 21. Grafo de la relacion R : A A con apuntadores del nombre de la lista
listaLigada y hacia NULL
En C/C++ el identicador de la lista contiene la direccion del primer elemento
de la lista, as como sucede con los arreglos. El valor NULO es util para saber
cuando termina la lista, es una constante estandar y no tiene valor.
El contenido de los nodos, como ya hemos visto, son los elementos de un
conjunto. Si ese conjunto tiene elementos estructurados, tambien es valido
usarlos.
Normalmente cada nodo de la lista esta estructurado con dos partes:
1. La parte de informacion.
2. La parte de direccion al siguiente nodo de la lista.
El campo de informacion contiene el elemento real de la lista. El campo de
direcci on al siguiente contiene un apuntador al elemento con el cual esta rela-
cionado, es decir, al elemento siguiente de la lista. La lista completa se accesa
mediante el identicador de lalista. El campo de la direccion del ultimo nodo
apunta a una direccion nula.
La lista que no tiene elementos (solamente tiene un identicador que apunta a
nulo) se llama lista nula o lista vaca. Una lista se inicializa a una lista vaca
haciendo lista=null, recordemos que lista es un apuntador a una direccion
de memoria que puede albergar una variable del tipo que se hayan denido
los nodos; null es una direccion de cualquier tipo, as que el compilador asigna
la direccion null a lista.
45
Enseguida vamos a dar una lista de terminos usados para manejar los elemen-
tos de una lista simplemente encadenada, aunque no son los que usa C/C++ ,
pero s son bastante claros para hacer algoritmos. Si p es un apuntador a la
direcci on 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)) signica que se hace referencia a la seccion
de informacion del nodo siguiente al que apunta p.
5.2.1. Insertar y eliminar nodos de una lista
En el uso de las listas ligadas se ven involucradas varias operaciones, entre
ellas la de insertar un nuevo nodo a la lista y la operacion 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 signica obtener un espacio
de memoria disponible y relacionarlo con los elementos de la lista; as mismo,
eliminar un nodo de la lista signica liberar la memoria que ocupa ese nodo
sin perder la relacion con el resto de los nodos de la lista.
Insertar un elemento al inicio de la lista. La operacion p=getnode();
obtiene un nodo vaco y establece el contenido de una variable nombrada p en
la direccion de este nodo, como se muestra en la gura 22.a. Este nodo a un
no pertenece a alguna lista, simplemente se ha logrado dedicar un especio de
memoria que es apuntado por p, gura 22.b.
Figura 22. a) Creacion de un nuevo nodo. b) El nuevo nodo debe de ir insertado al
frente, atras 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 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 identicador 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 gura
23
Figura 23. Operaciones involucradas en la insercion 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 em-
bargo, debido a que list es el apuntador externo a la lista deseada, su valor
debe modicarse 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
gura 24
Figura 24. Operaciones involucradas en la eliminacion de un nodo al inicio de una
lista: c) p=list). d) x=info(p). e) list=next(p)
5.2.2. Listas en C/C++ con arreglos
Vamos a empezar una primera implementacion de listas usando arreglos, cada
elemento del arreglo debe ser un elemento compuesto. Cada elemento debe
contener una parte para la informacion y otra parte para apuntar al elemento
siguiente:
#include <iostream>
( 1) #define numNodes 500
( 2) struct nodeType{
( 3) int info;
48
( 4) int next;
( 5) };
( 6) struct nodeType node[numNodes];
int main (int argc, char * const argv[]) {
std::cout << "Hello, World!\n";
return 0;
}
En el programa anterior, en las 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 ultimo 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 nalmente 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 no-
dos 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 signica que no hay nodos disponibles, es decir, que el
arreglo esta completamente lleno. Esto signica 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 al-
goritmos 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. El uso de memoria dinamica en C/C++
Como sabemos, en lenguaje C/C++ , &x es la direccion 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 como
asignar y liberar el almacenamiento en forma dinamica y como se accesa al
almacenamiento dinamico en C/C++ .
En C/C++ , una variable que debe contener la direccion en la memoria que
almacena un n umero entero se crea mediante la declaracion
int *p;
Recordemos que esta declaracion 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
identicador, en este caso p.
Una vez declarada la variable p como un apuntador a un tipo especco de
dato, debe ser posible crear dinamicamente un objeto de este tipo especco
y asignar su direccion a p.
Esto se hace en C/C++ mediante la funcion de la biblioteca estandar malloc(size).
La fucnion malloc asigna de manera dinamica una parte de memoria de
tama no especicado en size y devuelve un apuntador a un elemento de tipo
char. Consideremos las siguientes declaraciones
extern char *malloc();
int *pi;
float *pr;
51
La palabra clave extern especica que una variable o funcion tiene un en-
lace externo. Esto signica que la variable o funcion a la que nos referimos
esta denida en alg un otro archivo fuente, o mas adelante en el mismo archi-
vo. 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 identicadores que se estan deniendo. 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 de-
nominan 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) std::cout<< *p << " " << *q << "\n";
return 0;
}
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. Observe-
mos que *p no hace referencia a una variable especca unica. Su valor cambia
conforme se modica 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 3
7 7
5 7
mallocEjemplo has exited with status 0.
La funcion free se usa en C para liberar almacenamiento de una variable
asignada dinamicamente. La orden
free(p);
invalida cualquier referencia futura a la variable *p (a menos que se asigne
nuevo espacio de memoraia a esa variable). Llamar free(p) hace que quede
disponible para re uso el almacenamiento ocupado por *p, si es necesario.
La funcion free espera un parametro apuntador del tipo char *, para que no
tengamos problemas de tipos, debemos hacer
free((char *)p);
Consideremos el siguiente ejemplo para ilustrar el uso de free:
53
#include <iostream>
int main (int argc, char * const argv[]) {
int *p, *q;
( 1) p=(int *)malloc(sizeof(int));
( 2) *p=5;
( 3) q=(int *)malloc(sizeof(int));
( 4) *q=8;
( 5) free(p);
( 6) p=q;
( 7) q=(int *)malloc(sizeof(int));
( 8) *q=6;
( 9) std::cout<<*p<<" "<<*q<<"\n";
return 0;
}
Que se imprime a la salida del programa?
Observemos que si se llama malloc dos veces sucesivas y se asigna su valor 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. Listas ligadas usando memoria dinamica
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 denimos el tipo de un apuntador y un nodo mediante
struct node{
int info;
struct node *next;
};
typedef struct node *nodePtr;
54
Un nodo de este tipo es igual a los nodos de la implementacion con arreglos,
excepto que el campo next es un apuntador y no un entero.
En lugar de declarar un arreglo, para que represente un conjunto acumulado
de nodos, estos se asignan y liberan seg un es necesario. Se elimina la necesidad
de un conjunto de nodos previamente declarado.
Si declaramos
nodePtr p;
la ejecucion de la orden
p=getNode();
debe colocar la direccion de un nodo disponible en p:
nodePtr getNode(void){
nodePtr p;
p=(nodePtr)malloc(sizeof(struct node));
return(p);
}
Para liberar la memoria utilizada usamos freeNode.
void freeNode(nodePtr p){
free(p);
}
Los procedimientos insAfter y delAfter usan la implementacion dinamica de
una lista ligada. Supongamos que list es una variable apuntador que se nala
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){
55
nodePtr q;
if((p==NULL)||(p->next==NULL)){
std::cout<<"Borrado prohibido\n";
} else{
q=p->next;
*px=q->info;
p->next=q->next;
freeNode(q);
}
}
5.5. Ejercicios de programacion
1. Implemente una pila usando memoria dinamica en listas ligadas. Imple-
mente las operaciones push, pop, empty y stackTop.
2. Implemente una cola usando memoria dinamica en listas ligadas. Imple-
mente las operaciones empty, insert y remove.
3. Desarrolle un programa para buscar un elemento en la lista (de 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
6.

Arboles
Los arboles son estructuras de datos utiles en muchas aplicaciones. Hay varias
formas de arboles y cada una de ellas es practica en situaciones especiales, en
este captulo vamos a denir algunas de esas formas y sus aplicaciones.
6.1. Concepto general de arbol
Desde el punto de vista de estructuras de datos, un arbol es un concepto
simple en su denicion, sin embargo es muy ingenioso. Un arbol es un grafo
con caractersticas muy especiales:
Denicion 9 Un arbol es un grafo A que tiene un unico nodo llamado raz
que:
Tiene 0 relaciones, en cuyo caso se llama nodo hoja
tiene un n umero nito de relaciones, en cuyo caso, cada una de esas rela-
ciones es un subarbol
Para empezar a estudiar los arboles, nos concentraremos en primer lugar en
el caso en que el nodo raz tenga 0, 1 o 2 subarboles.
6.2.

Arboles binarios
Denicion 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 subarboles 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 relacion a la derecha, el segundo elemento de la
relacion es el subarbol derecho.
La gura 25 muestra algunas conguraciones de grafos que s son arboles
binarios, y la gura 26 muestra algnas conguraciones de grafos que no son
arboles binarios.
Vamos a dar una lista de teerminos que se usan frecuentemente cuando se
trabaja con arboles:
57
Figura 25. Grafos que son estructuras tipo arbol binario
Figura 26. Grafos que no son arboles binarios
Si A es la raz de un arbol y B es la raz de su subarbol izquierdo (o dere-
cho), se dice que A es el padre de B y se dice que B es el hijo izquierdo (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 descen-
diente 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 dene 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 2
l
nodos en el nivel l. Un arbol binario
completo de profundidad d es el arbol que contiene exactamente 2
l
nodos en
cada nivel l entre 0 y d. La cantidad total de nodos t
n
en un arbol binario
completo de profundidad d, es igual a la suma de nodos en cada nivel entre 0
y d, por tanto:
t
n
= 2
0
+ 2
1
+ 2
2
+ + 2
d
=
d

j=0
2
j
Usando induccion matematica se puede demostrar que

d
j=0
2
j
= 2
d+1
1.
Dado que todas las hojas en este arbol estan en el nivel d, el arbol contiene
2
d
hojas y, por tanto, 2
d
1 nodos que no son hojas.
Si conocemos el n umero total de nodos t
n
en un arbol binario completo, pode-
mos calcular su profundidad d, a partir de la expresion t
n
= 2
d+1
1. As sabe-
mos 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 t
n
+ 1. Es decir, que en un
arbol binario completo,
d = log
2
(t
n
+ 1)
Denicion 11 Un arbol binario es un arbol binario casi completo si:
1. Cualquier nodo nd a un nivel menor que d 1 tiene 2 hijos
2. Para cualquier nodo nd en el arbol con un descendiente derecho en el
nivel d debe tener un hijo izquierdo y cada descendiente izquierdo de nd:
es una hoja en el nivel d o
tiene dos hijos
Los nodos en un arbol binario (completo, casi completo o incompleto) se
pueden enumerar del siguiente modo. Al nodo raz le corresponde el n umero
1, al hijo izquierdo le corresponde el doble del n umero asignado al padre y al
hijo derecho le corresponde el doble mas 1 del n umero asignado al padre.
59
Figura 27. Comparacion de un arbol binario y un arbol binario casi completo. El
arbol mostrado en (A) descumple la regla 2 de los arboles binarios casi completos.
6.2.1. Operaciones con arboles binarios
Con los arboles binarios es posible denir 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 utiles las operaciones makeTree,
setLeft y setRight. La operacion makeTree(x) crea un nuevo arbol binario
que consta de un unico 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. Aplicaciones de arboles binarios
Un arbol binario es una estructura de datos util 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 situacion es
bastante util en el manejo de las bases de datos, para evitar un problema que
se llama redundancia.
Una manera de encontrar los elementos duplicados en un arreglo es recorrer
todo el arreglo y comparar con cada uno de los elementos del arreglo. Esto
implica que si el arreglo tiene n elementos, se deben hacer n comparaciones,
claro, no es mucho problema si n es un n umero peque no, pero el problema se
va complicando mas a medida que n aumenta.
Si usamos un arbol binario, el n umero de comparaciones se reduce bastante,
veamos como.
El primer n umero del arreglo se coloca en la raz del arbol (como en este
ejemplo siempre vamos a trabajar con arboles binarios, simplemente diremos
arbol, para referirnos a un arbol binario) con sus subarboles izquierdo y dere-
cho vacos. Luego, cada elemento del arreglo se compara son la informacion
del nodo raz y se crean los nuevos hijos con el siguiente criterio:
Si el elemento del arreglo es igual que la informacion del nodo raz, entonces
noticar duplicidad.
Si el elemento del arreglo es menor que la informacion del nodo raz, entonces
se crea un hijo izquierdo.
Si el elemento del arreglo es mayor que la informacion del nodo raz, entonces
se crea un hijo derecho.
Una vez que ya esta creado el arbol, se pueden buscar los elementos repetidos.
Si x el elemento buscado, se debe recorrer el arbol del siguiente modo:
61
Sea k la informacion del nodo actual p. Si x > k entonces cambiar el nodo
actual a right(p), en caso contrario, en caso de que x = k informar una
ocurrencia duplicada y en caso de que x k cambiar el nodo actual a left(p).
El siguiente algoritmo
leer numero buscado >> n
tree=makeTree(n)
while(hay numeros en el arreglo){
leeSiguienteNumero >> k
p=q=tree;
while(k!=info(p)&&q!=NULL){
p=q
if(k<info(p))
q=left(p)
else
q=right(p)
}
if(k==info(p))
despliega<<" el numero es duplicado";
else
if (k<info(p))
setLeft(p,k)
else
setRight(p,k)
}
Figura 28.

Arbol binario para encontrar n umeros duplicados
Para saber el contenido de todos los nodos en un arbol es necesario recorrer
el arbol. Esto es debido a que solo tenemos conocimiento del contenido de
la direccion de un nodo a la vez. Al recorrer el arbol es necesario tener la
direcci on de cada nodo, no necesariamente todos al mismo tiempo, de hecho
normalmente se tiene la direccion de uno o dos nodos a la vez; de manera que
cuando se tiene la direccion de un nodo, se dice que se visita ese nodo.
62
Aunque hay un orden preestablecido (la enumeracion de los nodos) no siempre
es bueno recorrer el arbol en ese orden, porque el manejo de los apuntadores
se vuelve mas complejo. En su lugar se han adoptado tres criterios princi-
pales para recorrer un arbol binario, sin que de omita cualquier otro criterio
diferente.
Los tres criterios principales para recorrer un arbol binario y visitar todos sus
nodos son, recorrer el arbol en:
preorden: Se ejecutan las operaciones:
1. Visitar la raz
2. recorrer el subarbol izquierdo en preorden
3. recorrer el subarbol derecho en preorden
entreorden: Se ejecutan las operaciones:
1. recorrer el subarbol izquierdo en entreorden
2. Visitar la raz
3. recorrer el subarbol derecho en entreorden
postorden: Se ejecutan las operaciones:
1. recorrer el subarbol izquierdo en postorden
2. recorrer el subarbol derecho en postorden
3. Visitar la raz
Al considerar el arbol binario que se muestra en la gura 28 usando cada uno
de los tres criterios para recorrer el arbol se tienen las siguientes secuencias de
nodos:
En preorden: 14, 4, 3, 9, 7, 5, 15, 18, 16, 17, 20
En entreorden: 3, 4, 5, 7, 9, 14, 15, 16, 17, 18, 20
En postorden: 3, 5, 7, 9, 4, 17, 16, 20, 18, 15, 14
Esto nos lleva a pensar en otra aplicacion, el ordenamiento de los elementos
de un arreglo.
Para ordenar los elementos de un arreglo en sentido ascendente, se debe con-
struir un arbol similar al arbol binario de b usqueda, pero sin omitir las coin-
cidencias.
El arreglo usado para crear el arbol binario de b usqueda fue
<14,15,4,9,7,18,3,5,16,4,20,17,9,14,5>
El arbol de ordenamiento es el que se muestra en la gura 29
Para ordenar los elementos de este arreglo basta recorrer el arbol en forma de
entreorden.
63
Figura 29.

Arbol binario para ordenar una secuencia de n umeros
Cual sera el algoritmo para ordenarlo de manera descendente?
6.3. Representacion en C/C++ de los arboles binarios
Vamos a estudiar estas representaciones por partes, primero los nodos y el
arbol; despues las operaciones para el manejo del arbol.
6.3.1. Representacion de los nodos
Los nodos de los arboles binarios son estructuras en C/C++ que estan com-
puestas por tres partes:
Un apuntador al subarbol izquierdo, left
Un apuntador al subarbol derecho, right
Una parte de informacion, que puede ser una estructura en s misma, info.
Adicionalmente es muy util poner un apuntador al padre del nodo. father.
Usando una implementacion de arreglos tenemos:
#define numNodes 500
struct nodeType{
int info;
int left;
int right;
int father;
};
struct nodeType node[numNodes];
y usando una representacion con memoria dinamica, los nodos de un arbol 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 respectiva-
mente. Las rutinas getnode y freenode simplemente asignan y liberan nodos
usando las rutinas malloc y free.
nodePtr makeTree(int x){
nodePtr p;
p = getNode();
p->info = x;
p->left = NULL;
p->right = NULL;
return p;
}
La rutina setLeft(p,x) establece un nodo con contenido x como el hijo
izquierdo de node(p).
void setLeft(nodePtr p, int x){
if(p == NULL)
std::cout<<"Insercion nula\n";
else
if(p->left != NULL)
std::cout<<"Insercion no valida\n";
else
p->left=maketree(x);
}
La rutina para setRight(p,x) es similar a la rutina anterior.
Cuando se establece la diferencia entre los nodos de hojas y los no-hojas, los
nodos que no son hojas se llaman nodos internos y los nodos que s son
hojas se llaman nodos externos.
6.3.2. Recorridos de arbol binario en C/C++
Aqu usaremos recursividad para hacer estas rutidas de los recorridos de
arboles binarios. Las rutinas se llaman preTr, inTr y postTr, que impri-
men el contenido de los nodos de un arbol binario en orden previo, en orden
65
y en orden posterior, respectivamente.
El recorrido en pre orden se logra con esta rutina:
void preTr(nodePtr tree){
if (tree != NULL){
std::cout<<tree->info;
preTr(tree->left);
preTr(tree->right);
}
}
El recorrido en entre-orden se logra con esta rutina:
void inTr(nodePtr tree){
if (tree != NULL){
inTr(tree->left);
std::cout<<tree->info;
inTr(tree->right);
}
}
y el recorrido en post-orden se logra con esta rutina:
void postTr(nodePtr tree){
if (tree != NULL){
postTr(tree->left);
postTr(tree->right);
std::cout<<tree->info;
}
}
6.4.

Arboles
Hasta ahora hemos visto los arboles binarios que son aquellos arboles que sus
nodos solamente pueden tener un maximo de dos hijos. Cuando ocurre que
los nodos tienen cualquier n umero nito de hijos, son arboles (en genreal). De
manera que
Denicion 12 Un arbol es un conjunto nito 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, her-
mano, 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 dene 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 ultimo hijo de un nodo en particular. El primer hijo de
un nodo en un arbol ordenado se denomina con frecuencia el hijo mas viejo
de este nodo y el ultimo se denomina el hijo mas joven. Vease la gura 30.
Un bosque es un conjunto ordenado de arboles ordenados.
Figura 30. El arbol de la izquierda es ordenado y el arbol de la derecha es un arbol
no ordenado.
6.4.1. Representacion dinamica en C de los arboles
Al igual que en los arboles binarios, los nodos en un arbol tienen una 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 gura 31.
Figura 31. Representacion con listas de los nodos de un arbol
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 deninen 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.
Para recorrer los nodos de un bosque, es preferible convertir todo el bosque
en un arbol binario correspondiente, como se ilustra en la gura 32. Cuando
ya se tiene el arbol 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 arboles del bosque
son arboles ordenados; entonces un recorrido en entreorden dara como resul-
tado una secuencia de nodos ordenada en sentido ascendente.
6.5. Ejercicios de programacion
1. Escriba un programa que acepte un apuntador a un nodo y devuelva un
valor verdadero si este nodo es la raz de un arbol binario valido y falso
en caso contrario.
2. Escriba un programa que acepte un apuntador a un arbol binario y un
apuntador a un nodo del arbol, y devuelva el nivel del nodo en el arbol.
3. Escriba un programa para ejecutar el experimento siguiente: genere 100
n umeros aleatorios. Conforme se genera cada n umero, insertelo en un
69
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. Re-
sumiendo, las listas son grafos en donde cada nodo tiene una arista que sale y
una arista que llega, excepto un par de nodos, uno de esos nodos es el inicio
de la lista que tiene no tiene arista que entra; y el otro nodo es el nal de la
lista que no tiene arista que sale; En los 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. Recordatorio de las deniciones
Un grafo consiste de una tupla G = N, A, en donde N es un conjunto de
elementos llamados nodos; y A es una relacion, representada por un conjunto
de pares ordenados de nodos.
El conjunto N de nodos debe de ser un conjunto no-vaco, esto signica que
para que exista un grafo es necesario al menos un nodo. El conjunto A de
aristas puede ser el conjunto vaco. En la gura 33 se muestra un grafo y sus
conjuntos de nodos y de aristas.
Figura 33. Grafo dirigido o digrafo
Si las aristas de un grafo no estan dirigidas se omiten las echas, y se dice
entonces que es un grafo (no un grafo dirigido). Cuando en las aristas no hay
echas, se entiende que hay una relacion reexiva, es decir, si para un grafo
71
G = N, A; a, b N y se tiene que (a, b), (b, a) A, entonces no se dibujan
las echas. Porque la echa indica el sentido de la relacion.
Si G = N, A es un grafo, los siguientes terminos son frecuentemente usados
al trabajar con G:
Nodo incidente: Si (a, b) A entonces tanto el nodo a como el nodo b
son nodos incidentes.
Grado de incidencia: Se dene para cada nodo, y es su maximo numero de
incidencias. Tambien se conoce con el nombre de valencia.
Grado interno: Tambien se dene para cada nodo y es el n umero de aris-
tas que llegan a ese nodo. Otro nombre para este termino es valencia de
entrada.
Grado externo: Para cada nodo es el n umero de aristas que salen del nodo.
Se conoce tambien con el nombre de valencia de salida.
Adyacencia: Si a, b N, el nodo a es adyacente al nodo b si (a, b) A.
Note que si (b, a) A, pero (a, b) A, entonces el nodo a no es adyacente
al nodo b, pero el nodo b si es adyacente al nodo a.
Sucesor: Si el nodo a es adyacente al nodo b, entonces el nodo b es el sucesor
del nodo a.
Antecesor: Si el nodo a es adyacente al nodo b, entonces el nodo a es el
antecesor del nodo b.
Es posible asociar una etiqueta a cada arista, como se muestra en la gura 34.
La etiqueta asociada con cada arista se denomina peso.
Figura 34. Grafo dirigido con pesos
Los grafos ponderados son relaciones denidas por un conjunto de elementos,
en donde cada elemento es un trio ordenado (a, b, c) donde a, b N y c W,
para alg un conjunto W de pesos. Con los grafos y grafos ponderados (los que
tienen pesos) se pueden tener algunas operaciones basicas:
72
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 dene como una se-
cuencia de k + 1 nodos n
1
, n
2
, . . . , n
k
, n
k+1
, tal que n
1
= a, n
k+1
= b y
isAdjacent(n
i
,n
i+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 gura 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)}
Figura 35. Grafo G = N, R
Se desea saber si existe un camino entre un par de nodos dado.
73
Referencias
[LAT97] Yedidyah Langsam, Moshe J. Augenstein, and Aaron M. Tenenbaum.
Estructura de datos con C y C++. Prentice-Hall, Inc., 2a edition,
1997.
[MP97] William H. Murray and Chris H. Pappas. Manual de Borland C++,
volume ISBN: 0-07-882216-5. Osborne McGraw-Hill, 1997.
[TA83] Aaron M. Tenenbaum and Moshe J. Augenstein. Estructura de datos
en Pascal. Phh-PrenticeHall, 1983.
74

También podría gustarte