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