























Prepara tus exámenes y mejora tus resultados gracias a la gran cantidad de recursos disponibles en Docsity
Gana puntos ayudando a otros estudiantes o consíguelos activando un Plan Premium
Prepara tus exámenes
Prepara tus exámenes y mejora tus resultados gracias a la gran cantidad de recursos disponibles en Docsity
Prepara tus exámenes con los documentos que comparten otros estudiantes como tú en Docsity
Encuentra los documentos específicos para los exámenes de tu universidad
Estudia con lecciones y exámenes resueltos basados en los programas académicos de las mejores universidades
Responde a preguntas de exámenes reales y pon a prueba tu preparación
Consigue puntos base para descargar
Gana puntos ayudando a otros estudiantes o consíguelos activando un Plan Premium
Comunidad
Pide ayuda a la comunidad y resuelve tus dudas de estudio
Ebooks gratuitos
Descarga nuestras guías gratuitas sobre técnicas de estudio, métodos para controlar la ansiedad y consejos para la tesis preparadas por los tutores de Docsity
La implementación de árboles binarios en c++ utilizando vectores y celdas enlazadas como estructuras de datos. Se explican los conceptos básicos de árboles binarios, como el grado de un nodo, el grado de un árbol, el camino, la rama, la altura, la profundidad, el nodo raíz, el nodo hoja y el desequilibrio. Además, se proporciona el código fuente de las clases abin y agen, que implementan los árboles binarios en c++.
Tipo: Apuntes
1 / 31
Esta página no es visible en la vista previa
¡No te pierdas las partes importantes!
























