Está en la página 1de 43

TRABAJO FINAL: TERCER

PARCIAL
Árboles binarios y árboles Bayer, grafos, recursividad, pistas, colas y
listas enlazadas

Jennifer Alondra Alcántara López - 20181163


UNIVERSIDAD APEC | ESTRUCTURA DE DATOS – INF152 | PROF. WILLY PADUA
Tabla de contenido
Introducción .................................................................................................... 3
1. Árboles binarios y árboles Bayer ........................................................... 4
1.1. Árboles binarios ............................................................................. 6
1.2. Árboles Bayer ............................................................................... 13
2. Grafos ................................................................................................... 17
3. Recursividad ......................................................................................... 26
4. Pilas y Colas .......................................................................................... 29
4.1. Pilas .............................................................................................. 29
4.2. Colas ............................................................................................ 30
4.3. Ejemplos de Pilas y Colas ............................................................. 31
5. Listas enlazadas ................................................................................... 35
5.1. Listas doblemente enlazadas ....................................................... 40
Conclusión ................................................................................................ 42
Bibliografía .................................................................................................... 43
INTRODUCCIÓN
El presente trabajo trata sobre varios temas importantes en lo que es Estructura de
datos, los cuales son: árboles binarios y árboles de Bayer o árboles-B, que son estructuras
de datos compuestas de nodos, vértices y aristas, siendo también acíclica. Además, se
hablará acerca de los grafos, que son un conjunto de vértices unidos por enlaces llamados
aristas. Estos dos temas hacen uso de la recursividad, que es cuando un proceso o función
se llama a sí mismo. Las Pilas y Colas son estructuras de datos que se utilizan
generalmente para simplificar ciertas operaciones de programación. Estas pueden utilizar
listas enlazadas para su implementación, las cuales se definen como una estructura lineal
que almacena una colección de elementos generalmente llamados nodos, en donde cada
nodo puede almacenar datos y ligas a otros nodos. En cada tema se desarrollará su
definición, tipos, diagramas y ejemplos.
1. Árboles binarios y árboles Bayer
Un árbol es una estructura (posiblemente no lineal) de datos compuesta de nodos,
vértices y aristas que es acíclica. Un árbol que no tiene ningún nodo se llama árbol vacío
o nulo. Un árbol que no está vacío consta de un nodo raíz y potencialmente muchos
niveles de nodos adicionales que forman una jerarquía.

Para comprender mejor qué es un árbol es importante conocer los siguientes


términos:
• Raíz: El nodo superior de un árbol.
• Rama: Una ruta del nodo raíz a cualquier otro nodo.
• Hijo: Un nodo conectado directamente con otro cuando se aleja de la raíz.
• Padre: La noción inversa de hijo.
• Hermanos: Un conjunto de nodos con el mismo padre.
• Descendiente: Un nodo accesible por descenso repetido de padre a hijo.
• Ancestro: Un nodo accesible por ascenso repetido de hijo a padre.
• Hoja: Un nodo sin hijos.
• Nodo interno: Un nodo con al menos un hijo.
• Grado: Número de subárboles de un nodo.
• Brazo: La conexión entre un nodo y otro.
• Camino: Una secuencia de nodos y brazos conectados con un nodo
descendiente.
• Nivel: El nivel de un nodo se define por 1 + (el número de brazos entre el nodo
y la raíz).
• Altura de un nodo: La altura de un nodo es el número de brazos en el camino
más largo entre ese nodo y una hoja.
• Altura de un árbol: La altura de un árbol es la altura de su nodo raíz.
• Profundidad: La profundidad de un nodo es el número de brazos desde la raíz
del árbol hasta un nodo.
• Bosque: Un bosque es un conjunto de árboles n ≥ 0 disjuntos.
1.1. Árbol binario
Un árbol binario es una estructura de datos en la cual cada nodo puede tener un
hijo izquierdo y un hijo derecho. En un árbol binario cada nodo puede tener cero, uno o
dos hijos (subárboles). No pueden tener más de dos hijos (de ahí el nombre "binario"). Si
algún hijo tiene como referencia a null, es decir que no almacena ningún dato, entonces
este es llamado un nodo externo. En el caso contrario el hijo es llamado un nodo interno.
Se conoce el nodo de la izquierda como hijo izquierdo y el nodo de la derecha como hijo
derecho. Usos comunes de los árboles binarios son los árboles binarios de búsqueda, los
montículos binarios y Codificación de Huffman.

Árboles binarios completos:


Se define como un árbol binario completo (ABC) como un árbol en el que todos
sus nodos, excepto por los del último nivel, tienen dos hijos: el subárbol izquierdo y el
subárbol derecho.
Árboles binarios perfecto:
Es un Árbol completo en donde todos las Hojas están en el mismo Nivel.

Implementación de árboles binarios en C


Un árbol binario puede declararse de varias maneras. Algunas de ellas son:
• Estructura con manejo de memoria dinámica, siendo el puntero que apunta
al árbol de tipo tArbol:
typedef struct nodo {
int clave;
struct nodo *izdo, *dcho;
} nodo;

• Estructura con arreglo indexado:

typedef struct tArbol {


int clave;
tArbol hIzquierdo, hDerecho;
} tArbol;
tArbol árbol [NUMERO_DE_NODOS];

• En el caso de un árbol binario casi-completo (o un árbol completo), puede


utilizarse un sencillo arreglo de enteros con tantas posiciones como nodos
deba tener el árbol. La información de la ubicación del nodo en el árbol es
implícita a cada posición del arreglo. Así, si un nodo está en la posición i, sus
hijos se encuentran en las posiciones 2i+1 y 2i+2, mientras que su padre (si
tiene), se encuentra en la posición truncamiento((i-1) /2) (suponiendo que la
raíz está en la posición cero). Este método se beneficia de un almacenamiento
más compacto y una mejor localidad de referencia, particularmente durante
un recorrido en pre-orden. La estructura para este caso sería, por tanto:

int árbol [NUMERO_DE_NODOS];

Recorridos sobre árboles:


Los recorridos son algoritmos que nos permiten recorrer un árbol en un orden
especifico, los recorridos nos pueden ayudar encontrar un nodo en el árbol, o buscar una
posición determinada para insertar o eliminar un nodo.
• Búsquedas no informadas:
Las búsquedas no informadas son aquellas en que se realiza el viaje por todo
el árbol sin tener una pista de donde pueda estar el dato deseado. Este tipo de
búsquedas también se conocen como búsquedas a ciegas. Los siguientes métodos
de búsqueda que veremos a continuación (Búsqueda en profundad y Búsqueda en
amplitud) pertenecen a las búsquedas no informadas:
o Búsqueda en profundidad:
▪ Recorrido Pre-orden: El recorrido inicia en la Raíz y luego se recorre
en pre-orden cada uno de los subárboles de izquierda a derecha.

