4 - Construcción de árboles binarios

Con el modelo de nodos definido en el tema anterior podemos concentrarnos en construir árboles binarios de distintas formas. Este capítulo cubre las inserciones iterativas, la carga desde listas, la generación balanceada y algunas rutas para recibir datos del usuario sin comprometer la integridad del árbol en Python.

4.1 Planificar la construcción

Antes de escribir código conviene definir si los datos llegan ordenados, desordenados o en un flujo interactivo. De esto depende si necesitamos herramientas extra para balancear el árbol o si basta con aplicar la inserción secuencial.

  • Listas desordenadas: se insertan una a una usando comparaciones. El árbol puede quedar sesgado si la entrada ya está casi ordenada.
  • Listas ordenadas: permiten construir un árbol balanceado tomando siempre el elemento central como raíz del subárbol.
  • Entradas interactivas: requieren validar cada valor antes de insertarlo y ofrecer opciones para finalizar la carga.

4.2 Componentes base reutilizables

Usaremos las mismas clases del tema 3 junto con las utilidades de inicialización y limpieza para comenzar desde un estado conocido.

class Nodo:
    def __init__(self, valor, nivel=0):
        self.valor = valor
        self.izquierdo = None
        self.derecho = None
        self.nivel = nivel


class ArbolBinarioOrdenado:
    def __init__(self):
        self.raiz = None
        self.cantidad = 0

    def crear_nodo(self, valor, nivel=0):
        return Nodo(valor, nivel=nivel)

    def limpiar_arbol(self):
        self.liberar_subarbol(self.raiz)
        self.raiz = None
        self.cantidad = 0

    def liberar_subarbol(self, nodo):
        if nodo is None:
            return
        self.liberar_subarbol(nodo.izquierdo)
        self.liberar_subarbol(nodo.derecho)
        nodo.izquierdo = None
        nodo.derecho = None

Estas utilidades se reutilizan en cada variante de construcción, por lo que conviene mantenerlas en un módulo independiente del proyecto.

4.3 Inserción iterativa reutilizable

Partimos de la misma estructura del tema 3: el recorrido es iterativo, compara valores y enlaza el nuevo nodo al encontrar una referencia nula.

class ArbolBinarioOrdenado:
    # ...
    def insertar(self, valor):
        if self.raiz is None:
            self.raiz = self.crear_nodo(valor)
            self.cantidad = 1
            return self.raiz

        actual = self.raiz
        while True:
            if valor < actual.valor:
                if actual.izquierdo is None:
                    actual.izquierdo = self.crear_nodo(valor, nivel=actual.nivel + 1)
                    self.cantidad += 1
                    return actual.izquierdo
                actual = actual.izquierdo
            else:
                if actual.derecho is None:
                    actual.derecho = self.crear_nodo(valor, nivel=actual.nivel + 1)
                    self.cantidad += 1
                    return actual.derecho
                actual = actual.derecho

El nivel se calcula sumando uno al nivel del nodo padre, lo que simplifica recorridos posteriores.

4.4 Carga desde lista

La forma más rápida de poblar el árbol con datos desordenados es recorrer una lista y enviar cada valor a insertar. El método siguiente también reinicia el árbol cuando se indica.

class ArbolBinarioOrdenado:
    # ...
    def construir_desde_lista(self, valores, reiniciar=True):
        if reiniciar:
            self.limpiar_arbol()
        for valor in valores:
            self.insertar(valor)

Esta rutina es ideal para pruebas: basta con definir una lista de enteros y llamarla con reiniciar en True para descartar el contenido anterior.

4.5 Construcción balanceada desde datos ordenados

Si la lista ya viene ordenada podemos crear un árbol balanceado seleccionando el elemento central como raíz y repitiendo el proceso en cada mitad. El resultado mantiene alturas similares entre los subárboles.

class ArbolBinarioOrdenado:
    # ...
    def _construir_balanceado_rec(self, valores, inicio, fin, nivel):
        if inicio > fin:
            return None
        medio = (inicio + fin) // 2
        nodo = self.crear_nodo(valores[medio], nivel=nivel)
        nodo.izquierdo = self._construir_balanceado_rec(valores, inicio, medio - 1, nivel + 1)
        nodo.derecho = self._construir_balanceado_rec(valores, medio + 1, fin, nivel + 1)
        self.cantidad += 1
        return nodo

    def construir_balanceado(self, valores):
        self.limpiar_arbol()
        if not valores:
            return True
        self.cantidad = 0
        self.raiz = self._construir_balanceado_rec(valores, 0, len(valores) - 1, 0)
        return self.raiz is not None

El método devuelve True si pudo crear la estructura completa; en caso de fallo deja el árbol vacío para evitar estados inconsistentes.

4.6 Construcción interactiva

Cuando los datos provienen de la entrada estándar debemos validar cada valor. El siguiente ejemplo solicita enteros hasta que el usuario ingresa 0:

def cargar_interactivo(arbol):
    while True:
        try:
            valor = int(input("Ingrese un entero (0 para terminar): "))
        except ValueError:
            print("Entrada invalida, cancelando.")
            break
        if valor == 0:
            break
        arbol.insertar(valor)

Este patrón se adapta a cualquier fuente de datos mientras podamos convertir la entrada a entero.

