modulo 09 - Universidad Nacional de Colombia

Anuncio
3004597 – Estructura de Datos – Modulo 09
Universidad Nacional de Colombia – Sede Medellín
MODULO 09
OBJETIVOS
Con el desarrollo de este módulo se pretende que el estudiante:
-
Comprenda la implementación del árbol binario de búsqueda en la computadora.
-
Aprenda como pueden implementarse las operaciones más importantes de los árboles y
reconozca el papel fundamental que desempeña la recursión en estas. Comprenda los
conceptos de profundidad y recorridos.
CONTENIDO
1
Implementación del árbol binario de búsqueda
1.1 La clase Nodo
1.2 La clase ArbolBin
2
Implementación de las operaciones del árbol binario de búsqueda
2.1 Inserción
2.1.1 Buscar lugar
2.2 Buscar un elemento por su clave
2.2.1 Buscar dato
2.3 Buscar el padre de un elemento
2.3.1 Padre
2.4 Recorridos
2.4.1 PreOrden
2.4.2 PosOrden
2.4.3 EnOrden
A Referencias
B Taller
02 - 2003
1
3004597 – Estructura de Datos – Modulo 09
1
Universidad Nacional de Colombia – Sede Medellín
IMPLEMENTACIÓN DEL ÁRBOL BINARIO DE BÚSQUEDA
Los árboles binarios son similares a las listas, básicamente son una estructura que agrupa un
conjunto de elementos estándar. Como se vio en módulos anteriores, la diferencia fundamental
entre árboles y listas es el tipo de relación que existe entre los elementos que los constituyen;
como ya se sabe, en un árbol binario cada elemento se conecta máximo con otros dos elementos
llamados hijo derecho e hijo izquierdo respectivamente. En un árbol binario de búsqueda, la clave
de todo nodo siempre es mayor que las claves de su hijo izquierdo y todos los descendientes de su
hijo izquierdo. Y la clave de todo nodo siempre es menor que las claves de su hijo derecho y todos
los descendientes de su hijo derecho.
Al igual que las listas, los árboles almacenan elementos estándar, que no son más que instancias
de cualquier tipo de dato que posea un campo clave. Durante el desarrollo de este módulo
usaremos datos de tipo Cuenta como elementos estándar. El tipo de dato Cuenta, es un tipo de
dato agregado, que representa una cuenta bancaria y posee un campo clave llamado codigo.
A continuación se presenta la clase Cuenta.
class Cuenta
{
public:
int codigo;
char nombreCliente[50];
float saldo;
public:
Cuenta();
//constructor por defecto
Cuenta(int codi,float saldoInic);//constructor con parámetros
int key();
};
Cuenta::Cuenta()
{
codigo=0;
saldo=0.0;
nombreCliente[0]='\0';
}
Cuenta::Cuenta(int codi, float saldoInic)
{
codigo=codi;
saldo=saldoInic;
cout<<"Escriba el nombre del cliente: ";
cin>>nombreCliente;
}
int Cuenta::key()
{
return codigo;
}
02 - 2003
2
3004597 – Estructura de Datos – Modulo 09
Universidad Nacional de Colombia – Sede Medellín
Esta clase cuenta con un constructor por defecto, que el compilador requiere para crear las
variables que almacenaran los argumentos recibidos por las funciones que reciben datos de tipo
Cuenta. Además, la clase posee otro constructor, que recibe como argumento el saldo inicial de la
nueva cuenta y pide al usuario que ingrese el nombre del cliente.
La función key() retorna el campo clave de la Cuenta.
1.1 LA CLASE NODO
Los elementos que componen el árbol binario de búsqueda se llamarán nodos, cada uno de los
cuales posee espacio suficiente para guardar el dato que se desea almacenar (esto es, un
elemento estándar) y un par de enlaces (punteros) que conectan al nodo con sus hijos izquierdo y
derecho. Con este formato, cada nodo posee tres elementos: dato (elemento estándar), puntero
izquierdo y puntero derecho como se ve en la figura 1.1.
Figura 1.1: Nodo de un árbol
Elemento estándar
dato
Puntero hijo izquierdo
Hijo
izq
Hijo
der
Puntero hijo derecho
A continuación se muestra el código de la clase Nodo con su respectivo constructor:
class Nodo
{
public:
Cuenta dato;
Nodo *hijoIzq;
//Apuntador a hijo izquierdo
Nodo *hijoDer;
//Apuntador a hijo derecho
public:
Nodo(Cuenta nDato);
};
Nodo::Nodo(Cuenta nDato) //Constructor
{
dato = nDato;
hijoIzq = NULL;
hijoDer = NULL;
}
1.2 LA CLASE ÁRBOL BINARIO
En esta implementación se representará al árbol binario de búsqueda con una clase llamada
ArbolBin, la cual agrupará todas las funciones que pueden aplicarse al árbol binario de búsqueda
y contendrá un atributo (campo) que apunta al nodo raíz del árbol y uno que apunta al nodo
02 - 2003
3
3004597 – Estructura de Datos – Modulo 09
Universidad Nacional de Colombia – Sede Medellín
actual. Para ejemplificar, se usará un nodo cuyo elemento estándar es una Cuenta tal como se
definió anteriormente.
A continuación se declaran la clase ArbolBin:
Figura 1.2: Estructura del árbol binario
[001]
5
003
009
NULL
004
[003]
[004]
3
7
012
031
[009]
[012]
[031]
2
4
6
NULL
NULL
NULL
NULL
NULL
NULL
class ArbolBin
{
public:
Nodo *raiz;
//Apunta a la raíz del arbol
Nodo *actual;
public:
ArbolBin(); //Constructor
void insertar(Cuenta dato);
void buscar_lugar(Nodo *r, Cuenta dato);
//Recorridos:
void inorden(Nodo *r);
void preorden(Nodo *r);
void posorden(Nodo *r);
02 - 2003
4
3004597 – Estructura de Datos – Modulo 09
Nodo
Nodo
Nodo
Nodo
Universidad Nacional de Colombia – Sede Medellín
*findkey(int key);
*buscar_dato(Nodo *r, int key);
*buscarPadre(int dato);
*padre(Nodo *r, int key);
};
//Constructor
ArbolBin::ArbolBin()
{
raiz=NULL;
actual=NULL;
}
2
IMPLEMENTACIÓN DE OPERACIONES SOBRE EL ARBOL BINARIO DE BUSQUEDA
(NO AVL)
A continuación se muestra la implementación de algunas de las funciones (operaciones) del árbol
binario de búsqueda.
2.1 INSERCIÓN
Esta función inserta un elemento estándar llamado dato en el árbol, para hacerlo revisa si el
árbol es vacío, si lo es crea un nuevo nodo con el elemento estándar dato (usando la función
asignar memoria) y lo posiciona como raíz. De otro modo invoca a la función buscar_lugar que
crea el nuevo nodo con el elemento estándar dato y lo ubica en el lugar adecuado del árbol.
void ArbolBin::insertar(Cuenta dato)
{
if(raiz == NULL)
//Si árbol vacío
{
raiz = new Nodo(dato); //Crea el nodo
actual=raiz;
//El nuevo a su vez es el actual
}
else
//Arbol no vacío
buscar_lugar(raiz, dato);
}
2.1.1
Buscar lugar
Esta función recursiva busca el lugar adecuado para insertar un nodo con el elemento estándar
dato en el árbol cuya raíz es *r.
Para hacerlo, la función primero chequea que r no sea nulo. De no serlo, verifica si la clave de r es
mayor que la clave de dato (esto significa que el nuevo nodo debe insertarse a la izquierda de r), y
si es así, se verifica que r no tenga hijo izquierdo y se inserta el nuevo nodo como hijo izquierdo de
r; de lo contrario, se invoca de nuevo buscar_lugar() pero esta vez para el subárbol que inicia en el
hijo izquierdo de r. En caso de que el valor de r sea menor que el valor de dato, se hace un
procedimiento similar al anterior, pero esta vez a través del hijo derecho de r.
02 - 2003
5
3004597 – Estructura de Datos – Modulo 09
Universidad Nacional de Colombia – Sede Medellín
void ArbolBin::buscar_lugar(Nodo *r, Cuenta dato)
{
if(r)
{
if( (r->dato).key() > dato.key() ) // Si clave del nuevo es menor
// que la clave de r
{
if( !r->hijoIzq ) // Si no tiene hijo izq
{
r->hijoIzq =new Nodo(dato); //Lo crea e inserta
actual=r->hijoIzq;
}
else
buscar_lugar(r->hijoIzq, dato);
}
else if( (r->dato).key() < dato.key())// Si clave del nuevo es
// mayor que la clave de r
{
if(!r->hijoDer) // Si no tiene hijo der
{
r->hijoDer =new Nodo(dato); //Lo crea e inserta
actual=r->hijoDer;
}
else
buscar_lugar(r->hijoDer, dato); //Sigue buscando
}
}
else
{
cout<<"Error! referencia a nodo invalida";
}
}
2.2 BUSCAR ELEMENTO POR SU CLAVE (findkey)
La función findkey() recibe como argumento un número entero, que representa la clave del
elemento que se desea encontrar. Esta función invoca a buscar_dato() , una función recursiva
que se describe en la siguiente sección:
Nodo *ArbolBin::findkey(int key)
{
return buscar_dato(raiz, key);
}
2.2.1
Buscar dato
La función buscar_dato() examina recursivamente el árbol cuya raíz es *r buscando el elemento
con la clave dada. Esta función aprovecha la estructura del árbol binario de búsqueda para reducir
02 - 2003
6
3004597 – Estructura de Datos – Modulo 09
Universidad Nacional de Colombia – Sede Medellín
considerablemente el número de elementos promedio que debe examinar antes de encontrar el
elemento buscado o darse cuenta de que no existe en él.
Si la dirección a Nodo dada como argumento a la función no es nula, esta procede a buscar en el
subárbol de este Nodo. Si el dato del nodo *r es la cuenta buscada, el actual se posiciona allí y la
función retorna su dirección, de otro modo, si el dato que se está buscando es menor que el dato
del nodo *r, se procede a buscar en el subárbol cuya raíz es el hijo izquierdo de *r (esto se debe a
que, gracias a la estructura del árbol binario de búsqueda, sabemos que todos los elementos con
clave menor a la que se esta examinando, se encuentran a la izquierda de él). De manera similar,
si el dato buscado es mayor que el dato del nodo *r, se procede a buscar en el subárbol cuya raíz
es el hijo derecho de *r.
Nodo *ArbolBin::buscar_dato(Nodo *r, int key)
{
Nodo *b;
if(r) //Si raíz no nula
{
if( (r->dato).key()==key ) //Si lo encuentra
{
actual=r;
return r;
}
else if( (r->dato).key()>key ) //Si key es menor que la clave
//de r
{
actual=r;
b = buscar_dato( r->hijoIzq, key );
return b;
}
else if( (r->dato).key()<key ) //Si key es mayor que la clave
//de r
{
actual=r;
b = buscar_dato(r->hijoDer, key);
return b;
}
}
cout<<"Cuenta no encontrada"<<endl;
return NULL;
}
2.3 BUSCAR EL PADRE DE UN ELEMENTO
La función buscar_padre() busca el padre de un elemento cuya clave es dada como argumento y
retorna su dirección. Para hacerlo, llama a la función recursiva padre():
Nodo *ArbolBin::buscarPadre(int key)
{
return padre(raiz, key);
}
2.3.1
Padre
02 - 2003
7
3004597 – Estructura de Datos – Modulo 09
Universidad Nacional de Colombia – Sede Medellín
Esta es una función recursiva que recibe como argumentos la raíz del árbol en el cual se quiere
buscar y la clave del elemento al que se le quiere encontrar el padre. Primero examina si el nodo
*r tiene hijo izquierdo, si es así y además la clave del elemento buscado es menor que la clave del
dato almacenado en *r, podemos asegurar, gracias a la estructura de árbol binario de búsqueda,
que el elemento buscado se encuentra a la izquierda de *r, por tanto se pregunta si el elemento
buscado es el hijo izquierdo de *r, de ser así, se retorna r, de lo contrario se continúa buscando el
padre en el subárbol izquierdo de *r.
Un análisis similar se hace si la clave buscada es mayor que la clave de *r, solo que esta vez se
examina el lado derecho de *r. Pero ¿que sucede si el elemento al cual se le quiere encontrar el
padre es la raíz? En este caso, se cumple el último else if, y la función retorna NULL, como debe
ser (ya que la raíz no tiene padre).
Nodo *ArbolBin::padre(Nodo *r, int key)
{
if( r->hijoIzq && (key < r->dato.key()) )
//Si tiene hijo izquierdo no nulo y la clave del buscado es menor
//que la clave de r
{
if( (r->hijoIzq)->dato.key()==key )
//Si el hijo de la izquierda es el buscado entonces r es el padre
return r;
else
return padre( r->hijoIzq, key ); //Seguir buscando…
}
else if( r->hijoDer && (key > r->dato.key()) )
//Si tiene hijo derecho no nulo y la clave del buscado es mayor
//que la clave de r
{
if( (r->hijoDer)->dato.key()==key )
//Si el hijo de la derecha es el buscado entonces r es el padre
return r;
else
return padre( r->hijoDer, key);
}
else if( key==r->dato.key() )
//en caso de que el buscado sea la raíz
{
cout<<"El hijo no tiene padre ya que es la raiz";
return NULL;
}
cout<<"hijo no encontrado"; //en caso de no encontrarlo
return NULL;
}
2.4 RECORRIDOS
Un árbol binario puede recorrerse en su totalidad en tres órdenes diferentes, que determinan la
secuencia en la cual se procesan cada uno de los nodos. Estos órdenes se conocen
02 - 2003
8
3004597 – Estructura de Datos – Modulo 09
Universidad Nacional de Colombia – Sede Medellín
tradicionalmente como: Preorden, Posorden e inorden. Los recorridos de árboles son operaciones
muy fáciles de implementar de manera recursiva, lo que las hace un excelente ejemplo de cómo la
recursión simplifica enormemente algunos algoritmos.
2.4.1
Preorden
En este recorrido se procesa primero el nodo raíz, luego su subárbol izquierdo y finalmente su
subárbol derecho. La función preorden, que implementa este recorrido, procesa primero el nodo
que le es dado como argumento, luego se llama recursivamente para el subárbol izquierdo de este
nodo y finalmente para su subárbol derecho.
void ArbolBin::preorden(Nodo *r)
{
if(r) //Si es no nulo
{
cout<<" "<<(r->dato).codigo<<" ";
preorden(r->hijoIzq);
preorden(r->hijoDer);
}
}
2.3.1
Posorden
En el recorrido en posorden se procesa primero el subárbol izquierdo del nodo raíz, luego el
subárbol derecho y por último se procesa el nodo. La función posorden se llama recursivamente
para el subárbol izquierdo del nodo dado como argumento, luego hace lo mismo para el subárbol
derecho y por último procesa el nodo dado como argumento.
void ArbolBin::posorden(Nodo *r)
{
if(r)
{
posorden(r->hijoIzq);
posorden(r->hijoDer);
cout<<" "<<(r->dato).codigo<<" ";
}
}
2.3.2
Inorden
El recorrido en inorden consiste en primero procesar el subárbol izquierdo del nodo, luego se
procesa el nodo y por último su subárbol derecho. La función inorden se aplica recursivamente al
subárbol izquierdo del nodo dado como argumento, luego procesa el nodo y por último se aplica
recursivamente al subárbol derecho del nodo dado como argumento.
void ArbolBin::inorden(Nodo *r)
{
if(r)
{
02 - 2003
9
3004597 – Estructura de Datos – Modulo 09
Universidad Nacional de Colombia – Sede Medellín
inorden(r->hijoIzq);
cout<<" "<<(r->dato).codigo<<" ";
inorden(r->hijoDer);
}
}
Por medio del siguiente programa principal se puede probar el funcionamiento del árbol:
void main(void)
{
//Se crea un nuevo árbol
ArbolBin miarbol;
//Se crea una cuenta que va a ser insertada en el árbol
Cuenta cuenta1(20, 1000);
miarbol.insertar(cuenta1);
Cuenta cuenta2(10, 2000);
miarbol.insertar(cuenta2);
Cuenta cuenta3(30, 5000);
miarbol.insertar(cuenta3);
//Se ha creado un árbol con raíz 20, hijo izq 10 e hijo dcho 30
//Ahora comprobemos con los recorridos:
cout<<endl<<"Inorden: ";
miarbol.inorden(miarbol.raiz);
//Imprimió 10 20 30
cout<<endl<<"Preorden: ";
miarbol.preorden(miarbol.raiz);
//Imprimió 20 10 30
cout<<endl<<"Posorden: ";
miarbol.posorden(miarbol.raiz);
//Imprimió 10 30 20
}
02 - 2003
10
3004597 – Estructura de Datos – Modulo 09
Universidad Nacional de Colombia – Sede Medellín
ANEXOS
A REFERENCIAS
Villalobos Jorge, Quintero Alejandro, Otero Mario. Estructuras de datos. Ediciones Uniandes.
B TALLER
1. Implemente una función que borre un subárbol dada la clave de su raíz
2. Implemente una función que borre un elemento dada la clave de su raíz.
C LABORATORIO
Implementar las funciones
1. La función recibe como argumento un árbol a, debe retornar el número de hojas de
dicho árbol.
2. La función recibe como argumento un árbol a y un elemento e, debe retornar el
padre del elemento e y si e es la raíz, la función retornará null.
3. La función recibe como argumento un árbol a y un elemento e, debe retornar una
lista donde sus elementos son los nodos de a que se tienen que recorrer para ir
desde la raíz del árbol hasta el elemento e, si e no esta en el árbol retornará null.
4. La función recibe como argumento un árbol a y retorna el número de nodos que
este árbol posea.
5. La función recibe como argumento un árbol a y dos elementos e1 y e2, debe
retornar el ancestro común más próximo a e1 y e2.
6. La función recibe como argumento un árbol a y un nodo e, debe mostrar los nodos
primos al nodo e. (Se entiende por nodos primos de E, a los hijos del hermano del
padre de E).
7. La función recibe como argumento un árbol a y un entero n, debe mostrar los
nodos de a que están a un nivel n dentro del árbol.
8. La función recibe como argumento un árbol a, y debe retornar la altura de ese
árbol a.
9. La función recibe como argumento un árbol a y un entero n, debe retornar el
número de nodos que tiene el árbol en el nivel n.
10. La función recibe como argumento un árbol a y un nodo e, debe retornar el nivel en
que el nodo e se encuentra en el árbol.
11. La función recibe como argumento un árbol a y retorna una lista L donde sus
elementos son los mismos elementos de a dados en INORDEN.
12. La función recibe como argumento un árbol A y un elemento e, devuelve una lista
con todos los sucesores al elemento e.
13. Dados los recorridos en POSORDEN y en INORDEN de un árbol binario por medio
de una lista L, esta función recibe la lista y retorna el árbol original.
14. Dados los recorridos en PREORDEN y en INORDEN de un árbol binario por medio
de una lista L, esta función recibe la lista y retorna el árbol original.
D CREDITOS
Editor: PhD. Fernando Arango
02 - 2003
11
3004597 – Estructura de Datos – Modulo 09
Universidad Nacional de Colombia – Sede Medellín
Colaboradores: Ing. Edwin Hincapié, Ms.C Francisco Moreno, Santiago Londoño, Alberto Jiménez,
Juan Carlos Hernández, Carlos Andrés Giraldo, Diego Figueroa.
02 - 2003
12
Descargar