En este tipo de recorrido se realiza cierta acción (quizás


simplemente imprimir por pantalla el valor de la clave de ese nodo)
sobre el nodo actual y posteriormente se trata el subárbol izquierdo
y cuando se haya concluido, el subárbol derecho. Otra forma para
entender el recorrido con este método seria seguir el orden: nodo
raíz, nodo izquierdo, nodo derecho.
Implementación en C:
void preorden (tArbol *a)
{
if (a! = NULL) {
tratar(a); //Realiza una operación en nodo
preorden(a->hIzquierdo);
preorden(a->hDerecho);
}
}

▪ Recorrido Pos-orden: Se recorre el pos-orden cada uno de los


subárboles y al final se recorre la raíz. En este caso se trata
primero el subárbol izquierdo, después el derecho y por último
el nodo actual. Otra forma para entender el recorrido con este
método seria seguir el orden: nodo izquierdo, nodo derecho,
nodo raíz
Implementación en C:

void postorden(tArbol *a){


if (a != NULL) {
postorden(a->hIzquiedo);
postorden(a->hDerecho);
tratar(a); //Realiza una operación en nodo
}
}

▪ Recorrido in-orden: Se recorre en in-orden el primer subárbol,


luego se recorre la raíz y al final se recorre en in-orden los demás
subárboles. En este caso se trata primero el subárbol izquierdo,
después el nodo actual y por último el subárbol derecho. En un
ABB este recorrido daría los valores de clave ordenados de
menor a mayor. Otra forma para entender el recorrido con este
método seria seguir el orden: nodo izquierdo, nodo raíz, nodo
derecho.
Implementación en C:
void inorden(tArbol *a)
{
if (a != NULL) {
inorden(a->hIzquierdo);
tratar(a); //Realiza una operación en nodo
inorden(a->hDerecho);
}
}

o Búsqueda en amplitud: Se recorre primero la raíz, luego se recorren los


demás nodos ordenados por el nivel al que pertenecen en orden de
Izquierda a derecha.

Al contrario que en los métodos de recorrido en profundidad, el


recorrido por niveles no es de naturaleza recursiva. Por ello, se debe
utilizar una cola para recordar los subárboles izquierdos y derecho de cada
nodo.

El esquema algoritmo para implementar un recorrido por niveles es


exactamente el mismo que el utilizado en la versión iterativa del recorrido
en preorden pero cambiando la estructura de datos que almacena los
nodos por una cola.
Implementación en C:

void arbol_recorrido_anch (tipo_Arbol* A) {


tipo_Cola cola_nodos; // esta cola esta implementada previamente,
almacena punteros (posiciones de nodos de árbol)

tipo_Pos nodo_actual; // este es un puntero llevara el recorrido

if (vacio(A)) // si el árbol esta vacio, salimos


return;

cola_inicializa(&cola_nodos); // obvio, y necesario

cola_enqueue(A, &cola_nodos); // se encola la raíz

while (!vacia(&cola_nodos)) { // mientras la cola no se vacie se realizará


el recorrido
nodo_actual = cola_dequeue(&cola_nodos) // de la cola saldrán los
nodos ordenados por nivel

printf("%c,", nodo_actual->info); // se "procesa" el nodo donde va el


recorrido, en este caso se imprime

if (nodo_actual->izq != null) // si existe, ponemos el hijo izquierdo en


la cola
cola_enqueue(nodo_actual->izq, &cola_nodos);

if (nodo_actual->der != null) // si existe, ponemos el hijo derecho en la


cola
cola_enqueue(nodo_actual->der, &cola_nodos);

} // al vaciarse la cola se han visitado todos los nodos del árbol


}
1.2. Árboles Bayer
Un árbol Bayer o B-árbol es un árbol de búsqueda que puede estar vacío o aquel
cuyos nodos pueden tener varios hijos, existiendo una relación de orden entre ellos.
Un árbol-B de orden M (el máximo número de hijos que puede tener cada nodo) es
un árbol que satisface las siguientes propiedades:
• Cada nodo tiene como máximo M hijos.
• Cada nodo (excepto raíz y hojas) tiene como mínimo M/2 hijos.
• La raíz tiene al menos 2 hijos si no es un nodo hoja.
• Todos los nodos hoja aparecen al mismo nivel.
• Un nodo no hoja con k hijos contiene k-1 elementos almacenados.
• Los hijos que cuelgan de la raíz (r1, ···, rm) tienen que cumplir ciertas
condiciones:
o El primero tiene valor menor que r1.
o El segundo tiene valor mayor que r1 y menor que r2, etc.
o El último hijo tiene valor mayor que rm.

Estructura de un nodo:
Cada elemento de un nodo interno actúa como un valor separador, que lo
divide en subárboles.
Los nodos internos de un árbol B, es decir los nodos que no son hoja,
usualmente se representan como un conjunto ordenado de elementos y punteros
a los hijos. Cada nodo interno contiene un máximo de U hijos y, con excepción del
nodo raíz, un mínimo de L hijos. Esta relación entre U y L implica que dos nodos que
están a medio llenar pueden juntarse para formar un nodo legal, y un nodo lleno
puede dividirse en dos nodos legales. Estas propiedades hacen posible que el árbol
B se ajuste para preservar sus propiedades ante la inserción y eliminación de
elementos.
Los nodos hoja tienen la misma restricción sobre el número de elementos,
pero no tienen hijos, y por tanto carecen de punteros. El nodo raíz tiene límite
superior de número de hijos, pero no tiene límite inferior. Algunos árboles
balanceados guardan valores sólo en los nodos hoja, y por lo tanto sus nodos
internos y nodos hoja son de diferente tipo. Los árboles B guardan valores en cada
nodo, y pueden utilizar la misma estructura para todos los nodos. Sin embargo,
como los nodos hoja no tienen hijos, una estructura especial para éstos mejora el
funcionamiento.

Class nodo árbol B en c++


