Árboles
Antes de comenzar a trabajar sobre árboles es importante estudiar recursividad
Introducción
Algunas definiciones:
- Un árbol es una estructura de datos enlazada que organiza elementos en forma jerárquica. Es decir, hay una relación padre/hijos.
- Cada nodo puede tener más de un hijo, pero un solo padre.
- Existe un nodo que no tiene padre denominado raiz.
- Los nodos que no tienen hijos se denominan hojas
- Un árbol es de orden N (o N-ario) cuando la máxima cantidad de hijos que puede tener un nodo es N.
- La profundidad de un árbol es la distancia (saltos entre nodos) desde la raiz hasta la hoja más lejana.
Visión Recursiva
Cada rama de un árbol puede ser visto como un sub-árbol. De esta forma, el árbol genealógico de la abuela Bouvier está compuesto por el árbol de Marge, el de Patty y el de Selma. Esto repercute en el estilo de programación: todas las funciones que recorren o modifican el árbol hacen uso de características recursivas. Esto significa que a veces el término nodo y árbol se usan indistintamente, lo cual puede causar confusión al desprevenido.
Ejemplo de un árbol en python
definición
Haciendo uso de la noción recursiva de un árbol, podemos definir un árbol como la unión de un elemento y sus ramas.
class Arbol:
def __init__(self, elemento):
self.hijos = []
self.elemento = elemento
agregar elementos
Una forma de agregar elementos podría ser:
abuela = "Jacqueline Gurney"
marge = "Marge Bouvier"
patty = "Patty Bouvier"
selma = "Selma Bouvier"
bart = "Bart Simpson"
lisa = "Lisa Simpson"
maggie = "Maggie Simpson"
ling = "Ling Bouvier"
arbol = Arbol(abuela)
agregarElemento(arbol, patty, abuela)
agregarElemento(arbol, selma, abuela)
agregarElemento(arbol, ling, selma)
agregarElemento(arbol, marge, abuela)
agregarElemento(arbol, bart, marge)
agregarElemento(arbol, lisa, marge)
agregarElemento(arbol, maggie, marge)
Para lo cual necesitamos escribir el método agregarElemento que recibe el árbol, el elemento a agregar, y el padre de dicho elemento. Este método obtiene el sub-árbol que contiene al elemento padre y le agrega una nueva rama (árbol) con el nuevo elemento.
Es importante entender que la búsqueda del sub-árbol debe ser recursiva. La condición de corte es si el elemento está en el árbol actual, si no se busca en cada uno de los hijos.
def agregarElemento(arbol, elemento, elementoPadre):
subarbol = buscarSubarbol(arbol, elementoPadre);
subarbol.hijos.append(Arbol(elemento))
def buscarSubarbol(arbol, elemento):
if arbol.elemento == elemento:
return arbol
for subarbol in arbol.hijos:
arbolBuscado = buscarSubarbol(subarbol, elemento)
if (arbolBuscado != None):
return arbolBuscado
return None
Profundidad y grado
- La profundidad de un árbol es 1 + la profundidad del hijo más profundo
- El grado es el maximo entre la cantidad de sus hijos directos y el grado de sus hijos
def profundidad(arbol):
if len(arbol.hijos) == 0:
return 1
return 1 + max(map(profundidad,arbol.hijos))
def grado(arbol):
return max(map(grado, arbol.hijos) + [len(arbol.hijos)])
Recorridos
Recorrer un árbol significa comenzar a visitar a cada uno de los elementos (tanto al elemento de la raiz como a los elementos de sus descendientes). A veces se realiza porque se quiere ejecutar algo por cada uno u otras veces es porque se está buscando uno en particular.
Existen dos maneras de recorrer un árbol: profundidad primero y ancho primero.
Profundidad Primero
Esta forma es la más sencilla: consiste en explorar toda una rama antes de pasar a la siguiente. Es decir, hasta no terminar de recorrer toda la rama de marge, no iniciará con Patty.
Acá un diagrama que muestra el órden de recorrido sobre un árbol:
En el método que vimos anteriormente de buscarSubArbol estamos usando esta estrategia para encontrar el elemento. A continuación un ejemplo de una función de orden superior que ejecuta sobre cada elemento del árbol:
def ejecutarProfundidadPrimero(arbol, funcion):
funcion(arbol.elemento)
for hijo in arbol.hijos:
ejecutarProfundidadPrimero(hijo, funcion)
Al utilizarlo con una función que imprime un elemento :
def printElement(element):
print element
ejecutarProfundidadPrimero(arbol, printElement)
podemos ver el siguiente resultado:
Jacqueline Gurney
Patty Bouvier
Selma Bouvier
Ling Bouvier
Marge Bouvier
Bart Simpson
Lisa Simpson
Maggie Simpson
Ancho Primero
Esta forma es un poco más compleja pero puede ser conveniente dependiendo del problema. En nuestro ejemplo irá recorriendo generación a generación: Primero la abuela, luego todas las hijas, y luego todos los nietos.
Acá otro diagrama a modo esquemático:
Para poder resolver este algoritmo, es necesario retrasar la ejecución de la función sobre los hijos hasta que no se haya terminado de ejecutar todos los hermanos/primos. Por eso se usa una cola (Primero que entra, Primero que sale) para lograr esto. Cuando se visita a un nodo, ejecuta la función, agrega sus hijos a la cola, y luego llama a la función recursiva pero en lugar de hacerlo sobre algún hijo suyo (como hacía profundidad primero) lo hace sobre el próximo de la cola, que seguramente es un alguien de su nivel si aún quedan o el primero del próximo nivel.
def ejecutarAnchoPrimero(arbol, funcion, cola = deque()):
funcion(arbol.elemento)
if (len(arbol.hijos) > 0):
cola.extend(arbol.hijos)
if (len(cola) != 0):
ejecutarAnchoPrimero(cola.popleft(), funcion, cola)
Se usa de la siguiente manera:
def printElement(element):
print element
ejecutarAnchoPrimero(arbol, printElement)
Veamos paso a paso que ocurre:
Invocación:1
Elemento: Jackeline
Consola
Jacqueline Gurney
Cola
Patty Bouvier
Selma Bouvier
Marge Bouvier
Invocación:2
Elemento: Patty
No tiene hijos, por lo tanto no agrega elementos a la cola
Consola
Jacqueline Gurney
Patty Bouvier
Cola
Selma Bouvier
Marge Bouvier
Invocación:3
Elemento: Selma
Agrega a Ling a la cola
Consola
Jacqueline Gurney
Patty Bouvier
Selma Bouvier
Cola
Marge Bouvier
Ling Bouvier
Invocación:4
Elemento: Marge
Agrega a Bart, Lisa y Maggie
Consola
Jacqueline Gurney
Patty Bouvier
Selma Bouvier
Marge Bouvier
Cola
Ling Bouvier
Bart Simpson
Lisa Simpson
Maggie Simpson
Invocación:5
Elemento: Ling
Consola
Jacqueline Gurney
Patty Bouvier
Selma Bouvier
Marge Bouvier
Ling Bouvier
Cola
Bart Simpson
Lisa Simpson
Maggie Simpson
Invocación:6
Elemento: Bart
Consola
Jacqueline Gurney
Patty Bouvier
Selma Bouvier
Marge Bouvier
Ling Bouvier
Bart Simpson
Cola
Lisa Simpson
Maggie Simpson
Invocación:7
Elemento: Lisa
Consola
Jacqueline Gurney
Patty Bouvier
Selma Bouvier
Marge Bouvier
Ling Bouvier
Bart Simpson
Lisa Simpson
Cola
Maggie Simpson
Invocación:8
Elemento: Maggie
Consola
Jacqueline Gurney
Patty Bouvier
Selma Bouvier
Marge Bouvier
Ling Bouvier
Bart Simpson
Lisa Simpson
Maggie Simpson
Cola
Código fuente
El código fuente de árboles que vimos acá está adjunto a esta página como un export de un proyecto PyDev de eclipse.
Igualmente, está online en éste link:
http://xp-dev.com/svn/uqbar/examples/prog2/unidad5-recursividadYArboles/arboles