2 - Componentes básicos de un árbol

Profundizamos en cada pieza que forma un árbol binario: cómo se representa el nodo, qué rol cumplen la raíz y las hojas, y cómo medir niveles, alturas y grados para programar operaciones seguras. Estos cimientos nos acompañarán durante todo el tutorial.

2.1 Nodo básico y memoria

El nodo es un struct que almacena el valor y las referencias a sus dos hijos. Mantener los punteros inicializados en NULL evita estados indefinidos y facilita detectar hojas.

typedef struct TreeNode {
  int valor;
  struct TreeNode *izquierdo;
  struct TreeNode *derecho;
  unsigned char nivel;
} TreeNode;

TreeNode *crearNodo(int valor) {
  TreeNode *nodo = malloc(sizeof(TreeNode));
  if (!nodo) return NULL;
  nodo->valor = valor;
  nodo->izquierdo = NULL;
  nodo->derecho = NULL;
  nodo->nivel = 0;
  return nodo;
}

El campo nivel almacena la profundidad relativa del nodo; lo actualizaremos al enlazarlo con su padre.

2.2 Raíz y estado vacío

La raíz es el punto de entrada para cualquier operación. Si la raíz vale NULL, el árbol está vacío. Conviene encapsular este dato en una estructura contenedora junto con el conteo de nodos.

typedef struct {
  TreeNode *raiz;
  unsigned int cantidad;
} ArbolBinario;

void inicializarArbol(ArbolBinario *arbol) {
  arbol->raiz = NULL;
  arbol->cantidad = 0;
}

Con este contenedor podemos comprobar si el árbol está vacío antes de insertar o recorrer, evitando accesos a punteros nulos.

2.3 Hijos, hojas y enlaces

Los hijos izquierdo y derecho conectan cada nodo con sus subárboles. Un nodo es una hoja si carece de hijos. Identificarlas permite saber dónde insertar nuevos valores o cómo liberar memoria de abajo hacia arriba.

int esHoja(TreeNode *nodo) {
  return nodo && !nodo->izquierdo && !nodo->derecho;
}

El resto de enlaces se actualiza al crear hijos. Mantener esta relación coherente es clave para los recorridos recursivos.

2.4 Niveles y profundidad

El nivel indica cuántas aristas separan un nodo de la raíz. Cuando insertamos un hijo, basta con sumar 1 al nivel del padre para conservar la información actualizada.

TreeNode *crearHijo(TreeNode *padre, int valor, int esIzquierdo) {
  TreeNode *nuevo = crearNodo(valor);
  if (!nuevo) return NULL;
  nuevo->nivel = padre ? (unsigned char)(padre->nivel + 1) : 0;

  if (padre) {
    if (esIzquierdo) padre->izquierdo = nuevo;
    else padre->derecho = nuevo;
  }
  return nuevo;
}

Con la profundidad almacenada en cada nodo podemos visualizar el árbol, limitar la exploración o implementar algoritmos como minimax.

2.5 Altura y tamaño

La altura corresponde al nivel máximo alcanzado por cualquier nodo. Calcularla deja clara la forma del árbol y es un indicador del costo de búsqueda.

int altura(TreeNode *nodo) {
  if (!nodo) return -1; /* arbol vacio */
  int izquierda = altura(nodo->izquierdo);
  int derecha = altura(nodo->derecho);
  return 1 + (izquierda > derecha ? izquierda : derecha);
}

El tamaño total del árbol se mantiene en ArbolBinario.cantidad, incrementándolo al crear nodos y decrementándolo al eliminarlos.

2.6 Grado y validaciones

El grado indica cuántos hijos inmediatos posee un nodo. En árboles binarios solo puede ser 0, 1 o 2. Controlarlo sirve para verificar que ninguna rutina rompa las reglas de la estructura.

int grado(TreeNode *nodo) {
  if (!nodo) return 0;
  int g = 0;
  if (nodo->izquierdo) g++;
  if (nodo->derecho) g++;
  return g;
}

Las hojas tienen grado 0 y los nodos internos 1 o 2. Este dato guía las funciones de inserción, eliminación y balanceo.

2.7 Resumen operativo

Conocer estos componentes nos permite inspeccionar el árbol en cualquier punto: sabemos si está vacío, cuántos nodos contiene, dónde están las hojas y cuál es su altura. Estos diagnósticos evitan bugs cuando avancemos hacia el modelado de nodos y la construcción del árbol en C.