Documentos de Académico
Documentos de Profesional
Documentos de Cultura
2 de junio de 2005
Resumen
Una estructura de datos es una manera de almacenar y organizar datos para facilitar
el acceso y modificaciones. No hay una estructura de datos que sirva para todos los
propósitos, y por eso es importante saber sus ventajas y desventajas. Este documen-
to es una colección de apuntes para el curso de Estructuras de Datos. Los apuntes
se han tomado de algunas fuentes que son detalladas en la sección de bibliografı́a.
Índice
1.1. Arreglos 3
1.2. Apuntadores 10
2. La pila 21
1
3. Colas 31
4. Recursión 36
5. Listas 42
5.1. Grafos 42
6. Árboles 57
6.4. Árboles 66
7. Grafos 71
2
1. Preliminares de programación en C/C++
1.1. Arreglos
Los arreglos se pueden definir usando tipos de datos mixtos debido a que se
supone que todos los elementos son del mismo tamaño. Puesto que todos los
elementos son del mismo tamaño y ya que este hecho se utiliza para ayudar
a determinar cómo localizar un elemento dado, resulta que los elementos son
almacenados en localidades de memoria contiguas.
Por esta razón, un identificador de arreglo no puede ser usado nunca como un
valor-i (valor izquierdo). Los valores izquierdos representan variables que su
contenido puede ser alterado por el programa; frecuentemente aparecen a la
izquierda de las sentencias de asignación.
3
float SalariosDeEmpleados[Max_empleados];
.
.
.
SalariosDeEmpleados = 45739.0;
4
#define Coordenadas_Max 20
#define Tamano_MaX_Compania_Id 15
int CoordenadasDePantalla[Coordenadas_Max];
char IDCompania[Tamano_MaX_Compania_Id];
int Estado[Rango_Maximo_Estado]={-1,0,1};
Estado[0];
/*
* 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;
}
/*
/ dosDim.cpp
*/
#include <iostream>
#define numFilas 4
#define numColumnas 5
for(despFila=0;despFila<numFilas;despFila++)
for(despColumna=0;despColumna<numColumnas;despColumna++){
desplazamiento=numColumnas-despColumna;
multiplo=despFila;
despCalculados[despFila][despColumna]=
(despFila+1)*despColumna+desplazamiento * multiplo;
};
for(despFila=0;despFila<numFilas;despFila++){
std::cout<<"Fila actual: "<<despFila<<"\n";
std::cout<<"Distancia relativa desde la base: "<<"\n";
6
for(despColumna=0;despColumna<numColumnas;despColumna++)
std::cout<<" "
<<despCalculados[despFila][despColumna]
<<" ";
std::cout<<"\n\n";
}
return 0;
}
}
El programa utiliza dos ciclos for para calcular e inicial cada uno de los
elementos del arraglo a su respectiva distancia relativa desde la base. El arreglo
creado tiene 4 filas y 5 columnas por fila, haciendo un total de 20 elementos
enteros.
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
7
1.1.6. Arreglos como argumentos de funciones
/*
// ereArray.xcode
*/
#include <iostream>
#include <ctype.h>
#define maxArray 5
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úsculas
originales: aeiou, mientras que el segundo ciclo for en main() da como salida
los contenidos del arreglo después del llamado a la función ArrayMayuscula():
AEIOU.
/*
// ereArray2.xcode
*/
#include <iostream>
#include <ctype.h>
#define maxArray 5
for(desplazamiento=0;desplazamiento<maxArray;desplazamiento++)
ElementosArrayMayuscula(Array[desplazamiento]);
for(desplazamiento=0;desplazamiento<maxArray;desplazamiento++)
std::cout<<Array[desplazamiento];
return 0;
}
9
aeiou
aeiou
valarray has exited with status 0.
1.2. Apuntadores
direccionRam = &contenidoRAM
10
Figura 3. Notación de flecha para los apuntadores
direccionRAM = &contenidoRAM;
*direccionRAM = 20;
C/C++ requiere una definición para cada variable. Para definir una variable
apuntador direccionRAM que pueda contener la dirección de una variable
int, se escribe:
int *direccionRAM;
int *
direccionRAM
El asterisco que sigue a int significa “apuntador a”. Esto es, el siguiente tipo
de dato es una variable apuntador que puede contener una dirección a un int:
int *
11
En C/C++ una variable apuntador contiene la dirección de un tipo de dato
particular:
char *direccion_char;
char *direccion_int;
int *direccion_int;
float un_float = 98.34;
direccion_int = &un_float;
/*
// changeVals.xcode
*/
12
En la lı́nea (04) se han declarado tres variables de tipo entero, se da a cada
celda un nombre y se inicializan 2 de éstas. Supondremos que la dirección de
memoria asignada para la variable A int es la dirección 5328, y la dirección
en memoria RAM asignada para la variable B int es la dirección 7916, y la
celda llamada Temp int se le ha asignado la dirección 2385. Véase la figura 5;
Temp_int = *direccion_int;
13
Este puede ser un error muy difı́cil de localizar puesto que muchos compi-
ladores no emiten ninguna advertencia/error.
*direccion_int = B_int;
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 dirección de otra celda que puede contener un entero.
14
1.2.3. Utilización incorrecta del operador de dirección
puedeAlmacenarDireccionDeConstante = &37;
int RAM_int = 5;
puedeAlmacenarDireccionDeExpresionTemp = &(RAM_int +15);
puedeAlmacenarDireccionDeRegistro = &varRegistro;
struct campo_etiqueta{
tipo_miembro miembro_1;
tipo_miembro miembro_2;
15
tipo_miembro miembro_3;
:
:
tipo_miembro miembro_n;
};
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;
};
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;
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;
estructuraNombre.miembroNombre
gets(stbarco_usado.szmodelo);
std::cin>> stbarco_usado.sztipo;
srd::cout<< stbarco_usado.fprecioventa;
Ejemplo de estructuras:
/* fractionStruct.cpp -
#include <iostream>
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);
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) {
18
<< f.denominator << "\n";
}
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 estándares, pero no se
checan los tipos o los números de argumentos con el número de parámetros.
Los prototipos se usan para inicializar apuntadores a funciones, antes de
que las funciones sean definidas.
La lista de parámetros se usa para checar la correspondencia de los argu-
mentos en la llamada a la función con los parámetros en la definición de la
función
Con parámetros de funciones que sean de tipo matriz (que se pasan por
referencia). Ejemplo: int strlen(const char[]);
Cuando los parámetros son punteros (a fin de que desde dentro de la función
no puedan ser modificados los objetos referenciados). Ejemplo: int printf
(const char *format, ...);
Cuando el argumento de la función sea una referencia, previniendo ası́ que la
función pueda modificar el valor referenciado. Ejemplo: int dimen(const
X &x2);
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 método de inserción
b) ilustre cómo opera el algoritmo insertionSort(A) usando como en-
trada el arreglo A=<31,41,59,26,41,58>
2. Reescriba el programa y nómbrelo insertionSortNondec para que or-
dene los elementos en orden decreciente
3. Considere el siguiente problema de búsqueda:
Input: Una secuencia de n números A = ha1 , a2 , . . . , an i y un valor v.
Output: Un ı́ndice i tal que v = A[i] o el valor espacial N IL si v no
ocurre en A.
Escriba un programa que resuelva este problema de búsqueda.
4. Considere el problema de sumar dos números binarios de longitud n.
Cada número se almacena en uno de los arreglos A y B de tamaño n. La
suma se almacena en un arreglo C de tamaño n + 1, también como un
número binario. Escriba un programa que resuelva este problema.
20
2. La pila
Para describir cómo funciona esta estructura, debemos agregar un nuevo ele-
mento, el elemento G. Después de haber agregado el elemento G a la pila, la
nueva configuración es la que se muestra en la figura 10.
21
Figura 10. Operación 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
está en la cima de la pila y solamente podemos retirar el elemento que está en
la cima. Para que la sentencia “retira C de la pila” tenga sentido, debemos
replantear las órdenes a algo como:
22
Cuando se termina de ejecutar algún procedimiento, se recupera el registro que
está en la cima de la pila. En ese registro están los valores de las variables como
estaban antes de la llamada a la función, o algunas pueden haber cambiado si
valor, dependiendo del ámbito de las variables.
Esto nos lleva a pensar en otras utilidades de la pila. La pila sirve para en-
contrar errores.
Lo que sucede es que, cuando se retira el elemento G se debe hacer una evalu-
ación 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.
23
2.2. Operaciones básicas
push(S,e)
Para retirar un elemento de la pila S y asignarlo a una variable del mismo tipo
que el tipo de los elementos de la pila, usaremos la operación pop escribiéndola
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 operación 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 operación, e era el elemento en la cima, ahora ya no lo es más.
El apuntador “cima” decrece en una unidad.
Esta operación toma como argumento una estructura del tipo stack (pila) y
devuelve un valor booleano, devuelve un true si la pila está vacı́a y devuelve
24
un false si la pila tiene al menos un elemento, es decir:
true si S tiene 0 elementos
stackempty(S) =
f alse si S tiene más de 0 elementos
v=stacktop(S)
‘(’ : 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)
26
fin de la nueva estructura
// 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;
La lı́nea (2) incrementa el tope (cima) de la pila en una unidad, con el fin de
agregar el elemento en una posición libre de la pila, lo cual se logra en la lı́nea
(3), asignando el valor e en la casilla S->top del arreglo item de la pila.
27
2.5.2. La operación pop
La lı́nea (1) describe que esta función devuelve un tipo entero, el tipo de
elementos guardados en la pila; luego notamos que debemos dar sólo la direc-
ción de alguna variable de tipo estructura de pila (struct stack *). Obtener la
dirección se logra con el operador de indirección (&).
Las lı́neas (4) y (5) hacen todo el trabajo de esta función, se almacena el valor
que será devuelto en una variable de tipo entero y luego se decrementa el tope
de la pila.
28
2.5.4. La operación stacktop
Esta función debe devolver un número 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 dirección para dar la dirección de la variable que
alberga una estructura de tipo pila. El siguiente segmento de código ilustra
cómo se han usado las funciones antes creadas, por supuesto que se pueden
separar y crear una nueva función 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) }
...
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 prefijo, de manera que
se repite lo anterior, tomar el operador y la lista de sus operandos y
tratar cada uno de sus operandos como expresiones en prefijo: 2 +
[5 ∗ [/ + 574]], luego 2 + [5 ∗ [[+57]/4]] y finalmente 2 + [5 ∗ [[5 + 7]/4]]
y evaluar. Los paréntesis cuadrados son para ilustrar el ejemplo y no
son necesarios para su evaluación.
c) −1 No hay nada que hacer, pues es un operador unario.
30
3. Colas
Definición 5 Las colas son una estructura de datos similar a las pilas. Recorde-
mos que las pilas funcionan en un depósito 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 “fifo” (first
in, first out) porque funcionan como una tuberı́a, 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. Dinámica 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
31
Teóricamente no hay lı́mite para el tamaño de la cola, asi que siempre se
deberı́a 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 operación remove sólamente se puede hacer si
la cola no está vacı́a.
De manera similar a las pilas, las colas definen una estructura no estándar, de
manera que se debe crear un nuevo tipo de dado, el tipo cola, que debe tener
los siguientes elementos:
Suponiendo que los elementos son números 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.
struct cola{
int items[maxQueue];
int front;
int rear;
};
y al operación x=remove(Q)
32
}
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:
A diferencia de las pilas y las colas, en las colas de prioridad se pueden sacar
los elementos que no están 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.
34
carros. Los autos llegan por el extremo sur del estacionamiento y salen por
el extremo norte del mismo. Si llega un cliente para recoger un carro que
no está en el extremo norte, se sacan todos los automóviles 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 vacı́os
estén en la parte sur del estacionamiento. Escriba un programa que lea un
grupo de lineas de ingreso. Cada lı́nea contiene una “A” para las llegadas
y una “D” para las salidas y un número de placa. Se supone que los
carros llegan y salen en el orden especificado en la entrada. El programa
debe imprimir (en la terminal estándar) un mensaje cada vez que entra
o sale un auto. Cuando llega un carro, el mensaje debe especificar si hay
espacio o no para él en el estacionamiento. Si no hay espacio, el carro
espera hasta que hay espacio o hasta que se lee una lı́nea de salida para
el auto. Cuando queda disponible espacio, debe imprimirse otro mensaje.
Cuando salga un coche, el mensaje debe incluir la cantidad de veces que
se movió el auto dentro del estacionamiento, incluyendo la salida misma,
pero no la llegada. Este número es 0 si el carro sale de la fila de espera.
35
4. Recursión
n
Y
!n = i,
i=1
es decir, el producto de todos los números enteros menores o guales que él, lo
que se puede resolver fácilmente con una función iterativa, esto es, una función
con un ciclo que itere suficientes veces, incrementando un valor y entonces ir
almacenando en una variable el resultado de esas multiplicaciones.
Sin embargo una solución más elegante es usar la definición recursiva, y esta
es:
36
!n = n ∗ !(n − 1)
Aquı́ hay varias cosas que señalar, en primer lugar se ha creado una nueva
función, a diferencia de la definición iterativa en donde era suficiente traba-
jar en el programa principal. Esta función se llama factorial (como era de
suponerse), y empieza su encabezado en la lı́nea (1).
Allı́ mismo en la misma lı́nea (1), es de notar que hemos emplado ahora el
tipo double tanto para el tipo devuelto como para el tipo del argumento, a
diferencia de la versión iterativa en donde empleábamos tipos diferentes. La
razón es que al iniciar la recursión 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 función se recupera una entrada de la
pila. En la figura 16 ilustra cómo funciona la recursividad cuando se intenta
obtener el factorial(5).
37
4.0.1. La serie Fibonacci
Una de las series más famosas es sin duda alguna la serie de Fibonacci:
#include <iostream>
38
1
Si n = 1 ó n = 2
fib(n) =
fib(n − 1) + fib(n − 2) Si n > 2
( 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) }
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 recursión, es cuando deja
de hacer llamadas a la función recursiva y hace evaluaciones devolviendo
los resultados. En el ejemplo de la serie de Fibonacci, el paso base está en
la lı́nea ( 5). Además se debe asegurar de que es posible entrar a este paso.
2. El paso recursivo: Es la parte de la definición que hace llamadas a
esa misma función y que es la causante de las inserciones en la pila,
almacenando en cada una de las llamadas, información 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 lı́nea ( 7).
Otras cosas que se deben tener claras son por ejemplo si se pasa una variable
como referencia o por valor, si las variables apuntadores son del tipo adecuado
etc.
( 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) }
40
búsqueda termina.
Si el arreglo tiene más de 1 elemento, tendremos que dividir en dos el
arreglo y decidir en qué parte del arreglo buscar; luego buscarlo usando
busqueda binaria
41
5. Listas
Hay dos desventajas serias con respecto a las estructuras estáticas 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 solución es usar listas. Las listas son estructuras de datos que son dinámi-
cas, esto significa que adquieren espacio y liberan espacio a medida que se
necesita. sin embargo, hay una advertencia. Como regla general siempre hay
que tener cuidado al manejar direcciones de espacios de memoria, porque es
posible que accedamos a una localidad de memoria de la cual no deseabamos
cambiar su contenido.
Antes de estudiar las listas, daremos una breve introducción a los grafos, pues
las listas son un caso especial de los grafos.
5.1. Grafos
42
Figura 17. Relación “toma el curso de” para los conjuntos A de personas y B de
materias.
G = hA, N i
43
Notemos que el conjunto A de aristas puede ser un conjunto vacı́o, pero de
ningún modo hay grafo sin nodos, es decir el conjunto N debe ser diferente
que el conjunto vacı́o.
R = {(1, 2), (2, 3), (3, 4), (4, 5), (5, 6)}
A esta clase de grafos, en las que cada nodo tiene a lo más una arista dirigida
que sale y a lo más una arista dirigida que entra, se le llama lista.
Como vimos en la sección anterior, una lista es una relación de elementos, tales
que cada elemento está relacionado con únicamente un elemento del conjunto,
diferente a sı́ mismo.
44
Como cada elemento puede tener a lo más una arista dirigida que sale y una
arista dirigida que entra, bien puede tener 0 aristas que salen, o cero aristas
que entran. Si el nodo tiene 0 aristas que salen, entonces es el final de la lista.
Si el nodo tiene 0 aristas que entran, entonces es el inicio de la lista.
1. La parte de información.
2. La parte de dirección al siguiente nodo de la lista.
45
Enseguida vamos a dar una lista de términos 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ón de una variable del tipo declarado para los nodos de una lista:
Una vez que se ha creado un nuevo espacio para el nuevo nodo, se debe de
establecer la parte de información de ese nodo con la operación info(p), como
se ilustra en el siguiente ejemplo con el dato 6.
46
info(p)=6;
next(p)=lista;
Esta operación coloca el valor de lista (la dirección del primer nodo en la
lista) en el campo siguiente de node(p). Estos pasos se ilustran en la figura
23
Hasta ahora, p apunta a la lista con el elemento adicional incluido. Sin em-
bargo, debido a que list es el apuntador externo a la lista deseada, su valor
debe modificarse en la dirección del nuevo primer nodo de la lista. Esto se
hace ejecutando la operación
list=p;
p=getnode();
info(p)=6;
47
next(p)=list;
list=p;
p=list;
x=info(p);
list=next(p);
#include <iostream>
( 1) #define numNodes 500
( 2) struct nodeType{
( 3) int info;
48
( 4) int next;
( 5) };
En el programa anterior, en las lı́neas (2) a (5) se crea un nuevo tipo de dato,
el tipo nodo. Cada nodo tiene dos partes, su parte de información y su parte
de apuntador al siguiente. Como solamente tenemos 500 nodos (declarados en
la lı́nea (1), el tipo de siguiente es entero y hemos decidido almacenar números
enteros solamente.
Al principio todos los nodos están 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, podrı́amos organizar inicialmente esta lista como:
void inicializaAvail(void){
int i;
avail = 0;
for(i=0; i<numNodes-1; i++){
node[i].next = i+1;
}
node[numNodes-1].next = -1;
}
49
int getNode(void){
int p;
if (avail==-1){
std::cout<<"Overflow\n";
exit(1);
}
p=avail;
avail=node[avail].next;
return 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 parámetros. Primero se asegura que p no sea nulo
y después se inserta x en el nodo siguiente al indicado por p.
50
std::cout<<"void detection\n";
}
else{
q=node[p].next;
*px = node[q].info;
node[p].next=node[q].next;
freeNode(q);
}
}
int *p;
51
La palabra clave extern especifica que una variable o función tiene un en-
lace externo. Esto significa que la variable o función a la que nos referimos
está definida en algún otro archivo fuente, o más 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 está usando el convenio de enlace de otro
lenguaje para los identificadores que se están definiendo. Para los programas
C++ la cadena por defecto es “C++”.
Los enunciados
pi = (int *) malloc(sizeof(int));
pr = (float *) malloc(sizeof(float));
crean directamente la variable entera *pi y la variable real *pr. Estas se de-
nominan variables dinámicas. Al ejecutar estos enunciados, el operador sizeof
devuelve el tamaño en bytes de su operando. Esto se usa para conservar la
independencia de máquina. Después, malloc crea un objeto de este tamaño.
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ñale a un entero, usamos el operador de
cálculo (int *) ó (float *).
pi=(int *)malloc((unsigned)(sizeof(int)));
#include <iostream>
52
(12) std::cout<< *p << " " << *q << "\n";
return 0;
}
La lı́nea (10) crea una nueva variable entera y coloca su dirección en p. Ahora
*p hace referencia a la variable entera recién creada que todavı́a 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 especı́fica única. Su valor cambia
conforme se modifica el valor de p. La lı́nea (11) establece el valor de esta
variable recién creada en 5 y la lı́nea 12 imprime los valores 5 y 7. Y ası́ la
salida del programa es:
3 3
7 7
5 7
free(p);
La función free espera un parámetro apuntador del tipo char *, para que no
tengamos problemas de tipos, debemos hacer
free((char *)p);
53
#include <iostream>
( 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;
}
p=(int *)malloc(sizeof(int));
*p=3;
p=(int *)malloc(sizeof(int));
*p=7;
Para hacer las listas ligadas necesitamos un conjunto de nodos, cada uno de
los cuales tiene dos campos: uno de información y un apuntador al siguiente
nodo de la lista. Además, un apuntador externo señala el primer nodo de la
lista. Usamos variables de apuntador para implementar apuntadores de listas.
Ası́ que definimos el tipo de un apuntador y un nodo mediante
struct node{
int info;
struct node *next;
};
54
Un nodo de este tipo es igual a los nodos de la implementación con arreglos,
excepto que el campo next es un apuntador y no un entero.
Si declaramos
nodePtr p;
la ejecución de la orden
p=getNode();
nodePtr getNode(void){
nodePtr p;
p=(nodePtr)malloc(sizeof(struct node));
return(p);
}
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);
}
}
56
6. Árboles
Los árboles son estructuras de datos útiles en muchas aplicaciones. Hay varias
formas de árboles y cada una de ellas es práctica en situaciones especiales, en
este capı́tulo vamos a definir algunas de esas formas y sus aplicaciones.
57
Figura 25. Grafos que son estructuras tipo árbol binario
Dos nodos son hermanos si son hijos izquierdo y derecho del mismo padre.
Otros términos relacionados con árboles, tienen que ver con su funcinoamiento
y topologı́a:
58
llegar desde ese nodo al nodo raı́z. De manera que el nivel del nodo raı́z es
0, y el nivel de cualquier otro nodo es el nivel del padre más uno.
d
tn = 20 + 21 + 22 + · · · + 2d = 2j
X
j=0
Dado que todas las hojas en este árbol están en el nivel d, el árbol contiene
2d hojas y, por tanto, 2d − 1 nodos que no son hojas.
d = log2 (tn + 1)
59
Figura 27. Comparación de un árbol binario y un árbol binario casi completo. El
árbol mostrado en (A) descumple la regla 2 de los árboles binarios casi completos.
Con los árboles binarios es posible definir algunas operaciones primitivas, estas
operaciones son en el sentido de saber la información de un nodo y sirven para
desplazarse en el árbol, hacia arriba o hacia abajo.
Estas otras operaciones son lógicas, tienen que ver con la identidad de cada
nodo:
q=father(p);
if(q==NULL)
return(false) /* porque p apunta a la raiz */
60
if (left(q)==p)
return(true);
return(false);
El primer número del arreglo se coloca en la raı́z del árbol (como en este
ejemplo siempre vamos a trabajar con árboles binarios, simplemente diremos
árbol, para referirnos a un árbol binario) con sus subárboles izquierdo y dere-
cho vacı́os. Luego, cada elemento del arreglo se compara son la información
del nodo raı́z y se crean los nuevos hijos con el siguiente criterio:
Si el elemento del arreglo es igual que la información del nodo raı́z, entonces
notificar duplicidad.
Si el elemento del arreglo es menor que la información del nodo raı́z, entonces
se crea un hijo izquierdo.
Si el elemento del arreglo es mayor que la información del nodo raı́z, entonces
se crea un hijo derecho.
Una vez que ya está creado el árbol, se pueden buscar los elementos repetidos.
Si x el elemento buscado, se debe recorrer el árbol del siguiente modo:
61
Sea k la información 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
62
Aunque hay un orden preestablecido (la enumeración de los nodos) no siempre
es bueno recorrer el árbol en ese orden, porque el manejo de los apuntadores
se vuelve más complejo. En su lugar se han adoptado tres criterios princi-
pales para recorrer un árbol binario, sin que de omita cualquier otro criterio
diferente.
Los tres criterios principales para recorrer un árbol binario y visitar todos sus
nodos son, recorrer el árbol en:
<14,15,4,9,7,18,3,5,16,4,20,17,9,14,5>
Para ordenar los elementos de este arreglo basta recorrer el árbol en forma de
entreorden.
63
Figura 29. Árbol binario para ordenar una secuencia de números
¿Cuál serı́a el algoritmo para ordenarlo de manera descendente?
Los nodos de los árboles binarios son estructuras en C/C++ que estan com-
puestas por tres partes:
struct nodeType{
64
int info;
struct nodeType *left;
struct nodeType *right;
struct nodeType *father;
};
struct nodeType *nodePtr;
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.
65
y en orden posterior, respectivamente.
6.4. Árboles
Hasta ahora hemos visto los árboles binarios que son aquellos árboles que sus
nodos solamente pueden tener un máximo de dos hijos. Cuando ocurre que
los nodos tienen cualquier número finito de hijos, son árboles (en genreal). De
manera que
66
Un nodo sin subárboles es una hoja. Usamos los términos padre, hijo, her-
mano, antecesor, descendiente, nivel y profundidad del mismo modo
que en los árboles binarios. El grado de un nodo es en número máximo de
hijos que alún nodo tiene.
Al igual que en los árboles binarios, los nodos en un árbol tienen una parte
de información, un apuntador al padre y uno o más apuntadores a los hijos.
De manera que una solución es crear una estructura que incluya una lista
dinámica de apuntadores, como lo muestra la figura 31.
67
int info;
struct treeNode *father;
struct treeNode *son;
struct treeNode *next;
};
Los métodos de recorrido para árboles binarios inducen métodos para recorrer
los árboles en general. Si un árbol se representa como un conjunto de nodos
de variables dinámicas con apuntadores son y next, una rutina en C/C++ para
imprimir el contenido de sus nodos se escribirı́a como:
Las rutinas para recorrer el árbol en los demás ordenes son similares. Estos
recorridos también se defininen directamente ası́:
68
Para hacer esta representación, la raı́z de cada árbol se coloca en una lista
de apuntadores; luego para cada nodo en la lista (la raı́z de cada árbol) se
procede del siguiente modo:
1. Se crea una lista de subárboles izquierdos con los apuntadores a cada uno
de los árboles en el bosque.
2. si un nodo tiene más de un hijo, entonces se crea un subárbol izquierdo
y se forma una lista de subárboles izquierdos con todos los hijos de ese
nodo.
Figura 32. Arriba: Un bosque de árboles. Abajo: El árbol binario que corresponde
a ese bosque.
Si el bosque es un bosque ordenado, es decir, que todos los árboles del bosque
son árboles ordenados; entonces un recorrido en entreorden dará como resul-
tado una secuencia de nodos ordenada en sentido ascendente.
69
árbol de búsqueda binaria inicialmente vacı́o. Después de insertar los 100
números, imprima el nivel de la hoja que tiene el nivel más grande y
el nivel de la hoja que tiene el nivel más chico. Repita este proceso 50
veces. Imprima una tabla que indique cuántas veces de las 50 ejecuciones
produjeron una diferencia entre el nivel de hoja máximo y mı́nimo de
0,1,2,3, y ası́ sucesivamente.
4. Implemente los recorridos de los árboles binarios.
5. Si un bosque se representa mediante un árbol binario, muestre que el
número de vı́nculos derechos nulos es 1 mayor que el número de no hojas
del bosque.
70
7. Grafos
En esta parte del curso vamos a retomar la idea de los gráfos. Hasta ahora
homos visto las listas y los árboles 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 final de la
lista que no tiene arista que sale; En los árboles, los nodos tienen una arista
que llega (la del padre) y una o más aristas que salen (los hijos).
Como veremos más adelante con mucho mayor detalle, los nodos en los grafos
no tienen lı́mite de aristas que salen o aristas que lleguen, por eso tanto las
listas como los árboles son casos particulares de los grafos.
71
G = hN, A i; a, b ∈ N y se tiene que (a, b), (b, a) ∈ A , entonces no se dibujan
las flechas. Porque la flecha indica el sentido de la relación.
Es posible asociar una etiqueta a cada arista, como se muestra en la figura 34.
La etiqueta asociada con cada arista se denomina peso.
72
Con grafos:
join(a,b): Agrega una relación del nodo a al nodo b. Si la relación no
existe, entonces crea una relación.
removeArc(a,b): Quita un arco del nodo a al nodo b
Con grafos ponderados:
joinWt(a,b,w): Agrega una relación del nodo a al nodo b y le asocia el
peso w. Si la relación no existe, entonces de crea la relación y le asocia el
peso indicado.
removeArcWt(a,b): Quita un arco del nodo a al nodo b con peso w.
La operación isAdjacent(a,b) devuelve un valor TRUE si el nodo a es
adyacente al nodo b, y devuelve un valor FALSE en caso contrario.
Una trayectoria de longitud k del nodo a al nodo b se define como una se-
cuencia de k + 1 nodos hn1 , n2 , . . . , nk , nk+1 , i tal que n1 = a, nk+1 = b y
isAdjacent(ni ,ni+1 ) para todas las 1 ≤ i < k.
Supongamos el grafo ponderado de la figura 35, este grafo tiene como conjunto
de nodos N = {3, 10, 17, 5, 8, 6} y una relación
R = {(3, 10, 1), (10, 17, 7), (8, 17, 1), (5, 8, 3), (5, 6, 1), (6, 17, 5)}
73
Referencias
74