#define TAMANO 1000
struct stclave {
int valor;
long registro;
};
class bnodo {
public:
bnodo (int nClaves); // Constructor
~bnodo (); // Destructor
private:
int clavesUsadas;
stclave *clave;
bnodo **puntero;
bnodo *padre;
friend class btree;
};
Algoritmos en un árbol Bayer
- Búsqueda: Se empieza en la raíz, y se recorre el árbol hacia abajo, escogiendo el
subnodo de acuerdo con la posición relativa del valor buscado respecto a los
valores de cada nodo.
• Situarse en el nodo raíz.
• (*) Comprobar si contiene la clave a buscar.
• Encontrada, fin de procedimiento.
• No encontrada:
• Si es hoja no existe la clave.
• En otro caso el nodo actual es el hijo que corresponde:
o La clave a buscar k < k1: hijo izquierdo.
o La clave a buscar k > ki y k < ki+1 hijo iésimo.
o Volver a paso 2(*).

- Inserción: todas las inserciones se hacen en los nodos hoja.


• Realizando una búsqueda en el árbol, se halla el nodo hoja en el cual
debería ubicarse el nuevo elemento.
• Si el nodo hoja tiene menos elementos que el máximo número de
elementos legales, entonces hay lugar para uno más. Inserte el nuevo
elemento en el nodo, respetando el orden de los elementos.
• De otra forma, el nodo debe ser dividido en dos nodos. La división se
realiza de la siguiente manera:
o Se escoge el valor medio entre los elementos del nodo y el nuevo
elemento.
o Los valores menores que el valor medio se colocan en el nuevo
nodo izquierdo, y los valores mayores que el valor medio se colocan
en el nuevo nodo derecho; el valor medio actúa como valor
separador.
• El valor separador se debe colocar en el nodo padre, lo que puede
provocar que el padre sea dividido en dos, y así sucesivamente.

- Eliminación: Es localizar y eliminar el elemento, y luego corregir, o hacer una


única pasada de arriba a abajo por el árbol, pero cada vez que se visita un nodo,
reestructurar el árbol para que cuando se encuentre el elemento a ser borrado,
pueda eliminarse sin necesidad de continuar reestructurando.

Se pueden dar dos problemas al eliminar elementos: primero, el elemento


puede ser un separador de un nodo interno. Segundo, puede suceder que, al
borrar el elemento, el número de elementos del nodo quede debajo de la cota
mínima. Estos problemas se tratan a continuación en orden.

- Eliminación en un nodo hoja:


• Busque el valor a eliminar.
• Si el valor se encuentra en un nodo hoja, se elimina directamente la clave,
posiblemente dejándolo con muy pocos elementos; por lo que se
requerirán cambios adicionales en el árbol.

- Eliminación en un nodo interno: Cada elemento de un nodo interno actúa como


valor separador para dos subárboles, y cuando ese elemento es eliminado,
pueden suceder dos casos. En el primero, tanto el hijo izquierdo como el
derecho tienen el número mínimo de elementos, L-1. Pueden entonces fundirse
en un único nodo con 2L-2 elementos. En el segundo caso, uno de los dos nodos
hijos tiene un número de elementos mayor que el mínimo. Entonces se debe
hallar un nuevo separador para estos dos subárboles. Note que el mayor
elemento del árbol izquierdo es el mayor elemento que es menor que el
separador. De la misma forma, el menor elemento del subárbol derecho es el
menor elemento que es mayor que el separador. Ambos elementos se
encuentran en nodos hoja, y cualquiera de los dos puede ser el nuevo separador.

• Si el valor se encuentra en un nodo interno, escoja un nuevo separador


(puede ser el mayor elemento del subárbol izquierdo o el menor
elemento del subárbol derecho), elimínelo del nodo hoja en que se
encuentra, y reemplace el elemento a eliminar por el nuevo separador.
• Como se ha eliminado un elemento de un nodo hoja, se debe tratar este
caso de manera equivalente.
2. Grafos
Los grafos no son más que la versión general de un árbol, es decir, cualquier nodo
de un grafo puede apuntar a cualquier otro nodo de éste (incluso a él mismo). Podemos
definir los grafos como un conjunto de vértices o nodos unidos por enlaces llamados
aristas o arcos, que permiten representar relaciones binarias entre elementos de un
conjunto.
El estudio de los grafos se estudia en una teoría o disciplina denominada teoría de
grafos. La teoría de grafos es una teoría vital en el mundo de la informática en la que se
tratan no solo temas relacionados con las características o los tipos de grafos, si no con
su implementación mediante un lenguaje de programación.
Este tipo de estructuras de datos tienen una característica que lo diferencia de las
estructuras que hemos visto hasta ahora: los grafos se usan para almacenar datos que
están relacionados de alguna manera (relaciones de parentesco, puestos de trabajo, ...);
por esta razón se puede decir que los grafos representan la estructura real de un
problema.
En lo que a ingeniería de telecomunicaciones se refiere, los grafos son una
importante herramienta de trabajo, pues se utilizan tanto para diseño de circuitos como
para calcular la mejor ruta de comunicación en Internet.

Terminología de grafos:
• Vértice: Nodo.
• Enlace: Conexión entre dos vértices (nodos).
• Adyacencia: Se dice que dos vértices son adyacentes si entre ellos hay un
enlace directo.
• Vecindad: Conjunto de vértices adyacentes a otro.
• Camino: Conjunto de vértices que hay que recorrer para llegar desde un
nodo origen hasta un nodo destino.
• Grafo conectado: Aquél que tiene camino directo entre todos los nodos.
• Grafo dirigido: Aquél cuyos enlaces son unidireccionales e indican hacia
donde están dirigidos.
• Gafo con pesos: Aquél cuyos enlaces tienen asociado un valor. En general en
este tipo de grafos no suele tener sentido que un nodo se apunte a sí mismo
porque el coste de este enlace sería nulo.

Representación en memoria de un grafo:

Hay dos formas de representar un grafo en memoria:


• Matricial: Usamos una matriz cuadrada de boolean en la que las filas
representan los nodos origen, y las columnas, los nodos destinos. De esta
forma, cada intersección entre fila y columna contiene un valor booleano
que indica si hay o no conexión entre los nodos a los que se refiere. Si se
trata de un grafo con pesos, en lugar de usar valores booleanos, usaremos
los propios pesos de cada enlace y en caso de que no exista conexión entre
dos nodos, rellenaremos esa casilla con un valor que represente un coste ∞,
es decir, con el valor Natural’Last. A esta matriz se le llama Matriz de
Adyacencia.

