6 - Inserción y eliminación en árboles generales

Ahora que conocemos los recorridos y las operaciones básicas, es momento de modificar la estructura del árbol general: agregar y quitar nodos de forma segura, reorganizar enlaces y liberar memoria correctamente en C.

typedef struct GeneralNode {
  int valor;
  struct GeneralNode *primerHijo;
  struct GeneralNode *siguienteHermano;
  struct GeneralNode *padre;
} GeneralNode;

6.1 Insertar un hijo

La inserción de un hijo en la representación hijo-izquierdo/hermano-derecho se realiza colocando el nuevo nodo al comienzo de la lista de hijos del padre. Esto mantiene la operación en O(1) y permite preservar el orden con una rutina posterior si fuera necesario.

#include <stdlib.h>

GeneralNode *crearNodo(int valor) {
  GeneralNode *nodo = malloc(sizeof(GeneralNode));
  if (!nodo) return NULL;
  nodo->valor = valor;
  nodo->primerHijo = NULL;
  nodo->siguienteHermano = NULL;
  nodo->padre = NULL;
  return nodo;
}

void insertarHijo(GeneralNode *padre, GeneralNode *nuevoHijo) {
  if (!padre || !nuevoHijo) return;
  nuevoHijo->padre = padre;
  nuevoHijo->siguienteHermano = padre->primerHijo;
  padre->primerHijo = nuevoHijo;
}

6.2 Insertar un hermano

Para mantener el orden o insertar un nodo en medio de la lista horizontal, creamos una función que se apoya en el puntero siguienteHermano.

void insertarHermanoDespues(GeneralNode *referencia, GeneralNode *nuevoHermano) {
  if (!referencia || !nuevoHermano) {
    return;
  }
  nuevoHermano->padre = referencia->padre;
  nuevoHermano->siguienteHermano = referencia->siguienteHermano;
  referencia->siguienteHermano = nuevoHermano;
}

Si necesitamos insertar antes de un hermano específico conservamos los punteros en una función auxiliar que recorra el listado hasta encontrar el anterior.

6.3 Eliminar un nodo hoja

Para eliminar una hoja basta con ajustar el puntero del padre y liberar la memoria asociada. Este procedimiento se implementa registrando el hijo previo durante el recorrido de hermanos.

void eliminarHoja(GeneralNode *padre, GeneralNode *hoja) {
  if (!padre || !hoja || hoja->primerHijo) {
    return;
  }
  GeneralNode *anterior = NULL;
  GeneralNode *actual = padre->primerHijo;
  while (actual && actual != hoja) {
    anterior = actual;
    actual = actual->siguienteHermano;
  }
  if (!actual) {
    return;
  }
  if (anterior) {
    anterior->siguienteHermano = actual->siguienteHermano;
  } else {
    padre->primerHijo = actual->siguienteHermano;
  }
  free(actual);
}

6.4 Eliminar un nodo con hijos (subárbol completo)

Cuando el nodo a eliminar tiene descendencia, debemos liberar el subárbol completo y luego desconectarlo del padre. Este patrón recursivo es similar al recorrido postorden.

void eliminarSubarbol(GeneralNode *nodo) {
  if (!nodo) {
    return;
  }
  GeneralNode *hijo = nodo->primerHijo;
  while (hijo) {
    GeneralNode *siguiente = hijo->siguienteHermano;
    eliminarSubarbol(hijo);
    hijo = siguiente;
  }
  free(nodo);
}

void desconectarDePadre(GeneralNode *nodo) {
  if (!nodo || !nodo->padre) {
    return;
  }
  GeneralNode *padre = nodo->padre;
  GeneralNode *anterior = NULL;
  GeneralNode *actual = padre->primerHijo;
  while (actual && actual != nodo) {
    anterior = actual;
    actual = actual->siguienteHermano;
  }
  if (!actual) {
    return;
  }
  if (anterior) {
    anterior->siguienteHermano = actual->siguienteHermano;
  } else {
    padre->primerHijo = actual->siguienteHermano;
  }
}

void eliminarNodo(GeneralNode *nodo) {
  if (!nodo) {
    return;
  }
  desconectarDePadre(nodo);
  eliminarSubarbol(nodo);
}

6.5 Reorganización de enlaces

Reubicar un subárbol implica desconectarlo de su padre actual y volver a enlazarlo bajo el nuevo padre. Conviene encapsular esta operación para garantizar que el puntero padre y los hermanos se actualicen correctamente.

void moverSubarbol(GeneralNode *nodo, GeneralNode *nuevoPadre) {
  if (!nodo || !nuevoPadre || nodo == nuevoPadre) {
    return;
  }
  desconectarDePadre(nodo);
  nodo->padre = nuevoPadre;
  nodo->siguienteHermano = nuevoPadre->primerHijo;
  nuevoPadre->primerHijo = nodo;
}

Antes de mover nodos verifica que no generes ciclos (por ejemplo, asignando un nodo como hijo de uno de sus descendientes).

6.6 Liberación completa del árbol

Cuando el árbol ya no se necesita debemos liberar todos los nodos empezando por la raíz. La siguiente función es una versión iterativa con pila para evitar desbordes en árboles grandes.

typedef struct StackNode {
  GeneralNode *nodo;
  struct StackNode *sig;
} StackNode;

static void push(StackNode **pila, GeneralNode *nodo) {
  StackNode *nuevo = malloc(sizeof(StackNode));
  if (!nuevo) return;
  nuevo->nodo = nodo;
  nuevo->sig = *pila;
  *pila = nuevo;
}

static GeneralNode *pop(StackNode **pila) {
  if (!*pila) return NULL;
  StackNode *tmp = *pila;
  GeneralNode *nodo = tmp->nodo;
  *pila = tmp->sig;
  free(tmp);
  return nodo;
}

void liberarArbol(GeneralNode *raiz) {
  if (!raiz) {
    return;
  }
  StackNode *pila = NULL;
  push(&pila, raiz);
  while (pila) {
    GeneralNode *actual = pop(&pila);
    for (GeneralNode *hijo = actual->primerHijo; hijo; hijo = hijo->siguienteHermano) {
      push(&pila, hijo);
    }
    free(actual);
  }
}

Si preferimos una versión recursiva basta con llamar a eliminarSubarbol sobre la raíz.

6.7 Errores típicos con múltiples punteros

  • Olvidar actualizar padre: genera inconsistencias al calcular profundidades o mover subárboles.
  • Desconectar sin buscar el hermano anterior: provoca listas de hermanos corruptas y accesos a memoria liberada.
  • Reinsertar un nodo ya enlazado: puede crear ciclos que cuelgan los recorridos. Siempre desconecta antes de mover.
  • Perder referencias a hijos: insertar hermanos sin enlazar el resto de la lista deja nodos inaccesibles, produciendo fugas.
  • Duplicar liberaciones: llamar dos veces a free sobre el mismo nodo provoca comportamientos indefinidos; controla un flujo de borrado único.

Adoptar pruebas unitarias simples que inserten, muevan y eliminen nodos ayuda a detectar estos errores antes de que el árbol crezca en el proyecto real.