4.7 Herramientas de depuración

Para comprobar que la construcción fue correcta conviene implementar al menos un recorrido en preorden y otro en inorden.

def imprimir_preorden(nodo):
    if nodo is None:
        return
    print(f"{nodo.valor} (nivel {nodo.nivel})")
    imprimir_preorden(nodo.izquierdo)
    imprimir_preorden(nodo.derecho)


def imprimir_inorden(nodo):
    if nodo is None:
        return
    imprimir_inorden(nodo.izquierdo)
    print(nodo.valor, end=" ")
    imprimir_inorden(nodo.derecho)

El recorrido inorden de un árbol de búsqueda binaria debería producir una secuencia ordenada ascendentemente; si no ocurre, la construcción contiene errores.

4.8 Escenario práctico

Juntando todo podemos construir un árbol con datos desordenados, revisarlo y luego reemplazarlo por una versión balanceada:

def demo_construccion():
    arbol = ArbolBinarioOrdenado()

    datos = [10, 5, 20, 3, 7, 15]
    arbol.construir_desde_lista(datos)
    print("Preorden original:")
    arbol.imprimir_preorden(arbol.raiz)

    ordenados = [2, 4, 6, 8, 12, 16, 18]
    arbol.construir_balanceado(ordenados)
    print("\\nPreorden balanceado:")
    arbol.imprimir_preorden(arbol.raiz)

    arbol.limpiar_arbol()

El ejemplo anterior permite comparar la distribución de niveles antes y después de balancear.

4.9 Código completo listo para Python 3

El siguiente archivo integra todas las funciones y crea dos árboles: uno con inserciones secuenciales y otro balanceado a partir de una lista ordenada.

class Nodo:
    def __init__(self, valor, nivel=0):
        self.valor = valor
        self.izquierdo = None
        self.derecho = None
        self.nivel = nivel


class ArbolBinarioOrdenado:
    def __init__(self):
        self.raiz = None
        self.cantidad = 0

    def crear_nodo(self, valor, nivel=0):
        return Nodo(valor, nivel=nivel)

    def insertar(self, valor):
        if self.raiz is None:
            self.raiz = self.crear_nodo(valor)
            self.cantidad = 1
            return self.raiz

        actual = self.raiz
        while True:
            if valor < actual.valor:
                if actual.izquierdo is None:
                    actual.izquierdo = self.crear_nodo(valor, nivel=actual.nivel + 1)
                    self.cantidad += 1
                    return actual.izquierdo
                actual = actual.izquierdo
            else:
                if actual.derecho is None:
                    actual.derecho = self.crear_nodo(valor, nivel=actual.nivel + 1)
                    self.cantidad += 1
                    return actual.derecho
                actual = actual.derecho

    def construir_desde_lista(self, valores, reiniciar=True):
        if reiniciar:
            self.limpiar_arbol()
        for valor in valores:
            self.insertar(valor)

    def _construir_balanceado_rec(self, valores, inicio, fin, nivel):
        if inicio > fin:
            return None
        medio = (inicio + fin) // 2
        nodo = self.crear_nodo(valores[medio], nivel=nivel)
        nodo.izquierdo = self._construir_balanceado_rec(valores, inicio, medio - 1, nivel + 1)
        nodo.derecho = self._construir_balanceado_rec(valores, medio + 1, fin, nivel + 1)
        self.cantidad += 1
        return nodo

    def construir_balanceado(self, valores):
        self.limpiar_arbol()
        if not valores:
            return True
        self.cantidad = 0
        self.raiz = self._construir_balanceado_rec(valores, 0, len(valores) - 1, 0)
        return self.raiz is not None

    def imprimir_preorden(self, nodo):
        if nodo is None:
            return
        print(f"{nodo.valor} (nivel {nodo.nivel})")
        self.imprimir_preorden(nodo.izquierdo)
        self.imprimir_preorden(nodo.derecho)

    def liberar_subarbol(self, nodo):
        if nodo is None:
            return
        self.liberar_subarbol(nodo.izquierdo)
        self.liberar_subarbol(nodo.derecho)
        nodo.izquierdo = None
        nodo.derecho = None

    def limpiar_arbol(self):
        self.liberar_subarbol(self.raiz)
        self.raiz = None
        self.cantidad = 0


def imprimir_inorden(nodo):
    if nodo is None:
        return
    imprimir_inorden(nodo.izquierdo)
    print(nodo.valor, end=" ")
    imprimir_inorden(nodo.derecho)


if __name__ == "__main__":
    arbol = ArbolBinarioOrdenado()

    datos = [10, 5, 20, 3, 7, 15]
    arbol.construir_desde_lista(datos)

    print("Arbol generado con inserciones secuenciales:")
    arbol.imprimir_preorden(arbol.raiz)
    print("Inorden:")
    imprimir_inorden(arbol.raiz)
    print()

    ordenados = [2, 4, 6, 8, 12, 16, 18]
    if arbol.construir_balanceado(ordenados):
        print("\nArbol balanceado:")
        arbol.imprimir_preorden(arbol.raiz)
        print("Inorden:")
        imprimir_inorden(arbol.raiz)
        print()
    else:
        print("No se pudo construir el arbol balanceado.")

    arbol.limpiar_arbol()