Ejemplo: Representación matricial del grafo anterior:


OBSERVACIÓN: En caso de que el grafo sea no dirigido, la matriz
resultante es simétrica respecto a la diagonal principal.
NOTA: La representación de un grafo mediante una matriz sólo es
válida cuando el número de nodos del grafo es fijo.

• Dinámica: Usamos listas dinámicas. De esta manera, cada nodo tiene


asociado una lista de punteros hacia los nodos a los que está conectado:

Tipos de grafos
• Grafo simple o simplemente grafo es aquel que acepta una sola arista
uniendo dos vértices cualesquiera. Esto es equivalente a decir que una arista
cualquiera es la única que une dos vértices específicos. Es la definición
estándar de un grafo.
• Multígrafo o pseudografo son grafos que aceptan más de una arista entre
dos vértices. Estas aristas se llaman múltiples o lazos (loops en inglés). Los
grafos simples son una subclase de esta categoría de grafos. También se les
llama grafos no-dirigido.
• Grafo etiquetado. Grafos en los cuales se ha añadido un peso a las aristas
(número entero generalmente) o un etiquetado a los vértices.
• Grafo aleatorio. Grafo cuyas aristas están asociadas a una probabilidad.
• Hipergrafo. Grafos en los cuales las aristas tienen más de dos extremos, es
decir, las aristas son incidentes a 3 o más vértices.
• Grafo infinito. Grafos con conjunto de vértices y aristas de cardinal infinito.
Representación de grafos en una matriz de adyacencia:
Es la forma más común de representación y la más directa. Consiste en una tabla
de tamaño V x V, en que la que a[i][j] tendrá como valor 1 si existe una arista del nodo i
al nodo j. En caso contrario, el valor será 0. Cuando se trata de grafos ponderados en lugar
de 1 el valor que tomará será el peso de la arista. Si el grafo es no dirigido hay que
asegurarse de que se marca con un 1 (o con el peso) tanto la entrada a[i][j] como la
entrada a[j][i], puesto que se puede recorrer en ambos sentidos.

int V, A;
int a[maxV][maxV];
void inicializar()
{
int i,x,y,p;
char v1,v2;
// Leer V y A
memset(a,0,sizeof(a));
for (i=1; i<=A; i++)
{
scanf("%c %c %d\n",&v1,&v2,&p);
x=v1-'A'; y=v2-'A';
a[x][y]=p; a[y][x]=p;
}
}

En esta implementación se ha supuesto que los vértices se nombran con una letra
mayúscula y no hay errores en la entrada. Evidentemente, cada problema tendrá una
forma de entrada distinta y la inicialización será conveniente adaptarla a cada situación.
En todo caso, esta operación es sencilla si el número de nodos es pequeño. Si, por el
contrario, la entrada fuese muy grande se pueden almacenar los nombres de nodos en
un árbol binario de búsqueda o utilizar una tabla de dispersión, asignando un entero a
cada nodo, que será el utilizado en la matriz de adyacencia.
Como se puede apreciar, la matriz de adyacencia siempre ocupa un espacio de V*V,
es decir, depende solamente del número de nodos y no del de aristas, por lo que será útil
para representar grafos densos.
Representación por lista de adyacencia:
Otra forma de representar un grafo es por medio de listas que definen las aristas
que conectan los nodos. Lo que se hace es definir una lista enlazada para cada nodo, que
contendrá los nodos a los cuales es posible acceder. Es decir, un nodo A tendrá una lista
enlazada asociada en la que aparecerá un elemento con una referencia al nodo B si A y B
tienen una arista que los une. Obviamente, si el grafo es no dirigido, en la lista enlazada
de B aparecerá la correspondiente referencia al nodo A.
Las listas de adyacencia serán estructuras que contendrán un valor entero (el
número que identifica al nodo destino), así como otro entero que indica el coste en el
caso de que el grafo sea ponderado. En el ejemplo se ha utilizado un nodo z ficticio en la
cola (ver listas, apartado cabeceras y centinelas).

struct nodo {
int v;
int p;
nodo *sig;
};
int V,A; // vértices y aristas del grafo
struct nodo *a[maxV], *z;
void inicializar() {
int i,x,y,peso;
char v1,v2;
struct nodo *t;
z=(struct nodo *)malloc(sizeof(struct nodo));
z->sig=z;
for (i=0; i<V; i++)
a[i]=z;
for (i=0; i<A; i++) {
scanf("%c %c %d\n",&v1,&v2,&peso);
x=v1-'A'; y=v2-'A';
t=(struct nodo *)malloc(sizeof(struct nodo));
t->v=y; t->p=peso; t->sig=a[x]; a[x]=t;

t=(struct nodo *)malloc(sizeof(struct nodo));


t->v=x; t->p=peso; t->sig=a[y]; a[y]=t;
}
}
En este caso el espacio ocupado es O(V + A), muy distinto del necesario en la matriz
de adyacencia, que era de O(V2). La representación por listas de adyacencia, por tanto,
será más adecuada para grafos dispersos.
Hay que tener en cuenta un aspecto importante y es que la implementación con
listas enlazadas determina fuertemente el tratamiento del grafo posterior. Como se
puede ver en el código, los nodos se van añadiendo a las listas según se leen las aristas,
por lo que nos encontramos que un mismo grafo con un orden distinto de las aristas en
la entrada producirá listas de adyacencia diferentes y por ello el orden en que los nodos
se procesen variará. Una consecuencia de esto es que si un problema tiene varias
soluciones la primera que se encuentre dependerá de la entrada dada. Podría presentarse
el caso de tener varias soluciones y tener que mostrarlas siguiendo un determinado
orden. Ante una situación así podría ser muy conveniente modificar la forma de meter los
nodos en la lista (por ejemplo, hacerlo al final y no al principio, o incluso insertarlo en una
posición adecuada), de manera que el algoritmo mismo diera las soluciones ya ordenadas.

Exploración de grafos:
A la hora de explorar un grafo, nos encontramos con dos métodos distintos. Ambos
conducen al mismo destino (la exploración de todos los vértices o hasta que se encuentra
uno determinado), si bien el orden en que éstos son "visitados" decide radicalmente el
tiempo de ejecución de un algoritmo, como se verá posteriormente.
En primer lugar, una forma sencilla de recorrer los vértices es mediante una función
recursiva, lo que se denomina búsqueda en profundidad. La sustitución de la recursión
(cuya base es la estructura de datos pila) por una cola nos proporciona el segundo método
de búsqueda o recorrido, la búsqueda en amplitud o anchura.
Suponiendo que el orden en que están almacenados los nodos en la estructura de
datos correspondiente es A-B-C-D-E-F... (el orden alfabético), tenemos que el orden que
seguiría el recorrido en profundidad sería el siguiente:

A-B-E-I-F-C-G-J-K-H-D

En un recorrido en anchura el orden sería, por contra:

A-B-C-D-E-G-H-I-J-K-F

Es decir, en el primer caso se exploran primero los verdes y luego los marrones,
pasando primero por los de mayor intensidad de color. En el segundo caso se exploran
primero los verdes, después los rojos, los naranjas y, por último, el rosa.
Es destacable que el nodo D es el último en explorarse en la búsqueda en
profundidad pese a ser adyacente al nodo de origen (el A). Esto es debido a que primero
se explora la rama del nodo C, que también conduce al nodo D.
En estos ejemplos hay que tener en cuenta que es fundamental el orden en que los
nodos están almacenados en las estructuras de datos. Si, por ejemplo, el nodo D estuviera
antes que el C, en la búsqueda en profundidad se tomaría primero la rama del D (con lo
que el último en visitarse sería el C), y en la búsqueda en anchura se exploraría antes el H
que el G.

Búsqueda en profundidad:
Se implementa de forma recursiva, aunque también puede realizarse con una pila.
Se utiliza un array val para almacenar el orden en que fueron explorados los vértices. Para
ello se incrementa una variable global id (inicializada a 0) cada vez que se visita un nuevo
vértice y se almacena id en la entrada del array val correspondiente al vértice que se está
explorando.
La siguiente función realiza un máximo de V (el número total de vértices) llamadas
a la función visitar, que implementamos aquí en sus dos variantes: representación por
matriz de adyacencia y por listas de adyacencia.
int id=0;
int val[V];

void buscar()
{
int k;
for (k=1; k<=V; k++)
val[k]=0;
for (k=1; k<=V; k++)
if (val[k]==0) visitar(k);
}

void visitar(int k) // matriz de adyacencia


{
int t;
val[k]=++id;
for (t=1; t<=V; t++)
if (a[k][t] && val[t]==0) visitar(t);
}

void visitar(int k) // listas de adyacencia


{
struct nodo *t;
val[k]=++id;
for (t=a[k]; t!=z; t=t->sig)
if (val[t->v]==0) visitar(t->v);
}

El resultado es que el array val contendrá en su i-ésima entrada el orden en el que


el vértice i-ésimo fue explorado. Es decir, si tenemos un grafo con cuatro nodos y fueron
explorados en el orden 3-1-2-4, el array val quedará como sigue:

val[1]=2; // el primer nodo fue visto en segundo lugar


val[2]=3; // el segundo nodo fue visto en tercer lugar
val[3]=1; // etc.
val[4]=4;

Una modificación que puede resultar especialmente útil es la creación de un array


"inverso" al array val que contenga los datos anteriores "al revés". Esto es, un array en el
que la entrada i-ésima contiene el vértice que se exploró en i-ésimo lugar. Basta modificar
la línea:
val[k]=++id;

Sustituyéndola por:
val[++id]=k;

Para el orden de exploración de ejemplo anterior los valores serían los siguientes:
val[1]=3;
val[2]=1;
val[3]=2;
val[4]=4;

Búsqueda en amplitud o anchura:


La diferencia fundamental respecto a la búsqueda en profundidad es el cambio de
estructura de datos: una cola en lugar de una pila. En esta implementación, la función del
array val y la variable id es la misma que en el método anterior.

struct tcola *cola;


void visitar(int k) // listas de adyacencia
{
struct nodo *t;
encolar(&cola,k);
while (!vacia(cola))
{
desencolar(&cola,&k);
val[k]=++id;
for (t=a[k]; t!=z; t=t->sig)
{
if (val[t->v]==0)
{
encolar(&cola,t->v);
val[t->v]=-1;
}
}
}
}
3. Recursividad
La recursividad, también llamada recursión o recurrencia, es la forma en la cual se
especifica un proceso basado en su propia definición. O sea, si se tiene un problema de
tamaño N, este puede ser dividido en instancias más pequeñas que N del mismo problema
y conociendo la solución de las instancias más simples, se puede aplicar inducción a partir
de estas asumiendo que quedan resueltas.

Programación recursiva:
Crear una subrutina recursiva requiere principalmente la definición de un "caso
base", y entonces definir reglas para subdividir casos más complejos en el caso base. Para
una subrutina recursiva es esencial que, con cada llamada recursiva, el problema se
reduzca de forma que al final llegue al caso base.
Algunos expertos clasifican la recursión como "generativa" o bien "estructural". La
distinción se hace según de donde provengan los datos con los que trabaja la subrutina.
Si los datos proceden de una estructura de datos similar a una lista, entonces la subrutina
es "estructuralmente recursiva"; en caso contrario, es "generativamente recursiva".

Problemas resueltos con recursividad:


