Documentos de Académico
Documentos de Profesional
Documentos de Cultura
INTRODUCCIÓN ....................................................................................................................1
OBJETIVOS .............................................................................................................................2
TABLA DE ILUASTRACIONES ............................................................................................3
1. Listas ..................................................................................................................................4
1.1. Listas enlazadas. ..........................................................................................................4
Operaciones:..............................................................................................................4
Declaraciones de tipos para manejar listas .................................................................9
Recursos de Aprendizaje ......................................................................................... 18
1.2. Listas enlazadas Dobles.............................................................................................. 19
Listas dobles lineales. .............................................................................................. 19
Listas dobles circulares. ........................................................................................... 19
Operaciones básicas con listas doblemente enlazadas .............................................. 20
Recursos de Aprendizaje ......................................................................................... 31
1.3. Circulares. .................................................................................................................. 32
Añadir un elemento ................................................................................................. 33
Buscar un elemento en una lista circular .................................................................. 33
Borrar un elemento de una lista circular ................................................................... 34
Recursos de Aprendizaje ......................................................................................... 41
2. Pilas .................................................................................................................................. 42
2.1. Clases para la implementación de pilas...................................................................... 43
Recursos de Aprendizaje ......................................................................................... 52
3. Colas ................................................................................................................................. 53
3.1. Tipos. .......................................................................................................................... 53
3.2. Colas simples. ............................................................................................................. 54
3.3. Colas circulares. ......................................................................................................... 54
3.4. Colas dobles. .............................................................................................................. 55
Recursos de Aprendizaje ......................................................................................... 61
4. Árboles ............................................................................................................................. 62
4.1. Árboles Generales ...................................................................................................... 64
4.2. Árboles Binarios......................................................................................................... 65
Árbol binario de búsqueda (ABB)............................................................................ 71
4.3. Árboles AVL ............................................................................................................... 72
Altura logarítmica.................................................................................................... 73
Operaciones en Árbol AVL ..................................................................................... 73
Cambios en altura .................................................................................................... 74
Rotaciones ............................................................................................................... 74
Rotaciones en AVL ..................................................................................................... 75
4.4. Arboles B+ ................................................................................................................. 76
Recursos de Aprendizaje ....................................................................................... 103
5. Tablas Hash ................................................................................................................... 104
5.1. Funcionamiento ....................................................................................................... 104
Inserción................................................................................................................ 104
Búsqueda ............................................................................................................... 104
Prácticas recomendadas para las funciones hash .................................................... 105
Funciones Hash más usadas: .................................................................................. 106
5.2. Resolución de colisiones........................................................................................... 106
Direccionamiento Cerrado, Encadenamiento separado o Hashing abierto .............. 107
Direccionamiento abierto o Hashing cerrado.......................................................... 107
5.3. Ventajas e inconvenientes de las tablas hash ........................................................... 108
5.4. Funciones de Hash .................................................................................................. 110
Restas Sucesivas .................................................................................................... 110
Aritmética Modular ............................................................................................... 110
Mitad del Cuadrado ............................................................................................... 111
Truncamiento ........................................................................................................ 111
Plegamiento........................................................................................................... 112
Tratamiento de Colisiones ......................................................................................... 112
Prueba Lineal ........................................................................................................ 112
5.5. Hash Dinámico ........................................................................................................ 114
Métodos Totales .................................................................................................... 114
Métodos Parciales.................................................................................................. 116
Recursos de Aprendizaje ....................................................................................... 120
6. Grafos ............................................................................................................................ 121
Grafo no dirigido: .................................................................................................. 121
Grafo Dirigido ....................................................................................................... 122
6.1. Tipos de grafos. ........................................................................................................ 124
Grafo bipartito ....................................................................................................... 125
Grafo completo...................................................................................................... 125
Un grafo bipartito regular ...................................................................................... 125
Grafo nulo ............................................................................................................. 125
Grafos Isomorfos ................................................................................................... 125
Grafos Platónicos ...................................................................................................... 126
Grafos Eulerianos. ................................................................................................. 126
Grafos Conexos. .................................................................................................... 127
6.2. Representación de los Grafos ................................................................................... 128
Matriz de adyacencia................................................................................................. 128
Listas de adyacencia.................................................................................................. 129
6.3. Algoritmo de Dijkstra ............................................................................................... 130
Recursos de Aprendizaje ....................................................................................... 138
BIBLIOGRAFIA ................................................................................................................... 139
1
INTRODUCCIÓN
OBJETIVOS
Objetivo general
Objetivos específicos
TABLA DE ILUASTRACIONES
ESTRUCTURA DE DATOS - 1-1 LISTA SIMPLE 4
ESTRUCTURA DE DATOS - 1-2 LISTA DOBLEMENTE ENLAZADA 19
ESTRUCTURA DE DATOS - 1-3 LISTA DOBLEMENTE ENLAZADA CIRCULAR 19
ESTRUCTURA DE DATOS - 1-4 LISTA CIRCULAR 32
ESTRUCTURA DE DATOS - 2-1 REPRESENTACIÓN DE UNA PILA 42
ESTRUCTURA DE DATOS - 3-1 COLA CIRCULAR 54
ESTRUCTURA DE DATOS - 3-2 COLA DOBLE 55
ESTRUCTURA DE DATOS - 4-1 ÁRBOL 62
ESTRUCTURA DE DATOS - 4-2 ÁRBOL BINARIO 65
ESTRUCTURA DE DATOS - 4-3 PREORDEN 68
ESTRUCTURA DE DATOS - 4-4 INORDEN 69
ESTRUCTURA DE DATOS - 4-5 POSTORDEN 70
ESTRUCTURA DE DATOS - 4-6 ABB 71
ESTRUCTURA DE DATOS - 4-7 ÁRBOL AVL 72
ESTRUCTURA DE DATOS - 6-1 GRAFO NO DIRIGIDO 121
ESTRUCTURA DE DATOS - 6-2 GRAFO DIRIGIDO 122
ESTRUCTURA DE DATOS - 6-3 GRAFO NULO 125
ESTRUCTURA DE DATOS - 6-4 GRAFOS ISOMORFOS 126
ESTRUCTURA DE DATOS - 6-5 GRAFOS PLATÓNICOS 126
ESTRUCTURA DE DATOS - 6-6 GRAFOS CONEXOS 127
ESTRUCTURA DE DATOS - 6-7 MATRIZ DE ADYACENCIA 128
ESTRUCTURA DE DATOS - 6-8 LISTA DE ADYACENCIA 129
1. Listas
Una lista enlazada o encadenada es una colección de elementos ó nodos, en donde cada uno
contiene datos y un enlace o liga. Un nodo es una secuencia de caracteres en memoria dividida en
campos (de cualquier tipo). Un nodo siempre contiene la dirección de memoria del siguiente nodo
de información si este existe.
Dato Enlace
El campo enlace, que es de tipo puntero, es el que se usa para establecer la liga con el
siguiente nodo de la lista. Si el nodo fuera el último, este campo recibe como valor NULL (vacío).
A continuación, se muestra el esquema de una lista:
Operaciones:
Las operaciones que podemos realizar sobre una lista enlazada son las siguientes:
I. Recorrido.
Esta operación consiste en visitar cada uno de los nodos que forman la lista. Para recorrer
todos los nodos de la lista, se comienza con el primero, se toma el valor del campo enlace para
avanzar al segundo nodo, el campo enlace de este nodo nos dará la dirección del tercer nodo, y así
sucesivamente.
II. Inserción.
Esta operación consiste en agregar un nuevo nodo a la lista. Para esta operación se pueden
considerar 4 casos:
De nuevo partiremos de un nodo a insertar, con un puntero que apunte a él, y de una lista, en este
caso no vacía:
Suponemos que ya disponemos del nuevo nodo a insertar, apuntado por nodo, y un puntero al nodo
a continuación del que lo insertaremos.
Es muy importante que el programa nunca pierda el valor del puntero al primer elemento, ya
que si no existe ninguna copia de ese valor, y se pierde, será imposible acceder al nodo y no
podremos liberar el espacio de memoria que ocupa.
III. Borrado.
La operación de borrado consiste en quitar un nodo de la lista, redefiniendo los enlaces que
correspondan. Se pueden presentar cuatro casos:
Es el caso más simple. Partiremos de una lista con uno o más nodos, y usaremos un puntero
auxiliar, nodo:
Si no guardamos el puntero al primer nodo antes de actualizar Lista, después nos resultaría
imposible liberar la memoria que ocupa. Si liberamos la memoria antes de actualizar Lista,
perderemos el puntero al segundo nodo.
Si la lista sólo tiene un nodo, el proceso es también válido, ya que el valor de Lista.siguiente
es NULO, y después de eliminar el primer nodo la lista quedará vacía, y el valor de Lista será
NULO.
Una operación que se suele usar para borrar listas completas es eliminar el primer nodo hasta
que la lista esté vacía.
En todos los demás casos, eliminar un nodo se puede hacer siempre del mismo modo. Supongamos
que tenemos una lista con al menos dos elementos, y un puntero al nodo anterior al que queremos
eliminar. Y un puntero auxiliar nodo.
2. Ahora, asignamos como nodo siguiente del nodo anterior, el siguiente al que queremos
eliminar: anterior.sig = nodo.sig
IV. Búsqueda.
Esta operación consiste en visitar cada uno de los nodos, tomando al campo liga como
puntero al siguiente nodo a visitar.
Normalmente se definen varios tipos que facilitan el manejo de las listas, la declaración de tipos
puede tener una forma parecida a esta:
struct nodo {
int dato;
nodo *siguiente;
}
Lista es el tipo para declarar listas, como puede verse, un puntero a un nodo y una lista son la
misma cosa. En realidad, cualquier puntero a un nodo es una lista, cuyo primer elemento es el nodo
apuntado.
Ejemplos
Lista enlazada con inserción al inicio en C++
// Lista enlazada con un puntero al primer nodo e insercion al inicio de la lista
// Archivos de Cabecera
#include <iostream.h>
#include <conio.h>
// Funcion Principal
int main()
{
// Despliegue de la Lista
cout << "\n\nElementos de la Lista: \n";
q = lista;
while (q != NULL) {
cout << q->nro << "\t"; // muestra el elemento de la lista
q = q->sgte;
}
while (!kbhit());
// Archivos de Cabecera
#include <iostream.h>
#include <conio.h>
#include <ctype.h>
#include <stdlib.h>
// Funcion Principal
void main(void)
{
// Inicializacion de la lista
lista = NULL;
do {
} while (toupper(cont)=='S');
// Despliega la Lista
cout << "\n\nListado: \n";
nuevo = lista;
while (nuevo!=NULL)
{
cout << nuevo->nro << "\t";
nuevo = nuevo->sgte;
}
system ("pause");
}
#include <iostream.h>
#include <conio.h>
// Definicion de la estructura
struct nodo {
int nro;
struct nodo *sgte;
};
int main()
{
// Declaracion de Variables
TLista lista=NULL;
int opcion, valor, pos;
// Menu principal
do {
clrscr();
cout << "*** Menu Principal ***";
cout << "\n1. Insertar al Inicio";
cout << "\n2. Insertar al Final";
cout << "\n3. Insertar en una Posición";
cout << "\n4. Desplegar la Lista";
cout << "\n5. Buscar Valor";
cout << "\n6. Salir del Programa";
cout << "\n\n Ingrese su opción: ";
cin >> opcion;
switch(opcion)
{
case 1: cout << "Valor a insertar al inicio: ";
cin >> valor;
insertaAlInicio(lista,valor);
break;
return 0;
} // Fin del Main
// Funcion que imprime la Lista
void imprimir(TLista lista)
{
cout << "\n\nLista:\n";
while (lista !=NULL)
{
cout << lista->nro << "\t";
lista = lista->sgte;
}
while (!kbhit());
}
if (t==NULL)
{
cout << "El valor ingresado no existe";
while (!kbhit());
return;
}
else
while (t->nro != valor)
if (t->sgte == NULL) {
cout << "El valor ingresado no existe";
while (!kbhit());
return; }
else {
t = t->sgte;
i++;
}
cout << "El valor esta en la posicion: " << i;
while (!kbhit());
return;
}
if (lista==NULL)
lista=q;
else
{
t = lista;
while (t->sgte!=NULL)
t=t->sgte;
t->sgte=q;
}
}
// Verificar posicion
if (pos==1){
q->sgte = lista;
lista=q;
}
else {
t=lista;
for (i=1; t!=NULL; i++)
{
if (i==pos-1)
{
q->sgte = t->sgte;
t->sgte = q;
return;
}
t=t->sgte;
} // Fin del For
} // Fin del Else
Recursos de Aprendizaje
Podemos encontrar los siguientes recursos, enlaces de videos y/o páginas en donde
encontraremos más información y ejemplos sobre los temas.
Listas enlazadas: Teoría
https://youtu.be/0NzAFk1CwaQ?list=PLTd5ehIj0goMTSK7RRAPBF4wP-Nj5DRvT
Li Dato Ld
Debido a que las listas dobles circulares son más eficientes, los algoritmos que en esta sección
se traten serán sobre listas dobles circulares.
En la figura siguiente se muestra un ejemplo de una lista doblemente enlazada lineal que
almacena números:
En la figura siguiente se muestra un ejemplo de una lista doblemente enlazada circular que
almacena números:
La inserción se efectuara antes de cierta posición pasado como argumento a la función. La posición
indicada no debe ser ni el primer ni el último elemento. En ese caso hay que utilizar las funciones
de inserción al inicio y/o al final de la lista.
Pasos:
Asignación de memoria al nuevo elemento
Rellenar el campo de datos del nuevo elemento
Elegir una posición en la lista (la inserción se hará después de la posición elegida)
El puntero siguiente del nuevo elemento apunta hacia el elemento actual.
El puntero anterior del nuevo elemento apunta hacia la dirección hacia la que apunta el puntero
anterior del elemento actual.
El puntero siguiente del elemento que precede al elemento actual apuntará hacia el nuevo elemento
El puntero anterior del elemento actual apunta hacia el nuevo elemento
El puntero fin no cambia
El puntero inicio cambia si la posición elegida es la posición 1
Pasos:
Asignación de memoria al nuevo elemento
Rellenar el campo de datos del nuevo elemento
Elegir una posición en la lista (la inserción se hará después de la posición elegida)
El puntero siguiente del nuevo elemento apunta hacia el elemento actual.
El puntero anterior del nuevo elemento apunta hacia la dirección hacia la que apunta el puntero
anterior del elemento actual.
El puntero siguiente del elemento que precede al elemento actual apuntará hacia el nuevo elemento
El puntero anterior del elemento actual apunta hacia el nuevo elemento
El puntero fin no cambia
El puntero inicio cambia si la posición elegida es la posición 1
El puntero anterior del elemento que va después del elemento actual apuntará hacia el nuevo
elemento.
El puntero siguiente del elemento actual apunta hacia el nuevo elemento
El puntero inicio no cambia
El puntero fin cambia si la posición elegida es la posición del último elemento (el tamaño de la
lista)
Ejemplos
Lista enlazada doble - Insertar Nodo y Desplegar lista
#include <iostream>
struct nodo{
int dato;
nodo* siguiente;
nodo* atras;
} *primero, *ultimo;
void insertarNodo();
void desplegarListaPU();
void desplegarListaUP();
int main(){
insertarNodo();
insertarNodo();
insertarNodo();
insertarNodo();
insertarNodo();
cout << "\n Lista Primero al ultimo\n";
desplegarListaPU();
cout << "\n Lista ultimo al Primero\n";
desplegarListaUP();
return 0;
}
void insertarNodo(){
nodo* nuevo = new nodo();
cout << " Ingrese el dato que contendra el nuevo Nodo: ";
cin >> nuevo->dato;
if(primero==NULL){
primero = nuevo;
primero->siguiente = NULL;
primero->atras = NULL;
ultimo = primero;
}else{
ultimo->siguiente = nuevo;
nuevo->siguiente = NULL;
nuevo->atras = ultimo;
ultimo = nuevo;
}
cout << "\n Nodo Ingresado\n\n";
void desplegarListaPU(){
nodo* actual = new nodo();
actual = primero;
if(primero!=NULL){
while(actual!=NULL){
cout << "\n " << actual->dato;
actual = actual->siguiente;
}
}else{
cout << "\n La listas se encuentra Vacia\n\n";
}
}
void desplegarListaUP(){
nodo* actual = new nodo();
actual = ultimo;
if(primero!=NULL){
while(actual!=NULL){
cout << "\n " << actual->dato;
actual = actual->atras;
}
}else{
cout << "\n La listas se encuentra Vacia\n\n";
}
}
using namespace::std;
struct nodo{
int dato;
nodo* siguiente;
nodo* atras;
} *primero, *ultimo;
void insertarNodo();
void buscarNodo();
void desplegarListaPU();
void desplegarListaUP();
int main(){
int opcion_menu=0;
do
{
cout << "\n|-------------------------------------|";
cout << "\n| ° LISTA DOBLE ° |";
cout << "\n|------------------|------------------|";
cout << "\n| 1. Insertar | 5. Desplegar P.U |";
cout << "\n| 2. Buscar | 6. Desplegar U.P |";
cout << "\n| 3. Modificar | 7. Salir |";
cout << "\n| 4. Eliminar | |";
cout << "\n|------------------|------------------|";
cout << "\n\n Escoja una Opcion: ";
cin >> opcion_menu;
switch(opcion_menu){
case 1:
cout << "\n\n INSERTA NODO EN LA LISTA \n\n";
insertarNodo();
break;
case 2:
cout << "\n\n BUSCAR UN NODO EN LA LISTA \n\n";
buscarNodo();
break;
case 3:
cout << "\n\n MODIFICAR UN NODO DE LA LISTA \n\n";
break;
case 4:
cout << "\n\n ELIMINAR UN NODO DE LA LISTA \n\n";
break;
case 5:
cout << "\n\n DESPLEGAR LISTA DE NODOS DEL PRIMERO
AL UTLIMO\n\n";
desplegarListaPU();
break;
case 6:
cout << "\n\n DESPLEGAR LISTA DE NODOS DEL UTLIMO AL
PRIMERO\n\n";
desplegarListaUP();
break;
case 7:
cout << "\n\n Programa finalizado...";
break;
default:
cout << "\n\n Opcion No Valida \n\n";
}
} while (opcion_menu != 7);
return 0;
}
// lista doble NULL <- 45 -> <- 67 -> <- 78 -> <- 12 -> NULL
void insertarNodo(){
nodo* nuevo = new nodo();
cout << " Ingrese el dato que contendra el nuevo Nodo: ";
cin >> nuevo->dato;
if(primero==NULL){
primero = nuevo;
primero->siguiente = NULL;
primero->atras = NULL;
ultimo = primero;
}else{
ultimo->siguiente = nuevo;
nuevo->siguiente = NULL;
nuevo->atras = ultimo;
ultimo = nuevo;
}
cout << "\n Nodo Ingresado\n\n";
}
void buscarNodo(){
nodo* actual = new nodo();
actual = primero;
bool encontrado = false;
int nodoBuscado = 0;
cout << " Ingrese el dato del Nodo a Buscar: ";
cin >> nodoBuscado;
if(primero!=NULL){
if(actual->dato == nodoBuscado){
cout << "\n Nodo con el dato ( " << nodoBuscado << " )
Encontrado\n\n";
encontrado = true;
}
actual = actual->siguiente;
}
if(!encontrado){
cout << "\n Nodo no Encontrado\n\n";
}
}else{
cout << "\n La listas se encuentra Vacia\n\n";
}
}
void desplegarListaPU(){
nodo* actual = new nodo();
actual = primero;
if(primero!=NULL){
while(actual!=NULL){
cout << "\n " << actual->dato;
actual = actual->siguiente;
}
}else{
cout << "\n La listas se encuentra Vacia\n\n";
}
}
void desplegarListaUP(){
nodo* actual = new nodo();
actual = ultimo;
if(primero!=NULL){
while(actual!=NULL){
cout << "\n " << actual->dato;
actual = actual->atras;
}
}else{
cout << "\n La listas se encuentra Vacia\n\n";
}
}
Recursos de Aprendizaje
Podemos encontrar los siguientes recursos, enlaces de videos y páginas en donde encontraremos
más información y ejemplos sobre los temas.
Lista Doblemente Enlazadas- Creación e inserción (java)
https://youtu.be/8oCjWIJJI9c?list=PLCLpAU8VN0j4RGemFfybZrWoSX57NbEq9
1.3. Circulares.
Las listas circulares tienen la característica de que el último elemento de la misma apunta al
primero.
Una lista circular es una lista lineal en la que el último nodo a punta al primero. Las listas circulares
evitan excepciones en las operaciones que se realicen sobre ellas. No existen casos especiales, cada
nodo siempre tiene uno anterior y uno siguiente. En algunas listas circulares se añade un nodo
especial de cabecera, de ese modo se evita la única excepción posible, la de que la lista esté vacía.
Los tipos que definiremos normalmente para manejar listas cerradas son los mismos que para para
manejar listas abiertas:
tipoNodo es el tipo para declarar nodos, evidentemente. pNodo es el tipo para declarar punteros a
un nodo. Lista es el tipo para declarar listas, tanto abiertas como circulares. En el caso de las
circulares, apuntará a un nodo cualquiera de la lista. A pesar de que las listas circulares
simplifiquen las operaciones sobre ellas, también introducen algunas complicaciones. Por ejemplo,
en un proceso de búsqueda, no es tan sencillo dar por terminada la búsqueda cuando el elemento
buscado no existe. Por ese motivo se suele resaltar un nodo en particular, que no tiene por qué ser
siempre el mismo. Cualquier nodo puede cumplir ese propósito, y puede variar durante la
ejecución del programa. Otra alternativa que se usa a menudo, y que simplifica en cierto modo el
uso de listas circulares es crear un nodo especial de hará la función de nodo cabecera. De este
modo, la lista nunca estará vacía, y se eliminan casi todos los casos especiales.
Operaciones básicas con listas circulares: A todos los efectos, las listas circulares son como las
listas abiertas en cuanto a las operaciones que se pueden realizar sobre ellas:
c) Borrar elementos.
Añadir un elemento
El único caso especial a la hora de insertar nodos en listas circulares es cuando la lista esté vacía.
En el primer caso necesitamos localizar el nodo anterior al que queremos borrar. Como el
principio de la lista puede ser cualquier nodo, haremos que sea precisamente lista quien apunte al
nodo anterior al que queremos eliminar.
Esto elimina la excepción del segundo caso, ya que lista nunca será el nodo a eliminar, salvo
que sea el único nodo de la lista.
Una vez localizado el nodo anterior y apuntado por lista, hacemos que lista->siguiente apunte
a nodo->siguiente. Y a continuación borramos nodo.
En el caso de que sólo exista un nodo, será imposible localizar el nodo anterior, así que
simplemente eliminaremos el nodo, y haremos que lista valga NULL.
El primer paso es conseguir que lista apunte al nodo anterior al que queremos eliminar. Esto
se consigue haciendo que lista valga lista->siguiente mientras lista->siguiente sea distinto de nodo.
Hacemos que lista->siguiente apunte a nodo->siguiente.
Eliminamos el nodo.
Este caso es mucho más sencillo. Si lista es el único nodo de una lista circular: Borramos el nodo
apuntado por lista.
Hacemos que lista = NULL
Otro algoritmo para borrar nodos: Existe un modo alternativo de eliminar un nodo en una
lista circular con más de un nodo.
Supongamos que queremos eliminar un nodo apuntado por nodo:
Copiamos el contenido del nodo->siguiente sobre el contenido de nodo.
Hacemos que nodo->siguiente apunte a nodo->siguiente->siguiente.
Eliminamos nodo->siguiente.
Si lista es el nodo->siguiente, hacemos lista = nodo.
Este método también funciona con listas circulares de un sólo elemento, salvo que el nodo a
borrar es el único nodo que existe, y hay que hacer que lista = NULL.
Ejemplos
nodo = *lista;
int main() {
Lista lista = NULL;
pNodo p;
Insertar(&lista, 10);
Insertar(&lista, 40);
Insertar(&lista, 30);
Insertar(&lista, 20);
Insertar(&lista, 50);
MostrarLista(lista);
Borrar(&lista, 30);
Borrar(&lista, 50);
MostrarLista(lista);
BorrarLista(&lista);
return 0;
}
pNodo nodo;
nodo = *lista;
do {
printf("%d -> ", nodo->valor);
nodo = nodo->siguiente;
} while(nodo != lista);
printf("\n");
}
Recursos de Aprendizaje
Podemos encontrar los siguientes recursos, enlaces de videos y páginas en donde
encontraremos más información y ejemplos sobre los temas.
2. Pilas
Las pilas son otro tipo de estructura de datos lineales, las cuales presentan restricciones
en cuanto a la posición en la cual pueden realizarse las inserciones y las extracciones
de elementos. Una pila es una lista de elementos en la que se pueden insertar y eliminar
elementos sólo por uno de los extremos.
Como consecuencia, los elementos de una pila serán eliminados en orden inverso al que
se insertaron. Es decir, el último elemento que se metió a la pila será el primero en salir
de ella.
En la vida cotidiana existen muchos ejemplos de pilas, una pila de platos en una alacena,
una pila de latas en un supermercado, una pila de papeles sobre un escritorio, etc.
Debido al orden en que se insertan y eliminan los elementos en una pila, también se le
conoce como estructura LIFO (Last In, First Out: último en entrar, primero en salir).
Definición.
Una pila (stack en inglés) es una estructura de datos de tipo LIFO (del inglés Last In First
Out, último en entrar, primero en salir) que permite almacenar y recuperar datos. Se
aplica en multitud de ocasiones en informática debido a su simplicidad y ordenación
implícita en la propia estructura.
Operaciones.
Para el manejo de los datos se cuenta con dos operaciones básicas: apilar (push), que
coloca un objeto en la pila, y su operación inversa, retirar (o desapilar, pop), que retira el
último elemento apilado.
En cada momento sólo se tiene acceso a la parte superior de la pila, es decir, al último
objeto apliado (denominado TOS, top of stack en inglés). La operación retirar permite la
Por analogía con objetos cotidianos, una operación apilar equivaldría a colocar un plato
sobre una pila de platos, y una operación retirar a retirarlo.
Ejemplos
Algoritmo de la función "push"
v = nodo->valor;
/* Borrar el nodo */
free(nodo);
return v;
}
#include <stdlib.h>
#include <stdio.h>
int main() {
Pila pila = NULL;
Push(&pila, 20);
Push(&pila, 10);
printf("%d, ", Pop(&pila));
Push(&pila, 40);
Push(&pila, 30);
Push(&pila, 90);
printf("%d, ", Pop(&pila));
printf("%d\n", Pop(&pila));
getchar();
return 0;
}
struct pilas
{
int d;
pilas *a;
}*c,*e;
void menu(void);
void ingresar(void);
void sacar (void);
void actualizar_pila(void);
main()
{
menu();
}
void menu(void)
{
int y,opc;
for(;;)
{
cout<<"\n1. Ingresar datos";
cout<<"\t2. Sacar datos";
cout<<"\t0. Terminar";
cout<<"\n Ingrese opcion: ";cin>>opc;
switch(opc)
{
case 1:
ingresar();
break;
case 2: sacar();
break;
case 0: exit(1);
default: cout<<"\n Opcion no valida!!"; break;
}
actualizar_pila();
cout<<"\n\nOprima una tecla para continuar";
getch();
}
}
e=new(pilas);
cout<<"\nIngrese elemento: ";
cin>>e->d;
e->a=c;
c=e;
}
void sacar(void)
{
if(!c)
{
cout<<"\n\nNo hay elementos!!";
return;
}
e=new(pilas);
e=c;
cout<<"\n\nElemento eliminado: " <<e->d;
c=e->a;
delete(e);
}
void actualizar_pila(void)
{
int y=2,i,ca=0;
e=c;
while(e)
{
ca++;
e=e->a;
}
for(i=0;i<=ca;i++)
{
cout<<" ";
}
//muestro lo que tiene la pila!!
i=0;
e=c;
while(e)
{
cout<<"\n";
cout<<++i<<" - "<<e->d;
e=e->a;
}
}
Recursos de Aprendizaje
Podemos encontrar los siguientes recursos, enlaces de videos y/o páginas en donde
encontraremos más información y ejemplos sobre los temas.
Pilas-Teoría
https://youtu.be/JDlhpEuLUrQ?list=PLTd5ehIj0goMTSK7RRAPBF4wP-Nj5DRvT
Pilas en C++
https://youtu.be/KMbf3UHaAs4?list=PLTd5ehIj0goMTSK7RRAPBF4wP-Nj5DRvT
Pilas (Java)
https://youtu.be/kRp_g3ywOjY?list=PLCLpAU8VN0j4RGemFfybZrWoSX57NbEq9
3. Colas
Una cola es una estructura de almacenamiento, donde la podemos considerar como una lista de
elementos, en la que éstos van a ser insertados por un extremo y serán extraídos por otro.
Las colas son estructuras de tipo FIFO (first-in, first-out), ya que el primer elemento en entrar a la
cola será el primero en salir de ella.
Existen muchísimos ejemplos de colas en la vida real, como, por ejemplo: personas esperando en
un teléfono público, niños esperando para subir a un juego mecánico, estudiantes esperando para
subir a un camión escolar, etc.
Definición.
Una cola es una estructura de datos, caracterizada por ser una secuencia de elementos en la que la
operación de inserción push se realiza por un extremo y la operación de extracción pop por el otro.
También se le llama estructura FIFO (del inglés First In First Out), debido a que el primer elemento
en entrar será también el primero en salir.
3.1. Tipos.
Colas de prioridad:
En ellas, los elementos se atienden en el orden indicado por una prioridad asociada a cada
uno. Si varios elementos tienen la misma prioridad, se atenderán de modo convencional según la
posición que ocupen.
· Bicolas: son colas en donde los nodos se pueden añadir y quitar por ambos extremos; se les llama
DEQUE (Double Ended QUEue). Para representar las bicolas lo podemos hacer con un array
circular con Ini y Fin que apunten a cada uno de los extremos. Hay variantes:
· Bicolas de entrada restringida: Son aquellas donde la inserción sólo se hace por el final, aunque
podemos eliminar al principio ó al final.
· Bicolas de salida restringida: Son aquellas donde sólo se elimina por el final, aunque se puede
insertar al principio y al final.
Colas de prioridad:
son las que cumplen dos reglas:
1 De dos elementos siempre se atenderá antes al que tenga mayor prioridad.
2 Si dos elementos tienen la misma prioridad se atiende primero el que llego antes.
Realización Se ponen todos los nodos en la misma cola. Su particularidad es que cada nodo tiene
un campo adicional con la prioridad del dato; de tal forma que cuando insertamos nuevos datos, el
nuevo nodo, se inserta al final de la cola de los que tengan su misma prioridad.
Esta significa el primero en llegar es el primero en salir. La estructura de este es como cualquier
tipo de fila como: la cola que tienes que hacer en un lugar esperando a ser atendido y como todos
sabemos, el primero en llegar estará en frente de la cola y será el primero en salir.
In- Sirve para ingresar los valores que se van agregando, este valor será el primero en la cola y se
quedara con el apuntador externo "front", los demás serán parte de la cola y el ultimo que este en
la cola será apuntado por el apuntador externo "end".
Out- Sirve para sacar el primer valor que fue ingresado. Si hay solo un valor en la estructura,
simplemente ya no hay más valores en la pila. Si se intenta sacar un valor cuando no hay valores
se avisará que no hay valores.
La condición de vacío en este tipo de cola es que el apuntador F sea igual a cero.
Las condiciones que debemos tener presentes al trabajar con este tipo de estructura son las
siguientes:
· Over flow, cuando se realice una inserción.
· Under flow, cuando se requiera de una extracción en la cola.
· Vacio
La primera variante sólo acepta inserciones al final de la cola, y la segunda acepta eliminaciones
sólo al frente de la cola.
I. Operaciones.
Las operaciones que nosotros podemos realizar sobre una cola son las
siguientes:
· Inserción.
· Extracción.
Las inserciones en la cola se llevarán a cabo por atrás de la cola, mientras que las eliminaciones se
realizarán por el frente de la cola (hay que recordar que el primero en entrar es el primero en salir).
Podría hacerse con un array secuencial, como se muestra en las siguientes figuras. 'Entrada' es la
posición de entrada a la cola, y 'Salida' por donde salen.
se desencola, obteniendo un 3:
se encola un 7:
Enseguida se aprecia que esto tiene un grave defecto, y es que llega un momento en el que se
desborda la capacidad del array. Una solución nada efectiva es incrementar su tamaño. Esta
implementación es sencilla pero totalmente ineficaz.
Como alternativa se usa el array circular. Esta estructura nos permite volver al comienzo del array
cuando se llegue al final, ya sea el índice de entrada o el índice de salida.
Ejemplos
Cola en C++
#include <iostream>
using namespace std;
class nodo {
public:
nodo(int v, nodo *sig = NULL) {
valor = v;
siguiente = sig;
}
private:
int valor;
nodo *siguiente;
class cola {
public:
cola() : ultimo(NULL), primero(NULL) {}
~cola();
int Pop();
private:
pnodo ultimo;
};
cola::~cola() {
while(primero) Leer();
}
void cola::Anadir(int v) {
pnodo nuevo;
int cola::Leer() {
Cola.Anadir(20);
cout << "Añadir(20)" << endl;
Cola.Anadir(10);
cout << "Añadir(10)" << endl;
cout << "Leer: " << Cola.Leer() << endl;
Cola.Anadir(40);
cout << "Añadir(40)" << endl;
Cola.Anadir(30);
cout << "Añadir(30)" << endl;
cout << "Leer: " << Cola.Leer() << endl;
cout << "Leer: " << Cola.Leer() << endl;
Cola.Anadir(90);
return 0;
Recursos de Aprendizaje
Podemos encontrar los siguientes recursos, enlaces de videos y/o páginas en donde
encontraremos más información y ejemplos sobre los temas.
Colas: Teoría
https://youtu.be/6i2f6k5PPjs?list=PLTd5ehIj0goMTSK7RRAPBF4wP-Nj5DRvT
Colas en C
https://youtu.be/nicbZo7sI_w?list=PLTd5ehIj0goMTSK7RRAPBF4wP-Nj5DRvT
4. Árboles
A los árboles ordenados de grado dos se les conocen como árboles binarios ya que cada
nodo del árbol no tendrá más de dos descendientes directos. Las aplicaciones de los árboles
binarios son muy variadas ya que se les puede utilizar para representar una estructura en la cual es
posible tomar decisiones con dos opciones en distintos puntos.
I. Definición.
En ciencias de la computación, un árbol es una estructura de datos ampliamente usada que
emula la forma de un árbol (un conjunto de nodos conectados). Un nodo es la unidad sobre la que
se construye el árbol y puede tener cero o más nodos hijos conectados a él. Se dice que un nodo a
es padre de un nodo b si existe un enlace desde a hasta b (en ese caso, también decimos que b es
hijo de a). Sólo puede haber un único nodo sin padres, que llamaremos raíz. Un nodo que no tiene
hijos se conoce como hoja.
Una sucesión de nodos del árbol, de forma que entre cada dos nodos consecutivos de la
sucesión haya una relación de parentesco, decimos que es un recorrido árbol.
Existen dos recorridos típicos para listar los nodos de un árbol: primero en profundidad
y primero en anchura.
En el primer caso, se listan los nodos expandiendo el hijo actual de cada nodo hasta llegar a
una hoja, donde se vuelve al nodo anterior probando por el siguiente hijo y así sucesivamente.
En el segundo, por su parte, antes de listar los nodos de nivel n + 1 (a distancia n + 1 aristas de
la raíz), se deben haber listado todos los de nivel n. Otros recorridos típicos del árbol son preorden,
postorden e inorden:
• Por medio de datos tipo punteros también conocidos como variables dinámicas o listas.
• Por medio de arreglos.
Sin embargo, la más utilizada es la primera, puesto que es la más natural para tratar este
tipo de estructuras.
Los nodos del árbol binario serán representados como registros que contendrán como
mínimo tres campos. En un campo se almacenará la información del nodo. Los dos restantes se
utilizarán para apuntar al subárbol izquierdo y derecho del subárbol en cuestión. Cada nodo se
representa gráficamente de la siguiente manera:
Los árboles pueden ser construidos con estructuras estáticas y dinámicas. Las estáticas son
arreglos, registros y conjuntos, mientras que las dinámicas están representadas por listas.
La definición de árbol es la siguiente: es una estructura jerárquica aplicada sobre una colección
de elementos u objetos llamados nodos; uno de los cuales es conocido como raíz. Además se crea
una relación o parentesco entre los nodos dando lugar a términos como padre, hijo, hermano,
antecesor, sucesor, ancestro, etc.. Formalmente se define un árbol de tipo T como una estructura
homogénea que es la concatenación de un elemento de tipo T junto con un número finito de árboles
disjuntos, llamados subárboles. Una forma particular de árbol puede ser la estructura vacía.
Se utiliza la recursión para definir un árbol porque representa la forma más apropiada y porque
además es una característica inherente de los mismos.
Los árboles tienen una gran variedad de aplicaciones. Por ejemplo, se pueden utilizar para
representar fórmulas matemáticas, para organizar adecuadamente la información, para construir
un árbol genealógico, para el análisis de circuitos eléctricos y para numerar los capítulos y
secciones de un libro.
Los árboles binarios son estructuras de datos muy similares a las listas doblemente enlazadas,
en el sentido que tienen dos punteros que apuntan a otros elementos, pero no tienen una estructura
lógica de tipo lineal o secuencial como aquellas, sino ramificada. Tienen aspecto de árbol, de ahí
su nombre.
Un árbol binario es una estructura de datos no lineal en la que cada nodo puede apuntar a uno
o máximo a dos nodos. También se suele dar una definición recursiva que indica que es una
estructura compuesta por un dato y dos árboles. Esto son definiciones simples. Este tipo de árbol
se caracteriza porque tienen un vértice principal y de él se desprende dos ramas. La rama izquierda
y la rama derecha a las que también se les conoce como subárboles.
La rama izquierda y la derecha, también son dos árboles binarios. El Vértice principal se
denomina raíz y cada una de las ramas se puede denominar como subárbol izquierdo y subárbol
derecho.
Teniendo en cuenta la gráfica del árbol binario de la imagen anterior podemos identificar
algunas generalidades y partes del árbol.
Nodo: Un árbol binario es un conjunto de elementos cada uno de los cuales se denomina nodo.
Un árbol Binario puede tener cero nodos y este caso se dice que está vacío. Puede tener un sólo
nodo, y en este caso solamente existe la raíz del árbol o puede tener un número finito de nodos.
Cada nodo puede estar ramificado por la izquierda o por la derecha o puede no tener ninguna
ramificación.
Con relación al tipo de nodos que hacen parte de los árboles, se identifican algunos nodos:
Nodo hijo: cualquiera de los nodos apuntados por uno de los nodos del árbol. En la gráfica de
la imagen, se tiene, ‘D’ y ‘M’ son hijos de ‘A’.
Nodo padre: nodo que contiene un puntero al nodo actual. En el ejemplo, el nodo ‘A’ es padre
de ‘D’ y ‘D’.
Los árboles con los que trabajará tienen otra característica importante: cada nodo sólo puede
ser apuntado por otro nodo, es decir, cada nodo sólo tendrá un padre. Esto hace que estos árboles
estén fuertemente jerarquizados, y es lo que en realidad les da la apariencia de árboles.
Nodo raíz: nodo que no tiene padre. Este es el nodo que usaremos para referirnos al árbol. En
el ejemplo anterior, es el nodo ‘A’.
Nodo hoja: nodo que no tiene hijos. En el ejemplo hay varios: ‘L’, ‘K’, ‘I’, ‘E’.
Existen otros conceptos que definen las características del árbol, en relación a su tamaño:
Orden: es el número potencial de hijos que puede tener cada elemento de árbol. De este modo,
se dice que un árbol en el que cada nodo puede apuntar a otros dos es de orden dos, si puede
apuntar a tres será de orden tres y así sucesivamente.
Grado: el número de hijos que tiene el elemento con más hijos dentro del árbol. En el árbol
del ejemplo en la imagen 2, el grado es dos, ya que tanto ‘A’ como ‘D’ y ‘M’ tienen dos hijos, y
no existen elementos con más de dos hijos.
Nivel: se define para cada elemento del árbol como la distancia a la raíz, medida en nodos. El
nivel de la raíz siempre será cero y el de sus hijos uno. Así sucesivamente. En el ejemplo de la
imagen 2, el nodo ‘D’ tiene nivel 1, el nodo ‘L’ tiene nivel 2.
Altura: la altura de un árbol se define como el nivel del nodo de mayor nivel. Como cada nodo
de un árbol puede considerarse a su vez como la raíz de un árbol, también se puede hablar de altura
de ramas.
Los árboles binarios, son estructuras de datos no lineales, son considerados como estructuras
jerárquicas y como tal su forma de recorrerlos difiere sustancialmente en comparación con las
listas enlazadas que son estructuras de datos de tipo lineal. En ese orden de ideas, el recorrido
de un árbol binario se lleva a cabo en tres sentidos: Preorden, Inorden y Postorden.
I. Recorrido en Preorden:
• Examinar la raíz.
El recorrido inicia con el subárbol izquierdo, el primer nodo a visitar es la raíz que es el nodo
10, luego se visita el subárbol izquierdo con el nodo 5, posteriormente el 3, luego el nodo 1, sigue
con el nodo 4, pasamos al nodo 7 y luego el 9.
Continuamos con el recorrido del subárbol derecho en preorden, con la visita del nodo 15,
luego el 14, se continúa con el 17, se visita el 16 y se finaliza con la visita del nodo 20.
• Examinar la raíz.
El recorrido inicia con el subárbol izquierdo, el primer nodo a visitar es el 3 luego se visita el
5 y posteriormente el 7, con esto se garantiza que el recorrido del subárbol izquierdo se hizo en
Inorden.
Finalizado el recorrido del subárbol izquierdo se visita el nodo de la raíz, que para este caso
es el número 10.
• Examinar la raíz.
El recorrido inicia con el subárbol izquierdo, el primer nodo a visitar es el 3 luego se visita el
7 y posteriormente el 5 que es la raíz, con esto se garantiza que el recorrido del subárbol izquierdo
se hizo en Postorden.
Solo queda recorrer la raíz del árbol que para este caso es el número 10.
3 – 7 – 5 – 11 – 15 – 12 – 10.
Partiendo de la gráfica del árbol de la imagen 6 realizaremos los tres recorridos, conservando
el orden correspondiente para cada uno.
Recorrido en preorden: 20 – 13 – 9 – 6 – 10 – 18 – 14 – 17 – 32 – 26 – 24 – 29 – 36 – 34 – 40
Recorrido en inorden: 6 – 9 – 10 – 13 – 14 – 18 – 17 – 20 – 24 – 26 – 29 – 32 – 34 – 36 – 40
Recorrido en postorden: 6 – 10 – 9 – 14 – 17 – 18 – 13 – 24 – 29 – 26 – 34 – 40 – 36 – 32 –
20
Altura logarítmica
Todo árbol binario con equilibrado AVL tiene altura logarítmica
Se define árbol de Fibonacci ( Fh ) como:
F-1 es el árbol vacío.
F0 es el árbol con un único nodo.
Fh es el árbol con subárbol izquierdo Fh-2 y derecho Fh-1
El árbol Fh tiene altura h y número de elementos:
Cambios en altura
En inserción (dH > 0), si un hijo ( y) incrementa su altura, el padre ( x) también la incrementa
si su factor de equilibrio era -1 o 0 (hijo izquierdo) o bien 0 o +1 (hijo derecho)
En borrado (dH < 0), si un hijo ( y) decrementa su altura, el padre ( x) también la decrementa
si su factor de equilibrio era -1 (hijo izquierdo) o +1 (hijo derecho)
Rotaciones
Una rotación es una reestructuración local de un subárbol BB que mantiene la propiedad de
ordenación.
Rotaciones en AVL
Tras una operación de inserción o borrado, se recorren los ascendientes, recalculando
sus factores de equilibrio y teniendo en cuenta el cambio en altura del subárbol.
Es posible que en el recorrido el factor de equilibrio de algún nodo pasa a valer +2 ó -2
(desequilibrado).
En ese caso se aplica una determinada rotación que restablece el equilibrio del nodo (aunque
es posible que cambie la altura del nodo).
En un árbol AVL se necesitan 2 tipos de rotaciones (simples y dobles), en un sentido u otro
(izquierdas y derechas).
Teniendo en cuenta los distintos ajustes de factores de equilibrio y posibles resultados respecto
al cambio de altura, existen seis casos a considerar.
4.4. Arboles B+
Los arboles B+ son una variante de los arboles B, se diferencian en que los arboles B+ toda
la información se encuentra almacenada en las hojas. En la raíz y en las páginas internas se
encuentran almacenado índices o claves para llegar a un dato.
Árbol B+ 4-1
Ejemplo de inserción:
-Insertar las siguientes claves a un árbol de orden 5: 10-27-29-17-25-21-15-31-13-51-20-24-48-
19-60-35-66
II. Eliminación:
La operación de eliminación en árboles-B+ es más simple que en árboles-B. Esto ocurre porque
las claves a eliminar siempre se encuentran en las páginas hojas. En general deben distinguirse
los siguientes casos:
Si al eliminar una clave, la cantidad de llaves queda mayor o igual que [m/2] entonces termina la
operación. Las claves de los nodos raíz o internos no se modifican por más que sean una copia de
la clave eliminada en las hojas.
Si al eliminar una clave, la cantidad de llaves queda menor que [m/2] entonces debe realizarse
una redistribución de claves, tanto en el índice como en las páginas hojas.
Ejemplo del caso 1:
En resumen:
El árbol B+ mantiene varios órdenes. Dentro de cada nodo existe orden. Entre las hojas existe
orden. En los punteros dentro de un nodo existe orden.
Ejemplos
1 Árbol Binario de Búsqueda (ABB) en C++
class ArbolABB {
private:
//// Clase local de Lista para Nodo de ArbolBinario:
class Nodo {
public:
// Constructor:
Nodo(const int dat, Nodo *izq=NULL, Nodo *der=NULL) :
dato(dat), izquierdo(izq), derecho(der) {}
// Miembros:
int dato;
Nodo *izquierdo;
Nodo *derecho;
};
public:
// Constructor y destructor básicos:
ArbolABB() : raíz(NULL), actual(NULL) {}
~ArbolABB() { Podar(raíz); }
// Insertar en árbol ordenado:
void main(void)
{ ABB arbol=NULL; // inicializa árbol
int opc, n;
do
{ cout<<endl<<"*** Menu ***"<<endl;
cout<<"1. Insertar"<<endl;
cout<<"2. Búsqueda recursiva"<<endl;
cout<<"3. Búsqueda iterativa"<<endl;
cout<<"4. Ver árbol"<<endl;
cout<<"5. Salir"<<endl;
cout <<"Ingrese opción:";
cin>>opc;
switch(opc)
{ case 1: cout <<"Valor a insertar: "; cin>>n;
inserta(arbol, n);
break;
case 2: cout <<"Valor a buscar recursivamente: "; cin>>n;
if(busquedaRec(arbol, n)==1)
cout<<"Elemento encontrado";
else cout<<"Elemento no encontrado";
break;
case 3: cout <<"Valor a buscar iterativamente: "; cin>>n;
if(busquedaIter(arbol, n)==1)
cout<<"Elemento encontrado";
else cout<<"Elemento no encontrado";
break;
case 4: verArbol(arbol, 0); // imprime arbol
break;
} // fin de switch
}while(opc!=5);
} // fin de main( )
void inserta(ABB &arbol, int n)
{ if (arbol==NULL)
{ arbol = new (struct nodo);
arbol->valor = n;
arbol->izq = NULL;
arbol->der = NULL;
}
else if (n<arbol->valor) inserta(arbol->izq, n);
else if (n>arbol->valor) inserta(arbol->der, n);
}
void verArbol(ABB arbol, int nro)
{ int i;
if (arbol==NULL) return;
verArbol(arbol->der, nro+1);
for (i=0; i<nro; i++) cout <<" ";
cout <<arbol->valor<<endl;
verArbol(arbol->izq, nro+1);
}
if(Padre)
if(Padre->derecho == P) Padre->derecho = Q;
else Padre->izquierdo = Q;
else *a = Q;
/* Reconstruir árbol: */
P->izquierdo = B;
Q->derecho = P;
/* Reasignar padres: */
P->padre = Q;
if(B) B->padre = P;
Q->padre = Padre;
if(Padre)
if(Padre->derecho == nodo) Padre->derecho = R;
else Padre->izquierdo = R;
else *raíz = R;
/* Reconstruir árbol: */
Q->derecho = B;
P->izquierdo = C;
R->izquierdo = Q;
R->derecho = P;
/* Reasignar padres: */
R->padre = Padre;
P->padre = Q->padre = R;
if(B) B->padre = Q;
if(C) C->padre = P;
switch(R->FE) {
case -1: Q->FE = 0; P->FE = 1; break;
case 0: Q->FE = 0; P->FE = 0; break;
case 1: Q->FE = -1; P->FE = 0; break;
}
R->FE = 0;
}
#include <iostream.h>
#include <stdlib.h>
{ ABB arbol=NULL;
int n, x;
cout<<"Cantidad de elementos del arbol: ";
cin>>n;
for (int i=0; i<n; i++)
{ cout<<"Ingrese nodo número "<<i<<": ";
cin>>x;
inserta(arbol,x);
}
verArbol(arbol, 0);
cout<< endl<<"Preorden : "; preorden(arbol);
cout<< endl<<"Postorden : "; postorden(arbol);
cout<< endl<<"Enorden : "; enorden(arbol);
system("pause");
}
{ if (arbol!=NULL)
{ enorden(arbol->izq);
cout<<arbol->valor<<" ";
enorden(arbol->der);
}
}
void postorden(ABB arbol)
{ if (arbol!=NULL)
{ postorden(arbol->izq);
postorden(arbol->der);
cout<<arbol->valor<<" ";
}
}
void verArbol(ABB arbol, int nro)
{ int i;
if (arbol==NULL) return;
verArbol(arbol->der, nro+1);
for (i=0; i<nro; i++) cout <<" ";
cout <<arbol->valor<<endl;
verArbol(arbol->izq, nro+1);
}
#include <iostream>
using namespace std;
class AVL;
class AVL {
private:
enum {IZQUIERDO, DERECHO};
// Punteros de la lista, para cabeza y nodo actual:
Nodo *raiz;
Nodo *actual;
int contador;
int altura;
public:
// Constructor y destructor básicos:
AVL() : raiz(NULL), actual(NULL) {}
~AVL() { Podar(raiz); }
// Insertar en árbol ordenado:
void Insertar(const int dat);
// Borrar un elemento del árbol:
void Borrar(const int dat);
// Función de búsqueda:
bool Buscar(const int dat);
// Comprobar si el árbol está vacío:
bool Vacio(Nodo *r) { return r==NULL; }
// Comprobar si es un nodo hoja:
bool EsHoja(Nodo *r) { return !r->derecho && !r->izquierdo; }
// Contar número de nodos:
const int NumeroNodos();
const int AlturaArbol();
// Calcular altura de un dato:
int Altura(const int dat);
// Devolver referencia al dato del nodo actual:
int &ValorActual() { return actual->dato; }
// Moverse al nodo raiz:
if(Padre)
if(Padre->derecho == nodo) Padre->derecho = R;
else Padre->izquierdo = R;
else raiz = R;
// Reconstruir árbol:
Q->derecho = B;
P->izquierdo = C;
R->izquierdo = Q;
R->derecho = P;
// Reasignar padres:
R->padre = Padre;
P->padre = Q->padre = R;
if(B) B->padre = Q;
if(C) C->padre = P;
if(Padre)
if(Padre->derecho == nodo) Padre->derecho = R;
else Padre->izquierdo = R;
else raiz = R;
// Reconstruir árbol:
P->derecho = B;
Q->izquierdo = C;
R->izquierdo = P;
R->derecho = Q;
// Reasignar padres:
R->padre = Padre;
P->padre = Q->padre = R;
if(B) B->padre = P;
if(C) C->padre = Q;
if(Padre)
if(Padre->derecho == P) Padre->derecho = Q;
else Padre->izquierdo = Q;
else raiz = Q;
// Reconstruir árbol:
P->izquierdo = B;
Q->derecho = P;
// Reasignar padres:
P->padre = Q;
if(B) B->padre = P;
Q->padre = Padre;
if(Padre)
if(Padre->derecho == P) Padre->derecho = Q;
else Padre->izquierdo = Q;
else raiz = Q;
// Reconstruir árbol:
P->derecho = B;
Q->izquierdo = P;
// Reasignar padres:
P->padre = Q;
if(B) B->padre = P;
Q->padre = Padre;
{
Nodo *padre = NULL;
Nodo *nodo;
int aux;
actual = raiz;
// Mientras sea posible que el valor esté en el árbol
while(!Vacio(actual)) {
if(dat == actual->dato) { // Si el valor está en el nodo actual
if(EsHoja(actual)) { // Y si además es un nodo hoja: lo borramos
if(padre) // Si tiene padre (no es el nodo raiz)
// Anulamos el puntero que le hace referencia
if(padre->derecho == actual) padre->derecho = NULL;
else if(padre->izquierdo == actual) padre->izquierdo = NULL;
delete actual; // Borrar el nodo
actual = NULL;
// El nodo padre del actual puede ser ahora un nodo hoja, por lo
tanto su
// FE es cero, pero debemos seguir el camino a partir de su
padre, si existe.
if((padre->derecho == actual && padre->FE == 1) ||
(padre->izquierdo == actual && padre->FE == -1)) {
padre->FE = 0;
actual = padre;
padre = actual->padre;
}
if(padre)
if(padre->derecho == actual) Equilibrar(padre, DERECHO,
false);
else Equilibrar(padre, IZQUIERDO,
false);
return;
}
else { // Si el valor está en el nodo actual, pero no es hoja
// Buscar nodo
padre = actual;
// Buscar nodo más izquierdo de rama derecha
if(actual->derecho) {
nodo = actual->derecho;
while(nodo->izquierdo) {
padre = nodo;
nodo = nodo->izquierdo;
}
}
// O buscar nodo más derecho de rama izquierda
else {
nodo = actual->izquierdo;
while(nodo->derecho) {
padre = nodo;
nodo = nodo->derecho;
}
}
// Intercambiar valores de no a borrar u nodo encontrado
// y continuar, cerrando el bucle. El nodo encontrado no tiene
// por qué ser un nodo hoja, cerrando el bucle nos aseguramos
// de que sólo se eliminan nodos hoja.
aux = actual->dato;
actual->dato = nodo->dato;
nodo->dato = aux;
actual = nodo;
}
}
else { // Todavía no hemos encontrado el valor, seguir buscándolo
padre = actual;
if(dat > actual->dato) actual = actual->derecho;
else if(dat < actual->dato) actual = actual->izquierdo;
}
}
}
// Calcular la altura del árbol, que es la altura del nodo de mayor altura.
const int AVL::AlturaArbol()
{
altura = 0;
int main()
{
// Un árbol de enteros
AVL ArbolInt;
ArbolInt.Borrar(8);
ArbolInt.Borrar(11);
cin.get();
// Arbol de cadenas:
AVL<Cadena> ArbolCad;
// Inserción de valores:
ArbolCad.Insertar("Hola");
ArbolCad.Insertar(",");
ArbolCad.Insertar("somos");
ArbolCad.Insertar("buenos");
ArbolCad.Insertar("programadores");
// Borrar "buenos":
ArbolCad.Borrar("buenos");
cout << "Borrar 'buenos': ";
ArbolCad.InOrden(Mostrar);
cout << endl; */
cin.get();
return 0;
}
Recursos de Aprendizaje
Podemos encontrar los siguientes recursos, enlaces de videos y/o páginas en donde
encontraremos más información y ejemplos sobre los temas.
5. Tablas Hash
5.1. Funcionamiento
Las operaciones básicas implementadas en las tablas hash son: inserción (llave, valor),
búsqueda(llave) que devuelve valor, la mayoría de las implementaciones también incluyen
borrar(llave).
También se pueden ofrecer funciones como iteración en la tabla, crecimiento y vaciado. Algunas
tablas hash permiten almacenar múltiples valores bajo la misma clave.
Una función resumen (hash) cuyo dominio sea el espacio de claves y su imagen (o rango) los
números naturales.
Inserción
Para almacenar un elemento en la tabla hash se ha de convertir su clave a un número. Esto se
consigue aplicando la función resumen (hash) a la clave del elemento.
Si en la posición de la tabla ya había otro elemento, se ha producido una colisión. Este problema
se puede solucionar asociando una lista a cada posición de la tabla, aplicando otra función o
buscando el siguiente elemento libre. Estas posibilidades han de considerarse a la hora de recuperar
los datos.
Búsqueda
Para recuperar los datos, es necesario únicamente conocer la clave del elemento, a la cual se le
aplica la función resumen.
Si el elemento existente en la posición indicada en el paso anterior tiene la misma clave que la
empleada en la búsqueda, entonces es el deseado. Si la clave es distinta, se ha de buscar el elemento
según la técnica empleada para resolver el problema de las colisiones al almacenar el elemento.
En una función hash ideal, el cambio de un simple bit en la llave (incluyendo el hacer la llave más
larga o más corta) debería cambiar la mitad de los bits del hash, y este cambio debería ser
independiente de los cambios provocados por otros bits de la llave. Como una función hash puede
ser difícil de diseñar, o computacionalmente cara de ejecución, se han invertido muchos esfuerzos
en el desarrollo de estrategias para la resolución de colisiones que mitiguen el mal rendimiento del
hasheo. Sin embargo, ninguna de estas estrategias es tan efectiva como el desarrollo de una buena
función hash de principio.
Es deseable utilizar la misma función hash para arrays de cualquier tamaño concebible. Para esto,
el índice de su ubicación en el array de la tabla hash se calcula generalmente en dos pasos:
2. Este valor es reducido a un índice válido en el vector encontrando su módulo con respecto al
tamaño del array.
El tamaño del vector de las tablas hash es con frecuencia un número primo. Esto se hace con el
objetivo de evitar la tendencia de que los hash de enteros grandes tengan divisores comunes con
el tamaño de la tabla hash, lo que provocaría colisiones tras el cálculo del módulo. Sin embargo,
el uso de una tabla de tamaño primo no es un sustituto a una buena función hash.
Un problema bastante común que ocurre con las funciones hash es el aglomeramiento. El
aglomeramiento ocurre cuando la estructura de la función hash provoca que llaves usadas
comúnmente tiendan a caer muy cerca unas de otras o incluso consecutivamente en la tabla hash.
Esto puede degradar el rendimiento de manera significativa, cuando la tabla se llena usando ciertas
estrategias de resolución de colisiones, como el sondeo lineal.
Cuando se depura el manejo de las colisiones en una tabla hash, suele ser útil usar una función
hash que devuelva siempre un valor constante, como 1, que cause colisión en cada inserción.
Dado un diccionario D, se fija un número m >= |D| (m mayor o igual al tamaño del diccionario) y
que sea primo no cercano a potencia de 2 o de 10. Siendo k la clave a buscar y h(k) la función
hash, se tiene h(k)=k % m (Resto de la división k/m).
2. Hash de Multiplicación
Si por alguna razón, se necesita una tabla hash con tantos elementos o punteros como una potencia
de 2 o de 10, será mejor usar una función hash de multiplicación, independiente del tamaño de la
tabla. Se escoge un tamaño de tabla m >= |D| (m mayor o igual al tamaño del diccionario) y un
cierto número irracional φ (normalmente se usa 1+5^(1/2)/2 o 1-5^(1/2)/2). De este modo se define
h(k)= Suelo (m*Parte fraccionaria(k*φ))
Para dar una idea de la importancia de una buena estrategia de resolución de colisiones, considérese
el siguiente resultado, derivado de la paradoja de las fechas de nacimiento. Aun cuando
supongamos que el resultado de nuestra función hash genera índices aleatorios distribuidos
uniformemente en todo el vector, e incluso para vectores de 1 millón de entradas, hay un 95% de
posibilidades de que al menos una colisión ocurra antes de alcanzar los 2.500 registros.
Las tablas hash encadenadas heredan las desventajas de las listas ligadas. Cuando se almacenan
cantidades de información pequeñas, el gasto extra de las listas ligadas puede ser significativo.
También los viajes a través de las listas tienen un rendimiento de caché muy pobre.
Otras estructuras de datos pueden ser utilizadas para el encadenamiento en lugar de las listas
ligadas. Al usarárboles auto-balanceables, por ejemplo, el tiempo teórico del peor de los casos
disminuye de O(n) a O(log n). Sin embargo, dado que se supone que cada lista debe ser pequeña,
esta estrategia es normalmente ineficiente a menos que la tabla hash sea diseñada para correr a
máxima capacidad o existan índices de colisión particularmente grandes. También se pueden
utilizar vectores dinámicos para disminuir el espacio extra requerido y mejorar el rendimiento del
caché cuando los registros son pequeños.
sondeo lineal
sondeo cuadrático
en el que el intervalo entre los intentos aumenta linealmente (por lo que los índices son
descritos por una función cuadrática), y
doble hasheo
en el que el intervalo entre intentos es constante para cada registro pero es calculado por
otra función hash.
El sondeo lineal ofrece el mejor rendimiento del caché, pero es más sensible al aglomeramiento,
en tanto que el doble hasheo tiene pobre rendimiento en el caché pero elimina el problema de
aglomeramiento. El sondeo cuadrático se sitúa en medio. El doble hasheo también puede requerir
más cálculos que las otras formas de sondeo.
Una razón de ocupación no muy elevada (a partir del 75% de ocupación se producen
demasiadas colisiones y la tabla se vuelve ineficiente).
Una función resumen que distribuya uniformemente las claves. Si la función está mal
diseñada, se producirán muchas colisiones.
La función de hash ideal debería ser biyectiva, esto es, que a cada elemento le corresponda un
índice, y que a cada índice le corresponda un elemento, pero no siempre es fácil encontrar esa
función, e incluso a veces es inútil, ya que puedes no saber el número de elementos a almacenar.
La función de hash depende de cada problema y de cada finalidad, y se pueden utilizar con números
o cadenas, pero las más utilizadas son:
Restas Sucesivas
Esta función se emplea con claves numéricas entre las que existen huecos de tamaño conocido,
obteniéndose direcciones consecutivas. Un ejemplo serían los alumnos de ingeniería en sistemas
que entraron en el año 2005 sus números de control son consecutivos y está definido el número de
alumnos.
05210800 -05210800 → 0
05210801 -05210800 → 1
05210802 -05210800 → 2
05210899 -05210800 → 99
Aritmética Modular
El índice de un número es resto de la división de ese número entre un número N prefijado,
preferentemente primo. Los números se guardarán en las direcciones de memoria de 0 a N-1. Este
método tiene el problema de que dos o más elementos pueden producir el mismo residuo y un
índice puede ser señalado por varios elementos. A este fenómeno se le llama colisión. Si el número
N es el 7, los números siguientes quedan transformados en:
1679 → 6
4567 → 3
8471 → 1
0435 → 1
5033 → 0
Mientras más grande sea número de elementos es mejor escoger un número primo mayor para
seccionar el arreglo en más partes. El número elegido da el número de partes en que se secciona
el arreglo, y las cada sección está compuesta por todos los elementos que arrojen el mismo residuo,
y mientras más pequeñas sean las secciones la búsqueda se agilizara más que es lo que nos interesa.
709^2=502681 → 26
456^2=207936 → 79
105^2=011025 → 10
879^2=772641 → 26
619^2=383161 → 31
Nota: en caso de que la cifra resultante tenga un número de dígitos impar, se toma el valor número
y el anterior.
Truncamiento
Consiste en ignorar parte del número y utilizar los elementos restantes como índice. También se
produce colisión. Por ejemplo, si un número de 7 cifras se debe ordenar en un arreglo de elementos,
se pueden tomar el segundo, el cuarto y el sexto para formar un nuevo número:
5700931 → 703
3498610 → 481
0056241 → 064
9134720 → 142
5174829 → 142
Plegamiento
Consiste en dividir el número en diferentes partes, y operar con ellas (normalmente con suma o
multiplicación). También se produce colisión. Por ejemplo, si dividimos el número de 7 cifras en
2, 2 y 3 cifras y se suman, dará otro número de tres cifras (y si no, se toman las tres últimas cifras):
Nota: Estas solo son sugerencias y que con cada problema se pude implementar una nueva función
hash que incluso tu puedes inventar o formular.
Tratamiento de Colisiones
Hay diferentes maneras de solucionarlas, pero lo más efectivo es en vez de crear un arreglo de
número, crear un arreglo de punteros, donde cada puntero señala el principio de una lista enlazada.
Así, cada elemento que llega a un determinado índice se pone en el último lugar de la lista de ese
índice. El tiempo de búsqueda se reduce considerablemente, y no hace falta poner restricciones al
tamaño del arreglo, ya que se pueden añadir nodos dinámicamente a la lista.
Prueba Lineal
Consiste en que una vez detectada la colisión se debe recorrer el arreglo secuencialmente a partir
del punto de colisión, buscando al elemento. El proceso de búsqueda concluye cuando el elemento
es hallado, o bien cuando se encuentra una posición vacía. Se trata al arreglo como a una estructura
circular: el siguiente elemento después del último es el primero. La función de rehashing es, por
tanto, de la forma: R(H(X)) = (H(X) + 1) % m (siendo m el tamaño del arreglo) Ejemplo: Si la
posición 397 ya estaba ocupada, el registro con clave 0596397 es colocado en la posición 398, la
cual se encuentra disponible. Una vez que el registro ha sido insertado en esta posición, otro
registro que genere la posición 397 o la 398 es insertado en la posición siguiente disponible.
1. No existen funciones hash perfectas que permitan asegurar que por cada transformación de un
elemento habrá una única correspondencia en la clave que contiene este elemento.
2. Son estructuras estáticas que no pueden crecer ya que necesitan un tamaño fijo para el
funcionamiento de la estructura.
Métodos Totales
I. Método de las expansiones totales
El método de las expansiones totales consiste en realizar una duplicación del tamaño del arreglo
establecido para realizar la tabla hash, esta expansión se ejecuta cuando se supera la densidad de
ocupación.2 Así si se tiene una tabla hash de tamaño N, al realizar la expansión total se obtendrá
una tabla hash de 2N, al realizar una segunda expansión se obtendrá una tabla hash de 4N, al
realizar una tercera expansión se obtendrá una tabla hash de 8N y en general el tamaño de la tabla
para una i-ésima expansión se define como aparece a continuación:
Dónde:
Dónde:
Cada vez que se pretende insertar un elemento es necesario calcular la densidad de ocupación, si
se supera esta densidad se procede a implementar la expansión. Al realizar cada de una de las
expansiones es necesario volver a implementar la función hash para cada uno de los registros
almacenados en la tabla y volver a insertarlos de nuevo en la tabla.
Dónde:
Para realizar una reducción la densidad de ocupación se debe disminuir a un valor menor al rango
establecido y los registros se deben eliminar de tal manera que los registros resultantes se puedan
ingresar en una tabla hash que posea la mitad del tamaño de la tabla original. Cada vez que se
implementa una reducción es necesario volver a utilizar la función hash con cada uno de los
registros almacenados.
Métodos Parciales
I. Método de las expansiones parciales
El método de las expansiones parciales consiste en incrementar en un 50% el tamaño del arreglo
establecido para realizar la tabla hash, esta expansión se ejecuta cuando se supera la densidad de
ocupación. Así si se tiene una tabla hash de tamaño N, al realizar la expansión parcial se obtendrá
una tabla hash de 1.5 N, al realizar una segunda expansión se obtendrá una tabla hash de 2.25 N,
al realizar una tercera expansión se obtendrá una tabla hash de 3.375 N y en general el tamaño de
la tabla para una i-ésima expansión se define como:
T = ↓( (1.5) ^ i * N )
Dónde:
Cada vez que se pretende insertar un elemento es necesario calcular la densidad de ocupación, si
se supera esta densidad se procede a implementar la expansión. Al realizar cada de una de las
expansiones es necesario volver a implementar la función hash para cada uno de los registros
almacenados en la tabla hash y volver a insertarlos de nuevo en la tabla.
T = ↑((0.5)^i*N)
Dónde:
Para realizar una reducción la densidad de ocupación debe disminuir a un valor menor al rango
establecido y los registros se deben eliminar de tal manera que los registros resultantes se puedan
ingresar en una tabla hash que posea la mitad del tamaño de la tabla original. Cada vez que se
implementa una reducción es necesario volver a utilizar la función hash con cada uno de los
registros almacenados.
Ejemplos
Funciones Hash
} else {
j++;
}
} while (j < m && !i);
return -1;
} else if (h[j].dato == n) {
if (h[j].estado == 1) {
return -1;
} else {
return j;
}
} else {
j++;
}
}
return -1;
}
Recursos de Aprendizaje
Podemos encontrar los siguientes recursos, enlaces de videos y/o páginas en donde
encontraremos más información y ejemplos sobre los temas.
Que son las Tablas Hash?
https://youtu.be/LluB6jU-SwY
Métodos de Búsqueda, Funciones Hash, Tablas Hash, Teoría (Java)
https://youtu.be/gYBnEDIn054?list=PLCLpAU8VN0j4RGemFfybZrWoSX57NbEq9
6. Grafos
Los grafos son nodos (o vértices) unidos por líneas (o aristas). Según las aristas podemos
encontrarnos grafos no dirigidos o grafos dirigidos, según si las aristas tienen dirección o no.
Grafo no dirigido:
La forma de representar el grafo en modo de ecuación es G = (N, A), siendo N(G) el conjunto de
nodos (vértices) y A(G) el conjunto de aristas del grafo. La representación del grafo de ejemplo
sería el siguiente:
N(G) = { 1, 2, 3, 4, 5, 6 }
Una arista que se enlaza un nodo consigo mismo se la denomina bucle o lazo.
Dos nodos son adyacentes si siendo distintos existe una arista que los une.
camino simple, es un camino que no emplea más de una vez la misma arista;
camino compuesto, cuando el camino emplea dos veces la misma arista (aunque no de forma
continua).
Grafo Dirigido
N(G) = { 1, 2, 3, 4, 5, 6 }
grado de entrada (o entrante) de un vértice que es el número de aristas que llegan a él (con
dirección hacia él); y el
grado de salida (o saliente) de un vértice que es el número de aristas que salen de él (con
dirección hacia otros vértices).
Un camino, en un grafo dirigido, es una secuencia finita de aristas entre dos vértices, siendo el
extremo final de cada arista el extremo inicial de la siguiente.
Un grafo dirigido es fuertemente conexo si para cada par de vértices distintos n y m hay un
camino de n a m y viceversa. El grafo dirigido expuesto como ejemplo tiene esta propiedad, ya
que se puede acceder a cualquier nodo desde cualquier otro.
Dos vértices o nodos están conectados si hay un camino que los une.
Las aristas, al igual que los vértices, también pueden contener información, este es el caso de
los grafos etiquetados (o valorados). Se representan de la siguiente forma:
G = (N,A,P)
N(G) = { 1, 2, 3, 4, 5, 6 }
P(G) = { 5, 8, 1, 10, 2, 6, 7 }
La representación gráfica:
Ejemplos
G1 = (V1, A1)
V1 = {1, 2, 3, 4} A1 = {(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)}
G2 = (V2, A2) V2 = {1, 2, 3, 4, 5, 6} A2 = {(1, 2), (1, 3), (2, 4), (2, 5), (3, 6)}
G3 = (V3, A3)
V3 = {1, 2, 3} A3 = {<1, 2>, <2, 1>, <2, 3>}
Algunos de los principales tipos de grafos son los que se muestran a continuación:
Grafo regular: Aquel con el mismo grado en todos los vértices. Si ese grado es k lo
llamaremos k-regular.
Grafo bipartito
Es aquel con cuyos vértices pueden formarse dos conjuntos disjuntos de modo que no haya
adyacencias entre vértices pertenecientes al mismo conjunto.
Ejemplo. - de los dos grafos siguientes el primero es bipartito y el segundo no lo es.
Grafo completo
Aquel con una arista entre cada par de vértices. Un grafo completo con n vértices se denota Kn.
A continuación, pueden verse los dibujos de K3, K4, K5 y K6.
Grafo nulo
Se dice que un grafo es nulo cuando los vértices que lo componen no están conectados, esto es,
que son vértices aislados.
Grafos Isomorfos
Dos grafos son isomorfos cuando existe una correspondencia biunívoca (uno a uno), entre sus
vértices de tal forma que dos de estos quedan unidos por una arista en común.
Grafos Platónicos
Son los Grafos formados por los vértices y aristas de los cinco sólidos regulares (Sólidos
Platónicos), a saber, el tetraedro, el cubo, el octaedro, el dodecaedro y el icosaedro.
Grafos Eulerianos.
Grafos Conexos.
Matriz de adyacencia
Se crea una matriz (la llamaremos MA por Matriz de Adyacencias) asociada a G, de modo que
tenga n x n elementos. Indicaremos sus contenidos con 0 y 1, siendo 1 que en la
posición MA[i,j] existe una arista y 0 lo contrario.
Su matriz de adyacencia sería la siguiente (teniendo en cuenta las filas como el origen y las
columnas como el destino):
En los grafos no dirigidos cada arista es simétrica. En el caso de los grafos valorados, los valores
que se representan en la tabla ya no serían 0 y 1, sino el valor del peso de cada arista.
Determinar cuántas aristas hay en un grafo tiene un coste de O(n2), y requiere de un espacio de
memoria de θ(n2). Por lo que no es aconsejado si hay pocas aristas.
Listas de adyacencia
Se representa por un vector de n listas, siendo n el número de nodos, por lo que contiene tantas
listas como nodos hay en el grafo. Por lo que el grafo que se muestra a continuación:
En el caso de los grafos etiquetados, las listas deberían incluir un campo adicional para
almacenar la etiqueta asociada a cada arista.
Determinar si existe una arista entre el nodo i y el nodo j puede llevar un tiempo de O(n), ya que
puede haber n vértices en la lista asociada de ese vértice en concreto. El coste en espacio de esta
representación está en θ(n+a), siendo n el número de nodos y a el número de aristas.
I. Aplicaciones
En múltiples aplicaciones donde se aplican los grafos, es necesario conocer el camino de menor
costo entre dos vértices dados:
El problema del camino más corto de un vértice a otro consiste en determinar el camino de
menor costo, desde un vértice u a otro vértice v. El costo de un camino es la suma de los costos
(pesos) de los arcos que lo conforman.
Es un algoritmo greddy.
Trabaja por etapas, y toma en cada etapa la mejor solución sin considerar consecuencias
futuras.
El óptimo encontrado en una etapa puede modificarse posteriormente si surge una solución
mejor.
Ejemplos
Algoritmo de Dijkstra – Ejemplo
Procedemos con las iteraciones, partimos de la iteración 0, es decir, asignar la etiqueta para el
nodo origen:
Iteración 0
En este paso, asignamos una etiqueta permanente para el nodo origen. Recordemos que la
etiqueta se compone por un valor acumulado, que en este caso debe ser 0, ya que es el punto base
y que no existe procedencia:
En este caso, podemos observar que la etiqueta del nodo 3, ya contiene el menor valor
acumulado posible para llegar a este. Ya que 30 es la mínima distancia posible, toda vez que para
llegar al nodo 3 por medio del nodo 2, tendrá que recorrer como mínimo el valor absoluto del
arco que une el origen con el nodo 2 = 100. Así entonces, la etiqueta del nodo 3 pasa a ser
permanente.
Tabulamos la iteración 1:
Iteración 2:
En este paso, evaluamos las posibles salidas desde el nodo 3, es decir los nodos 4 y 5. De manera
que debemos asignar las etiquetas para cada nodo:
30 es el valor acumulado en la etiqueta del nodo de procedencia, en este caso el valor acumulado
para el nodo 3. 10 es el valor del arco que une el nodo procedencia (3) y el nodo destino (4). 2 es
el número de la iteración.
En este caso, podemos observar que la etiqueta del nodo 4, contiene el menor valor acumulado
posible para llegar a este. Así entonces, la etiqueta del nodo 4 pasa a ser permanente.
Tabulamos la iteración 2:
Iteración 3:
En este paso, evaluamos las posibles salidas desde el nodo 4, es decir los nodos 2 y 5. De manera
que debemos asignar las etiquetas para cada nodo:
40 es el valor acumulado en la etiqueta del nodo de procedencia, en este caso el valor acumulado
para el nodo 4. 15 es el valor del arco que une el nodo procedencia (4) y el nodo destino (2). 3 es
el número de la iteración.
En este caso, podemos observar que el nodo 2 ahora cuenta con 2 etiquetas temporales, y
definitivas, ya que no existe otra ruta hacia dicho nodo. De manera que se elige la etiqueta que
tenga el menor valor acumulado. Así entonces, la etiqueta del nodo 2 con procedencia del nodo
4, pasa a ser permanente.
Tabulamos la iteración 3:
Iteración 4:
En este paso, evaluamos las posibles salidas desde el nodo 2 y el nodo 5. Sin embargo, el nodo 2
solo tiene un posible destino, el nodo 3, el cual ya tiene una etiqueta permanente, de manera que
no puede ser reetiquetado. Ahora, evaluamos el nodo 5 y es un nodo que no tiene destinos. Así
entonces, su etiqueta temporal pasa a ser permanente, en este caso cuenta con 2 etiquetas que
tienen el mismo valor, es decir, alternativas óptimas. De esta manera concluye el algortimo de
Dijkstra.
¿Cuál es la ruta más corta? La ruta más corta entre el nodo 1 (origen) y cualquier otro nodo de
la red (destino), se determina partiendo desde el nodo destino y recorriendo las procedencias de
sus etiquetas. Por ejemplo:
La ruta más corta entre el nodo 1 y el nodo 4 tiene un valor acumulado de: 40
Es necesario considerar, que los valores utilizados en los arcos de la red objeto de estudio del
algoritmo de Dijkstra, no necesariamente representan distancias. Si bien, es un modelo que
aborda el denominado «problema de la ruta más corta»; en la práctica, puede utilizarse para
optimizar: distancia, costos, tiempo.
Recursos de Aprendizaje
Podemos encontrar los siguientes recursos, enlaces de videos y/o páginas en donde
encontraremos más información y ejemplos sobre los temas.
1. Algoritmo de Dijkstra
https://youtu.be/eLFEIxDEphA
2. Algoritmo de Dijkstra
https://youtu.be/skF82H7dt1s
BIBLIOGRAFIA