Estructuras de datos no lineales – Teoría
Bloque 1: Árboles
Podemos definir el concepto de arbol como una colección de elementos del mismo tipo, que, debemos tener en cuenta que puede ser de cualquier tipo, tanto definido por el usuario como nativo del lenguaje a utilizar.
Además, estos elementos que pertenecen a un arbol determinado tienen la peculiaridad de que están relacionados entre sí mediante una relación de paternidad.
Definición recursiva formal de árbol
Podemos dar una definición recursiva algo más formal de la estructura de datos “Árbol”:
Dado un elemento “n”, ese unico elemento ya forma un arbol, formado por un único nodo (su raíz) Además, si existen varios elementos a los que llamaremos n1,n2..nk, raices de los subárboles A1,A2...Ak y se establece una relación de padre-hijo entre n y n1,n2...nk, nos queda el siguiente arbol:
Conceptos sobre árboles:
Para profundizar en el estudio de esta peculiar estructura de datos, debemos comprender una serie de conceptos que nos aclararán:
Las operaciones de inserción y eliminación en este caso no son demasiado complejas, pero vamos a tener en cuenta el aspecto de la eficiencia en la operación de eliminacion, ya que podriamos mejorarla.
template
nNodos++; nodos[n].hizq = n; nodos[nNodos].elto = e; nodos[nNodos].padre = n; nodos[nNodos].hizq = NODO_NULO; nodos[nNodos].hder = NODO_NULO; }
Pero con la operación de eliminación quizá tengamos más que hablar, porque...¿Cómo sería una operación de eliminación eficiente en este caso?
Podríamos probar con tres variantes:
1 Borrar la celda en cuestión y reorganizar todo el vector 2 Marcar las celdas libres con un carácter especial 3 “Machacar” la posición del indice con la posición del ultimo y reorganizar solo a los parientes del ultimo
Escogeremos la opción tres, que nos ofrece una eficiencia mucho mayor que las anteriores.
La operación de eliminación, en este caso, de un hijo izquierdo, sería:
template
if(hizqdo != nNodos-1){ nodos[hizqdo] = nodos[nNodos-1];
if(nodos[nodos[hizqdo].padre].hizq == nNodos – 1){ nodos[nodos[hizqdo].padre].hizq = hizqdo; else nodos[nodos[hizqdo].padre].hder = hizqdo;
if(nodos[hizqdo].padre != NODO_NULO){ nodos[nodos[hizqdo].hizq].padre = hizqdo; }
if(nodos[hizqdo].padre != NODO_NULO){ nodos[nodos[hizqdo].hder].padre = hizqdo; }
template
public: typedef int nodo; static const nodo NODO_NULO; explicit Abin(size_t tamMax); Abin(const Abin
void crearRaizB(const T& e); void insertarHijoIzquierdoB(const T& e, nodo n); void insertarHijoDerecho(const T& e, nodo n); void eliminarRaizB(); void eliminarHijoIzquierdoB(nodo n); void eliminarHijoDerechoB(nodo n);
bool arbolVacioB() const; nodo raizB() const; nodo padreB(nodo n) const; nodo hijoIzquierdoB(nodo n) const; nodo hijoDerechoB(nodo n) const;
private:
struct celda { T elto; nodo hizq,hder,padre; celda(const T& e, nodo *p = 0): elto(e), padre(p), hizq(NODO_NULO), hder (NODO_NULO){} };
int nNodos; int tamMax; celda* nodos; };
Arboles binarios – Implementaciones (ENLAZADA)
La implementación enlazada nos permite realizar un arbol binario sin tener en cuenta el tamaño de este en la creación, ya que podremos ir insertando y eliminando elementos sin preocuparnos por que alcance un tamaño maximo previamente fijado.
Además, las operaciones son mucho más sencillas de implementar y menos propensas a errores de programación.
Una cosa que debemos tener en cuenta en la implementación mediante una estructura dinámica es: ¿A quien deben apuntar los punteros y cuantos deben haber por cada celda?
La primera idea que se nos viene a la mente es que, precisamente, deben ser dos punteros: uno al hijo izquierdo del nodo en cuestión y otro al hijo derecho.
Pero nos ocurre tal y como nos ocurrió en la implementación anterior. De ser así, la operación observadora padreB(nodo n) sería de orden O(n).
Luego por cada celda almacenaremos: Elemento y punteros al padre, hizq y hder.
Implementación mediante un vector de posiciones relativas
La implementación de un arbol binario mediante un vector de posiciones relativas es similar a la implementación vectorial de un arbol binario, con la peculiaridad de que en este vector sólo se almacenarán los elementos, teniendo que recurrir a sencillas formulas matematicas para conocer tanto el hijo izquierdo, como el hijo derecho como el padre de un nodo.
La implementación mediante un vector de posiciones relativas sólo es rentable en cuanto a memoria cuando tenemos un arbol completo o casi completo, ya que la ausencia de un nodo en un nivel “h” provocará 2⁽n+h⁾-1 posiciones libres en el vector.
Hijo izquierdo: 2n+ Hijo derecho: 2n+ Padre: (n-1)/
La cabecera (.h) de la implementación mediante un vector de posiciones relativas será la siguiente:
#ifndef ABIN_V #define ABIN_V
#include
template
public:
typedef int nodo; static const nodo NODO_NULO; explicit Abin(size_t tam, const T& e_nulo = T()); Abin(const Abin
void crearRaizB(const T& e); void insertarHijoIzquierdo(const T& e, nodo n); void insertarHijoDerecho(const T& e, nodo n);
void eliminarRaiz(); void eliminarHijoizquierdo(nodo n); void eliminarHijoDerecho(nodo n);
bool arbolVacioB() const; nodo raizB() const; nodo padreB(nodo n) const; nodo hijoIzquierdo(nodo n) const; nodo hijoDerecho(nodo n) const;
private:
T* nodos; T elto_nulo; int tamMax;
};
Recorrido de arboles binarios:
Por lo general, podemos realizar el recorrido de una estructura de arbol de dos formas distintas:
Dentro de los recorridos en profundidad tenemos:
Recorrido en preorden
Cuando se realiza el recorrido en preorden de un arbol binario, el nodo que primero se visita es el nodo raíz, luego el subarbol izquierdo y luego el subarbol derecho de éste.
Por ejemplo, en el siguiente arbol:
El recorrido en preorden sería:
2 – 7 – 2 – 6 – 5 – 11 – 5 – 9 – 4
Una posible implementación recursiva del recorrido en preorden sería:
void preordenAbin(Abin
if(n != NODO_NULO){
procesar(n,A); preordenAbin(A,A.hijoIzquierdoB(n)); preordenAbin(A,A.hijoDerechoB(n));
}
Recorrido en inorden
En el recorrido en inorden, primero se visita el subarbol izquierdo del arbol en cuestión, después se visita la raíz y por ultimo el subárbol derecho, aunque el orden de izq-der puede variar.
Por ejemplo, en el arbol siguiente:
El recorrido en inorden sería:
2 – 7 – 5 – 6 – 11 – 2 – 5 – 9 – 4
Una posible implementación recursiva para el recorrido en inorden del arbol sería:
inordenAbin(Abin
if(n != NODO_NULO){
inordenAbin(A,A.hijoIzquierdoB(n)); procesar(n,A); inordenAbin(A,A.hijoDerechoB(n)); }
Recorrido en postorden
En el recorrido en postorden, primero se visita el subarbol izquierdo del arbol en cuestión, después se visita el subárbol derecho, y por ultimo se visita la raíz. Aunque el orden de izq-der puede variar.
Por ejemplo, en el arbol siguiente:
Recorrido en inorden
En el recorrido en inorden, primero se visita el subarbol izquierdo del arbol en cuestión, después se visita la raíz y por ultimo el subárbol derecho, aunque el orden de izq-der puede variar.
Por ejemplo, en el arbol siguiente:
El recorrido en anchura sería:
2 – 7 – 5 – 2 – 6 – 9 – 5 – 11 - 4
Una posible implementación iterativa para el recorrido en anchura sería:
Cola
C.push(n);
while(!C.vacia()){ n = C.frente(); C.pop(); procesar(n,A); if(n != NODO_NULO){ C.push(A.hijoIzquierdoB(n)); C.push(A.hijoDerechoB(n)); } }
Árboles generales
Podemos definir el concepto de árboles generales como árboles cuyos nodos tienen un grado indefinido, es decir, cada nodo de un arbol general puede tener un numero indeterminado de hijos.
Un ejemplo de un arbol general es el siguiente:
Por convención, denotaremos a los elementos del arbol general de la siguiente forma:
Existen multitud de implementaciones posibles a la hora de representar un arbol general, pero nosotros estudiaremos dos de ellas:
Construcción de un arbol general
Al igual que nos encontramos con el dilema de cómo construir un arbol binario, en la primera parte de estos apuntes, cuando llegamos a un arbol general (Que no es más que una generalización del concepto de arbol binario) nos encontramos con el mismo dilema: ¿Cómo construir un arbol general?
Como vimos que descartabamos la opción de crear un arbol desde la hojas hasta la raiz, ya que esta no era la manera natural de hacerlo, vamos sobre una base sólida de que la construcción del arbl debe ser desde la raíz hasta las hojas.
Por tanto, escogeremos cuatro operaciones para realizar la construcción de un arbol general:
Implementación mediante un vector de “celdas” de un arbol general
La implementacion mediante un vector de celdas de un arbol general es una implementación estática, es decir, su tamaño debe ser pasado como parámetro al constructor (que además debe ser explícito) y, además, no puede ser cambiado.
Se denomina, por tanto, “en tiempo de compilación”.
El problema que plantean este tipo de estructuras frente a las estructuras dinámicas es simple: No podemos redimensionar el vector, tiene un tamaño máximo.
Lo que haremos en esta ocasión será utilizar un vector de celdas, cada una de ellas compuesta de un elemento, el índice del padre y una lista de hijos.
Elto 1 ... Padre NULO (-1)
Hijos (1,2,3) ...
Pero... ¿Cómo hacer las operaciones para que esta implementación sea eficiente y podamos utilizarla?
Las operaciones, a excepción de la inserción y eliminación de nodos dentro del arbol, no tendrán más complicación que el manejo e los indices, pero la inserción y eliminación son casos especiales de ésta:
Operaciones
- Inserción: En la operación de inserción de un nodo determinado (Por ejemplo, un hijo izquierdo), debemos asegurarnos, por lo general, de cuatro cosas: El nodo existe (es 0 (raíz) o su padre es distinto del nodo nulo), caben mas elementos en el vector, y el numero de nodos es > 0. La inserción es sencilla: Vamos recorriendo los elementos del vector hasta encontrar una posición libre, ahí insertamos elto = e, y padre = n, y insertamos en la primera posición de la lista de hijos del padre (n) el indice de la posición encontrada. Para el caso de inserción de hermano derecho es similar, y nos tenemos que ocupar de buscar una posición libre, insertar elto y padre, y además insertar en la lista DE HIJOS DEL PADRE DE N el indice en la posición SIGUIENTE a n. - Eliminación: La eliminación es algo más tediosa, se basa en comprobar que el nodo existe, que, dada su lista de hijos ésta no está vacía, que el primer elemento de su lista de hijos (hizq) tiene una lista de hijos vacía, poner ese hizq.padre a nodo_nulo y eliminar de la lista de hijos de n la primera posición. Para eliminar en el hijo derecho es más tedioso aún: Debemos encontrar un nodo “p”, siguiente del nodo “n” en la lista de hijos del padre de n, además este nodo p no puede ser igual a fin(), y hederecho será lhp.elemento(p). Debemos comprobar también que la lista de hijos de hederecho está vacía, y ya entonces podemos colocar su padre a nodo nulo y eliminar p de la lista de hijos del padre.
Especificación (Fichero de cabecera .h)
#ifndef AGEN_V #define AGEN_V #include "Lista.h" #include
template
struct celda;