4 - Recorridos en árboles generales

Con la estructura de nodos lista, necesitamos recorrer el árbol para visitar, procesar y verificar cada dato. En este tema aplicamos los patrones de recorrido más comunes usando clases Nodo y ArbolGeneral en Python, tanto con listas de hijos como con el esquema hijo-izquierdo/hermano-derecho.

4.1 Recorrido preorden

En el recorrido preorden visitamos el nodo actual antes que sus hijos. Este orden resulta ideal para serializar el árbol, copiarlo u obtener una vista jerárquica.

class Nodo:
  def __init__(self, valor):
    self.valor = valor
    self.primer_hijo = None
    self.siguiente_hermano = None


class ArbolGeneral:
  def __init__(self):
    self.raiz = None

  def crear_raiz(self, valor):
    if self.raiz is None:
      self.raiz = Nodo(valor)
    return self.raiz

  def agregar_hijo(self, padre, valor):
    nuevo = Nodo(valor)
    nuevo.siguiente_hermano = padre.primer_hijo
    padre.primer_hijo = nuevo
    return nuevo

  def preorden(self, nodo=None):
    nodo = nodo or self.raiz
    if nodo is None:
      return
    print(nodo.valor, end=" ")
    hijo = nodo.primer_hijo
    while hijo:
      self.preorden(hijo)
      hijo = hijo.siguiente_hermano


def construir_demo():
  arbol = ArbolGeneral()
  raiz = arbol.crear_raiz(1)
  b = arbol.agregar_hijo(raiz, 3)
  a = arbol.agregar_hijo(raiz, 2)
  arbol.agregar_hijo(a, 4)
  arbol.agregar_hijo(a, 5)
  arbol.agregar_hijo(b, 6)
  return arbol


if __name__ == "__main__":
  arbol = construir_demo()
  arbol.preorden()
  print()

El ciclo de hijos itera sobre los hermanos y cada llamada recursiva procesa el subárbol completo del hijo.

4.2 Recorrido postorden

El recorrido postorden procesa primero a los hijos y luego al nodo. Este orden es el preferido para liberar memoria, calcular alturas o evaluar expresiones.

class Nodo:
  def __init__(self, valor):
    self.valor = valor
    self.primer_hijo = None
    self.siguiente_hermano = None


class ArbolGeneral:
  def __init__(self, raiz):
    self.raiz = raiz

  def postorden(self, nodo=None):
    nodo = nodo or self.raiz
    if nodo is None:
      return
    hijo = nodo.primer_hijo
    while hijo:
      self.postorden(hijo)
      hijo = hijo.siguiente_hermano
    print(nodo.valor, end=" ")


def construir_demo():
  raiz = Nodo(1)
  a = Nodo(2)
  b = Nodo(3)
  c = Nodo(4)
  raiz.primer_hijo = a
  a.siguiente_hermano = b
  b.siguiente_hermano = c
  d = Nodo(5)
  e = Nodo(6)
  b.primer_hijo = d
  d.siguiente_hermano = e
  return ArbolGeneral(raiz)


if __name__ == "__main__":
  arbol = construir_demo()
  arbol.postorden()
  print()

Recuerda que cada llamada delimita un subárbol completo; al terminar, disponemos del orden necesario para tareas como la liberación de nodos.

4.3 Recorrido por niveles con colas

El breadth-first search (BFS) recorre los nodos nivel por nivel utilizando una cola. En Python usamos collections.deque para encolar y desencolar eficientemente.

from collections import deque


class Nodo:
  def __init__(self, valor):
    self.valor = valor
    self.primer_hijo = None
    self.siguiente_hermano = None


class ArbolGeneral:
  def __init__(self, raiz):
    self.raiz = raiz

  def bfs(self):
    if self.raiz is None:
      return
    cola = deque([self.raiz])
    while cola:
      actual = cola.popleft()
      print(actual.valor, end=" ")
      hijo = actual.primer_hijo
      while hijo:
        cola.append(hijo)
        hijo = hijo.siguiente_hermano


def construir_demo():
  raiz = Nodo(1)
  a = Nodo(2)
  b = Nodo(3)
  c = Nodo(4)
  raiz.primer_hijo = a
  a.siguiente_hermano = b
  b.siguiente_hermano = c
  d = Nodo(5)
  e = Nodo(6)
  b.primer_hijo = d
  d.siguiente_hermano = e
  return ArbolGeneral(raiz)


if __name__ == "__main__":
  arbol = construir_demo()
  arbol.bfs()
  print()

Este recorrido es ideal para analizar la estructura por niveles, calcular anchuras y exportar el árbol a formatos que dependen de orden topológico.

4.4 Implementación recursiva y alternativas iterativas

Las funciones anteriores se apoyan en recursión natural. Sin embargo, cuando la altura del árbol es grande podría convenir una versión iterativa con pilas manuales.

class Nodo:
  def __init__(self, valor):
    self.valor = valor
    self.primer_hijo = None
    self.siguiente_hermano = None


class ArbolGeneral:
  def __init__(self, raiz):
    self.raiz = raiz

  def preorden_iterativo(self):
    if self.raiz is None:
      return
    pila = [self.raiz]
    while pila:
      actual = pila.pop()
      print(actual.valor, end=" ")
      hijos = []
      hijo = actual.primer_hijo
      while hijo:
        hijos.append(hijo)
        hijo = hijo.siguiente_hermano
      for h in reversed(hijos):
        pila.append(h)


def construir_demo():
  raiz = Nodo(1)
  a = Nodo(2)
  b = Nodo(3)
  c = Nodo(4)
  raiz.primer_hijo = a
  a.siguiente_hermano = b
  b.siguiente_hermano = c
  d = Nodo(5)
  e = Nodo(6)
  b.primer_hijo = d
  d.siguiente_hermano = e
  return ArbolGeneral(raiz)


if __name__ == "__main__":
  arbol = construir_demo()
  arbol.preorden_iterativo()
  print()

La versión iterativa evita desbordar la pila del sistema a costa de administrar estructuras auxiliares.

4.5 Uso de estructuras auxiliares

Los recorridos por niveles utilizan colas, mientras que las variantes iterativas de preorden y postorden emplean pilas. También es posible apoyar los recorridos en listas y arreglos temporales para preservar el orden original de los hijos. Elegir la estructura correcta depende de si necesitamos FIFO (cola), LIFO (pila) o un almacenamiento temporal ordenado.

En Python las colas (deque) y pilas (listas) se liberan automáticamente al salir del ámbito, pero sigue siendo buena práctica encapsular su uso en funciones o métodos para aislar la lógica de recorrido.

4.6 Complejidad aproximada

Todos los recorridos visitan cada nodo una sola vez, por lo que su complejidad temporal es O(n), siendo n la cantidad de nodos del árbol. La complejidad espacial depende de la estructura auxiliar:

  • Preorden/Postorden recursivos: consumen memoria proporcional a la altura del árbol debido a la pila de llamadas.
  • Preorden/Postorden iterativos: la pila manual crece al fan-out del árbol, pero controlamos su tamaño máximo.
  • Recorrido por niveles: la cola almacena tantos nodos como existan en el nivel más ancho.

Monitorear estas cotas ayuda a evitar sorpresas cuando el árbol crece con datos reales.