Docsity
Docsity

Prepara tus exámenes
Prepara tus exámenes

Prepara tus exámenes y mejora tus resultados gracias a la gran cantidad de recursos disponibles en Docsity


Consigue puntos base para descargar
Consigue puntos base para descargar

Gana puntos ayudando a otros estudiantes o consíguelos activando un Plan Premium


Orientación Universidad
Orientación Universidad


Implementación de Árboles Binarios en C++ - Prof. Gracía, Apuntes de Ingeniería Infórmatica

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

2013/2014

Subido el 18/06/2014

drezzler
drezzler 🇪🇸

4

(2)

2 documentos

1 / 31

Toggle sidebar

Esta página no es visible en la vista previa

¡No te pierdas las partes importantes!

bg1
Estructuras de datos no lineales – Teoría
Bloque 1: Árboles
Podemos denir 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 denido 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.
Denición recursiva formal de árbol
Podemos dar una denició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:
- Grado de un nodo: Número de hijos del nodo en cuestión
- Grado de un arbol: Numero máximo de grados de los nodos del arbol
- Camino: Sucesión de nodos n1,n2...ni, donde se establece una relación padre-hijo entre
cada nodo ni y cada nodo ni+1, además, la longitud del camino es el numero de nodos
que comprende – 1
Ak
A2
A1
nk
n2
n
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e
pf1f

Vista previa parcial del texto

¡Descarga Implementación de Árboles Binarios en C++ - Prof. Gracía y más Apuntes en PDF de Ingeniería Infórmatica solo en Docsity!

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:

  • Grado de un nodo: Número de hijos del nodo en cuestión
  • Grado de un arbol: Numero máximo de grados de los nodos del arbol
  • Camino: Sucesión de nodos n1,n2...ni, donde se establece una relación padre-hijo entre cada nodo ni y cada nodo ni+1, además, la longitud del camino es el numero de nodos que comprende – 1

Ak

A

A

nk

n

n

  • Rama: Camino que parte de un nodo y llega hasta una hoja
  • Altura: Longitud de la rama más alta que parte de un nodo en concreto
  • Profundidad: Longitud del camino mas corto desde la raiz hasta el nodo en cuestión
  • Nivel: Coincide con la altura
  • Nodo raiz: Nodo sin ascendentes directos
  • Nodo hoja: Nodo sin descendientes directos
  • Desequilibrio: Diferencia entre la altura del subarbol izquierdo y derecho de un nodo determinado

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.

La operación de inserción de un hijo izquierdo, por ejemplo, sería de la siguiente forma:

template void insertarHijoizquierdoB(nodo n, const T& e){ assert(n>=0 && n<tamMax); assert(nNodos<tamMax); assert(nodos[n] != NODO_NULO); assert(nodos[n].hizq == NODO_NULO);

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 void eliminarHijoIzquierdoB(nodo n){ assert(n>=0 && n<tamMax); assert(nodos[n] != NODO_NULO); typename Abin::nodo hizqdo = nodos[n].hizq; assert(hizqdo != NODO_NULO); assert(nodos[hizqdo].hizq == NODO_NULO && nodos[hizqdo].hder == NODO_NULO);

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; }

Definición de un arbol binario implementado mediante un vector de celdas:

template class Abin {

public: typedef int nodo; static const nodo NODO_NULO; explicit Abin(size_t tamMax); Abin(const Abin& A); Abin& operator = (const Abin& A);

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 class Abin {

public:

typedef int nodo; static const nodo NODO_NULO; explicit Abin(size_t tam, const T& e_nulo = T()); Abin(const Abin& A); Abin& operator = (const Abin& A);

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:

  • En profundidad
  • En anchura

Dentro de los recorridos en profundidad tenemos:

  • Recorrido en preorden
  • Recorrido en inorden
  • Recorrido en postorden

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& A, nodo n) {

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& A, nodo n){

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;

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:

  • Nodo raíz: Un nodo sin ascendentes directos (Sin padres, ni abuelos)
  • Nodo hoja: Nodo sin descendientes directos
  • Hijo izquierdo: Primer hijo, empezando por la derecha, de un nodo determinado.
  • Hermano derecho: Hijo de un nodo, también, pero que es el siguiente a la derecha del nodo.

Existen multitud de implementaciones posibles a la hora de representar un arbol general, pero nosotros estudiaremos dos de ellas:

  • La representación estática, mediante un vector de “celdas”, que explicaremos con más detalle un poco más adelante en estos apuntes
  • La representación dinámica, mediante celdas enlazadas.

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:

  • Agen(); El constructor por defecto, que crea un arbol vacío.
  • Void crearRaiz(const T& e); Crea una raíz
  • Void insertarHijoIzqdo(const T& e, nodo n); Inserta un nuevo hijo izquierdo al nodo “n”.
  • Void insertarHermDrcho(const T& e, nodo n); Inserta un nuevo hermano derecho al nodo “n”.

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 class Agen {

struct celda;