• Factorial: Un ejemplo clásico de una subrutina recursiva es la función usada
para calcular la factorial de un entero. La función factorial se define de la
siguiente forma:
𝒔𝒊 𝒏 = 𝟎 → 𝟏
𝒇𝒂𝒄𝒕𝒐𝒓𝒊𝒂𝒍 (𝒏) = {
𝒔𝒊 𝒏 > 𝟎 → 𝒏 ∗ 𝒇𝒂𝒄𝒕𝒐𝒓𝒊𝒂𝒍 (𝒏 − 𝟏)

Pseudocódigo:
función factorial:
input: entero n de forma que n >= 0
output: [n × (n-1) × (n-2) × … × 1]
1. if n es 0, return 1
2. else, return [ n × factorial(n-1) ]
end factorial
Implementación en C:

#include<iostream>
using namespace std;

int factorial(int n);


int main()
{
int n;
cout << "Enter a positive integer: ";
cin >> n;
cout << "Factorial of " << n << " = " << factorial(n);
return 0;
}

int factorial(int n)
{
if(n > 1)
return n * factorial(n - 1);
else
return 1;
}

• Fibonacci: Otra popular secuencia recursiva es el Número de Fibonacci. Los


primeros elementos de la secuencia son: 0, 1, 1, 2, 3, 5, 8, 13, 21... La sucesión
comienza con los números 0 y 1,2 a partir de estos, «cada término es la suma
de los dos anteriores», es la relación de recurrencia que la define. Su función
es la siguiente:

Pseudocódigo:
Función fib is:
input: entero n de forma que n >= 0
1. sí n es = 0, return 0
2. sí n es = 1, return 1
3. else, return [ fib(n-1) + fib(n-2) ]
end fib
Implementación en C:
#include<stdio.h>
#include<conio.h>
int main()
{
int first_number = 0, second_number = 1, third_number, i, number;
printf("Introduzca el número de la serie fibonacci:");
scanf("%d",&number);
printf("Serie Fibonacci para un número determinado:");
printf("\n%d %d", first_number, second_number); //Para imprimir
0y1
for(i = 2; i < number; ++i) //bucle comenzará a partir de 2 porque
hemos impreso 0 y 1 antes
{
third_number = first_number + second_number;
printf(" %d", third_number);
first_number = second_number;
second_number = third_number;
}
return 0;
}

Estructuras de datos recursivo (recursión estructural)


Una aplicación de importancia de la recursión en ciencias de la computación es la
definición de estructuras de datos dinámicos tales como listas y árboles. Las estructuras
de datos recursivos pueden crecer de forma dinámica hasta un tamaño teórico infinito en
respuesta a requisitos del tiempo de ejecución; por su parte, los requisitos del tamaño de
un vector estático deben declararse en el tiempo de complicación.
4. Pilas y Colas
Las pilas y colas son estructuras de datos que se utilizan generalmente para
simplificar ciertas operaciones de programación. Estas estructuras pueden
implementarse mediante arrays o mediante listas enlazadas.

4.1. Pilas:
Las pilas son estructuras de datos que tienes dos operaciones básicas: push (para
insertar un elemento) y pop (para extraer un elemento). Su característica fundamental es
que al extraer se obtiene siempre el último elemento que acaba de insertarse. Por esta
razón también se conocen como estructuras de datos LIFO (del inglés Last In First Out)
que quiere decir que el último que entra es el primero en salir. Una posible
implementación mediante listas enlazadas sería insertando y extrayendo siempre por el
principio de la lista. Gracias a las pilas es posible el uso de la recursividad (lo veremos en
detalle en el tema siguiente). La variable que llama al mismo procedimiento en el q está,
habrá que guardarla, así como el resto de las variables de la nueva llamada, para a la
vuelta de la recursividad ir sacándolas, esto es posible a la implementación de pilas.
Las pilas se utilizan en muchas aplicaciones que utilizamos con frecuencia. Por
ejemplo, la gestión de ventanas en Windows (cuando cerramos una ventana siempre
recuperamos la que teníamos detrás). Otro ejemplo es la evaluación general de cualquier
expresión matemática para evitar tener que calcular el número de variables temporales
que hacen falta. Por ejemplo:
3 + 4 * (8 – 2 * 5)
4.2. Colas
Las colas también son llamadas FIFO (First In First Out), que quiere decir “el
primero que entra es el primero que sale”.
Colas simples:
Se inserta por un sitio y se saca por otro, en el caso de la cola simple se inserta por
el final y se saca por el principio. Para gestionar este tipo de cola hay que recordar
siempre cual es el siguiente elemento que se va a leer y cual es el último elemento que
se ha introducido.

Colas circulares:
En las colas circulares se considera que después del último elemento se accede de
nuevo al primero. De esta forma se reutilizan las posiciones extraídas, el final de la cola
es a su vez el principio, creándose un circuito cerrado.

Lo que se ha hecho es insertar (5), sacar (1), e insertar (8). Se sabrá que una tabla
está llena cuando “rear” y “front” estén en una posición de diferencia.
El teclado de ordenador se comporta exactamente como una cola circular.
Para implementar las colas circulares mediante listas enlazadas se pone en el tipo
T_Lista los punteros front y rear.

Colas con prioridad:


Las colas con prioridad se implementan mediante listas o arrays ordenados. No nos
interesa en este caso que salgan en el orden de entrada sino con una prioridad que le
asignemos. Puede darse el caso que existan varios elementos con la misma prioridad, en
este caso saldrá primero aquel que primero llego (FIFO).

4.3. Ejemplos de pilas y colas

Implementación de pilas en C:

#include <stdlib.h>
#include <stdio.h>

typedef struct _nodo {


int valor;
struct _nodo *siguiente;
} tipoNodo;

typedef tipoNodo *pNodo;


typedef tipoNodo *Pila;

/* Funciones con pilas: */


void Push(Pila *l, int v);
int Pop(Pila *l);

int main() {
Pila = NULL;

Push(&pila, 20);
Push(&pila, 10);
printf("%d, ", Pop(&pila));
Push(&pila, 40);
Push(&pila, 30);
printf("%d, ", Pop(&pila));
printf("%d, ", Pop(&pila));
Push(&pila, 90);
printf("%d, ", Pop(&pila));
printf("%d\n", Pop(&pila));

getchar();
return 0;
}

void Push(Pila *pila, int v) {


pNodo nuevo;

/* Crear un nodo nuevo */


nuevo = (pNodo)malloc(sizeof(tipoNodo));
nuevo->valor = v;

/* Añadimos la pila a continuación del nuevo nodo */


nuevo->siguiente = *pila;
/* Ahora, el comienzo de nuestra pila es en nuevo nodo */
*pila = nuevo;
}

int Pop(Pila *pila) {


pNodo nodo; /* variable auxiliar para manipular nodo */
int v; /* variable auxiliar para retorno */

/* Nodo apunta al primer elemento de la pila */


nodo = *pila;
if(!nodo) return 0; /* Si no hay nodos en la pila
retornamos 0 */
/* Asignamos a pila toda la pila menos el primer elemento
*/
*pila = nodo->siguiente;
/* Guardamos el valor de retorno */
v = nodo->valor;
/* Borrar el nodo */
free(nodo);
return v;
}
Implementación de Colas en C:
#include <stdio.h>

typedef struct _nodo {


int valor;
struct _nodo *siguiente;
} tipoNodo;

typedef tipoNodo *pNodo;

/* Funciones con colas: */


void Anadir(pNodo *primero, pNodo *ultimo, int v);
int Leer(pNodo *primero, pNodo *ultimo);

int main() {
pNodo primero = NULL, ultimo = NULL;

Anadir(&primero, &ultimo, 20);


printf("Añadir(20)\n");
Anadir(&primero, &ultimo, 10);
printf("Añadir(10)\n");
printf("Leer: %d\n", Leer(&primero, &ultimo));
Anadir(&primero, &ultimo, 40);
printf("Añadir(40)\n");
Anadir(&primero, &ultimo, 30);
printf("Añadir(30)\n");
printf("Leer: %d\n", Leer(&primero, &ultimo));
printf("Leer: %d\n", Leer(&primero, &ultimo));
Anadir(&primero, &ultimo, 90);
printf("Añadir(90)\n");
printf("Leer: %d\n", Leer(&primero, &ultimo));
printf("Leer: %d\n", Leer(&primero, &ultimo));

return 0;
}

void Anadir(pNodo *primero, pNodo *ultimo, int v) {


pNodo nuevo;

/* Crear un nodo nuevo */


nuevo = (pNodo)malloc(sizeof(tipoNodo));
nuevo->valor = v;
/* Este será el último nodo, no debe tener siguiente */
nuevo->siguiente = NULL;
/* Si la cola no estaba vacía, añadimos el nuevo a continuación
de ultimo */
if(*ultimo) (*ultimo)->siguiente = nuevo;
/* Ahora, el último elemento de la cola es el nuevo nodo */
*ultimo = nuevo;
/* Si primero es NULL, la cola estaba vacía, ahora primero
apuntará también al nuevo nodo */
if(!*primero) *primero = nuevo;
}

int Leer(pNodo *primero, pNodo *ultimo) {


pNodo nodo; /* variable auxiliar para manipular nodo */
int v; /* variable auxiliar para retorno */

/* Nodo apunta al primer elemento de la pila */


nodo = *primero;
if(!nodo) return 0; /* Si no hay nodos en la pila retornamos 0 */
/* Asignamos a primero la dirección del segundo nodo */
*primero = nodo->siguiente;
/* Guardamos el valor de retorno */
v = nodo->valor;
/* Borrar el nodo */
free(nodo);
/* Si la cola quedó vacía, ultimo debe ser NULL también*/
if(!*primero) *ultimo = NULL;
return v;
}
5. Listas enlazadas

Una lista enlazada o estructura ligada, es una estructura lineal que almacena
una colección de elementos generalmente llamados nodos, en donde cada nodo
puede almacenar datos y ligas a otros nodos. De esta manera los nodos pueden
localizarse en cualquier parte de la memoria, utilizando la referencia que lo
relaciona con otro nodo dentro de la estructura.

Las listas enlazadas son estructuras dinámicas que se utilizan para almacenar
datos que están cambiando constante mente. A diferencia de los vectores, las
estructuras dinámicas se expanden y se contraen haciéndolas más flexibles a la
hora de añadir o eliminar información.

Las listas enlazadas permiten almacenar información en posiciones de


memoria que no sean contiguas; para almacenar la información contienen
elementos llamados nodos. Estos nodos poseen dos campos uno para almacenar la
información o valor del elemento y otro para el enlace que determina la posición
del siguiente elemento o nodo de la lista.

Lo más recomendable y flexible para la creación de un nodo es utilizar un


objeto por cada nodo, para ello debe comprender cuatro conceptos fundamentales
que son:

• Clase auto referenciada: es una clase con al menos un campo cuyo tipo de
referencia es el nombre de la misma clase:

struct lista {
gint dato;
lista *siguiente;
};
En una lista enlazada, cada elemento apunta al siguiente excepto el
último que no tiene sucesor y el valor del enlace es null. Por ello los
elementos son registros que contienen el dato a almacenar y un enlace al
siguiente elemento. Los elementos de una lista suelen recibir también el
nombre de nodos de la lista.
• Nodo: es un objeto creado a partir de una clase auto referenciada.
• Campo de enlace: es la variable de instancia que contiene el tipo que
corresponde con el nombre de la clase.
• Enlace: es el contenido del campo de enlace, que hace referencia (guarda la
dirección) a otro nodo.

Para que esta estructura sea un TDA lista enlazada, debe tener unos operadores
asociados que permitan la manipulación de los datos que contiene. Los operadores
básicos de una lista enlazada son:
• Insertar: inserta un nodo con dato x en la lista, pudiendo realizarse esta
inserción al principio o final de la lista o bien en orden.
• Eliminar: elimina un nodo de la lista, puede ser según la posición o por el
dato.
• Buscar: busca un elemento en la lista.
• Localizar: obtiene la posición del nodo en la lista.
• Vaciar: borra todos los elementos de la lista.

GSList:
La definición de la estructura GSList o, lo que es lo mismo, un nodo de la lista, está
definido de la siguiente manera:
struct GSList {
gpointer data;
GSList *next;
};
• Representa el dato a almacenar. Se utiliza un puntero genérico por lo que
puede almacenar un puntero a cualquier tipo de dato o bien almacenar un
entero utilizando las macros de conversión de tipos.
• Se trata de un puntero al siguiente elemento de la lista.

Operadores de inserción en listas enlazadas:

Operador Funciones asociadas a GSList.

GSList* g_slist_prepend (GSList *list, gpointer


Insertar al principio.
data)

GSList* g_slist_append (GSList *list, gpointer


Insertar al final.
data)

Insertar en la posición GSList* g_slist_insert (GSList *list, gpointer


indicada. data, gint position)

GSList* g_slist_insert_sorted (GSList *list,


Insertar en orden.
gpointer data, GCompareFunc func)

Ejemplo: Insertar un nuevo dato en una posición determinada.


/* obtiene el número de nodos de la lista */
length = g_slist_length (list);

g_print ("\nEscribe el nº de índice donde se insertará el dato (el


indice máximo es %d): ", length);
scanf ("%d", &index);

/* inserta el valor en la posición indicada */

if (index < length) {


list = g_slist_insert (list, GINT_TO_POINTER (value), index);
print_list (list);
}
En este ejemplo se utiliza la función g_slist_length para obtener el número de
nodos que contiene la lista. A esta función hay que pasarle como parámetro la lista de la
que se desea obtener el número de nodos y devuelve como resultado el número de nodos
de ésta.

Operadores de eliminación en listas enlazadas.

Operador Funciones asociadas a GSList.

Eliminar un nodo. GSList* g_slist_remove (GSList *list, gconstpointer data)

Eliminar nodos según GSList* g_slist_remove_all (GSList *list, gconstpointer


un patrón. data)

Las dos funciones expuestas para la eliminación de nodos, si bien tienen una
definición prácticamente idéntica, el resultado obtenido es distinto. En el caso
de g_slist_remove, se eliminará el nodo que contenga el valor data. Si hay varios nodos
con el mismo valor, sólo se eliminará el primero. Si ningún nodo contiene ese valor, no se
realiza ningún cambio en el GSList. En el caso de g_slist_remove_all, se eliminan todos los
nodos de la lista que contengan el valor data y nos devuelve la nueva lista resultante de
la eliminación de los nodos.
Ejemplo: Elimina un elemento de la lista.
if (list2 != NULL) {
g_print ("\nEl dato %d será eliminado de la lista.\n", list2-
>data);

/* eliminando un elemento de la lista */


g_slist_remove (list, list2->data);
}
Operadores de búsqueda en listas enlazadas:

Operador Funciones asociadas a GSList.

Buscar un nodo GSList* g_slist_find (GSList *list, gconstpointer


según un valor. data)

Buscar un nodo GSList* g_slist_find_custom (GSList *list,


según un criterio. gconstpointer data, GCompareFunc func)

Localizar el índice GSList* g_slist_index (GSList *list,


de un nodo. gconstpointer data)

Localizar la GSList* g_slist_position (GSList *list, GSList


posición de un nodo. *llink)

Obtener el último
GSList* g_slist_last (GSList *list)
nodo.

Obtener el
g_slist_next (slist)
siguiente nodo.

Obtener un nodo
GSList* g_slist_nth (GSList *list, guint n)
por su posición.

Obtener el dato
de un nodo según su gpointer g_slist_nth_data (GSList *list, guint n)
posición.

Todas estas funciones, a excepción de g_slist_nth_data, devuelven un nodo de la


lista o NULL si el elemento no existe. La función g_slist_nth_data devuelve el valor del
elemento según la posición que se le pasa como argumento en el parámetro n o NULL si
la posición que se le pasa está más allá del final de la lista.
La función g_slist_next, es una macro que nos devuelve el siguiente nodo. Esta
macro la podemos utilizar para recorrer la lista.
Ejemplo: Función que imprime una lista.
void print_list (GSList *list) {

gint i = 0;

while (list != NULL) {


g_print ("Node %d content: %d.\n", i, list->data);

/* apunta al siguiente nodo de la lista */


list = g_slist_next (list);
i++;
}
}

Operador para vaciar la lista:

Operador Funciones asociadas a GSList.

Vacía la lista y libera la


void g_slist_free (GSList *list)
memoria usada.

La función g_slist_free libera la memoria de la lista que se le pasa como parámetro.

5.1. Listas doblemente enlazadas:


El TDA lista doblemente enlazada, al igual que la lista enlazada, es un TDA dinámico
lineal, pero, a diferencia de este, cada nodo de la lista doblemente enlazada contiene dos
punteros, de forma que uno apunta al siguiente nodo y el otro al predecesor. Esta
característica, permite que se pueda recorrer la lista en ambos sentidos, cosa que no es
posible en las listas simples.
La declaración del tipo lista doblemente enlazada de enteros es la siguiente:
struct lista_doble {
gint dato;
lista_doble *siguiente;
lista_doble *anterior;
};

• Representa el dato a almacenar, que puede ser de cualquier tipo. En este


ejemplo se trataría de una lista de enteros.

• Se trata de un puntero al siguiente elemento de la lista. Con este puntero


se enlaza con el sucesor, de forma que podamos construir la lista.

• Es un puntero al elemento anterior de la lista. Este puntero enlaza con el


elemento predecesor de la lista y permite recorrerla en sentido inverso.

Sobre este TDA se definen los mismos operadores básicos que en las listas simples.
CONCLUSIÓN
Un árbol binario es una estructura de datos en la cual cada nodo puede tener un
hijo izquierdo y un hijo derecho. En un árbol binario cada nodo puede tener cero, uno o
dos hijos (subárboles). No pueden tener más de dos hijos (de ahí el nombre "binario"). Un
árbol Bayer o B-árbol es un árbol de búsqueda que puede estar vacío o aquel cuyos nodos
pueden tener varios hijos, existiendo una relación de orden entre ellos. Los grafos no son
más que la versión general de un árbol, es decir, cualquier nodo de un grafo puede
apuntar a cualquier otro nodo de éste (incluso a él mismo). Podemos definir los grafos
como un conjunto de vértices o nodos unidos por enlaces llamados aristas o arcos, que
permiten representar relaciones binarias entre elementos de un conjunto.

La recursividad, también llamada recursión o recurrencia, es la forma en la cual se


especifica un proceso basado en su propia definición. Las pilas son estructuras de datos
que tienes dos operaciones básicas: push (para insertar un elemento) y pop (para extraer
un elemento). Su característica fundamental es que al extraer se obtiene siempre el
último elemento que acaba de insertarse. Las colas también son llamadas FIFO (First In
First Out), que quiere decir “el primero que entra es el primero que sale”. Las listas
enlazadas son estructuras dinámicas que se utilizan para almacenar datos que están
cambiando constante mente.
BIBLIOGRAFÍA
https://www.oscarblancarteblog.com/2014/08/22/estructura-de-datos-arboles/
https://es.wikipedia.org/wiki/%C3%81rbol_binario
https://es.wikipedia.org/wiki/%C3%81rbol-B
https://rootear.com/desarrollo/grafos
https://sites.google.com/site/clasearbolesb/arboles-b
http://decsai.ugr.es/~jfv/ed1/tedi/cdrom/docs/arb_B.htm
http://www.iuma.ulpgc.es/users/jmiranda/docencia/programacion/Tema9_ne.pdf
https://www.lawebdelprogramador.com/pdf/11959-Tema-5-Grafos-Estructuras-de-datos.html
https://es.wikipedia.org/wiki/Recursi%C3%B3n_(ciencias_de_computaci%C3%B3n)
https://www.ecured.cu/Recursividad
https://geekytheory.com/que-es-la-recursividad/
https://www.programiz.com/cpp-programming/examples/factorial-recursion
https://www.educba.com/fibonacci-series-in-c/
http://www.iuma.ulpgc.es/users/jmiranda/docencia/programacion/Tema4_ne.pdf
http://www.inf.udec.cl/~jlopez/FUNDPRO/apuntesC/clase12.html
http://c.conclase.net/edd/?cap=002c
http://c.conclase.net/edd/?cap=003c
https://sites.google.com/site/estdatinfjiq/unidad-iii-listas-enlazadas
http://calcifer.org/documentos/librognome/glib-lists-queues.html

También podría gustarte