TL;DR
- El Problema: Optimización de complejidad temporal y espacial para estructuras de datos clave.
- El Enfoque: Problema estilo CTCI 4.6 para principiantes: halla el sucesor in-order de un nodo en un árbol de búsqueda binaria cuando cada nodo tiene enlace al padre. Más a la izquierda del subárbol derecho, o sube padres hasta no ser hijo derecho.
- Complejidad: Relación óptima de tiempo y espacio con gestión de casos límite.
El recorrido in-order de un BST imprime las claves en orden. Dado un nodo, el sucesor es la siguiente clave que ese recorrido visitaría. No reinicias desde la raíz ni barres todo el árbol. Ya tienes el nodo, y cada nodo tiene un puntero parent.
Este post es enseñanza original para principiantes en Java. Misma familia que las preguntas clásicas de sucesor en BST, no una copia de libro. Parte de la serie CTCI en Java.
1. Analogía de la fila ordenada
Imagina el BST como una fila de personas ordenadas por altura (o clave). In-order significa: subárbol izquierdo, luego yo, luego subárbol derecho. El sucesor de alguien es quien queda justo a su derecha en esa fila.
Dos formas de encontrarlo sin redibujar toda la fila:
- Tienes rama a la derecha. El siguiente no es tu hijo derecho. Es la persona más a la izquierda de esa rama derecha (la clave más pequeña que sigue siendo mayor que la tuya).
- No tienes rama derecha. Ya terminaste tu izquierda y a ti. Sube hacia la raíz mientras sigas siendo hijo derecho. El primer ancestro para el que estés a la izquierda es el siguiente en la fila. Si pasas la raíz, eras el último.
Los enlaces al padre son la escalera. Sin ellos buscarías desde la raíz cada vez.
2. Problema en palabras simples
Objetivo: dado un nodo n en un árbol de búsqueda binaria, devolver el sucesor in-order de n, o null si n es el último.
Supuestos:
- Los nodos tienen
left,rightyparent. - El árbol es un BST (claves izquierdas menores, derechas mayores), o al menos solo necesitas el siguiente nodo estructural in-order.
- Puedes partir solo de
n; no recibes la raíz por separado salvo que subas hasta ella.
Aclara antes de codear:
- ¿Y si
nes null? (Devuelve null.) - ¿Y si
nno tiene padre ni hijo derecho? (Es la raíz y el último; devuelve null.) - ¿Claves duplicadas? (El problema suele asumir claves únicas. Di tu regla si preguntan.)
3. Piensa primero
Primera idea mala: volcar todo el in-order
Recorre el árbol a una lista, busca n, devuelve el índice siguiente. Correcto pero O(N) en tiempo y espacio. Quieren O(H) con padres, donde H es la altura.
Caso A: existe hijo derecho
El sucesor es el mínimo del subárbol derecho:
- Ve a
n.right. - Mientras
leftno sea null, ve a la izquierda. - Ese nodo es la respuesta.
¿Por qué? In-order hace izquierda, nodo, derecha. Tras n, la primera visita en el subárbol derecho es su nodo más a la izquierda.
Caso B: sin hijo derecho
Sube por los padres:
- Pon
p = n.parent,c = n. - Mientras
pno sea null yc == p.right(sigues siendo hijo derecho), hazc = p,p = p.parent. - Devuelve
p(puede ser null si eras el último del árbol).
¿Por qué? Terminaste un subárbol derecho. Sigue subiendo hasta entrar a un nodo desde la izquierda. Ese nodo aún no se ha "visitado" en el recorrido in-order mental.
Esquema
20
/ \
10 30
/ \ \
5 15 40
/
12
| Nodo | Sucesor | Por qué |
|---|---|---|
| 10 | 12 | hay hijo derecho 15; lo más a la izquierda de esa rama es 12 |
| 15 | 20 | sin derecha; 15 es derecho de 10, sube; 10 es izquierdo de 20 → 20 |
| 40 | null | sin derecha; sube como derecho de 30, luego de 20; la raíz no tiene padre |
| 5 | 10 | sin derecha; 5 es izquierdo de 10 → padre 10 |
4. Solución en Java
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode parent;
TreeNode(int val) {
this.val = val;
}
}
class Solution {
/** Sucesor in-order de n, o null si n es el último / null. */
TreeNode inOrderSuccessor(TreeNode n) {
if (n == null) {
return null;
}
// Caso A: hay subárbol derecho → más a la izquierda de la derecha
if (n.right != null) {
return leftMostChild(n.right);
}
// Caso B: subir hasta no ser hijo derecho
TreeNode current = n;
TreeNode p = n.parent;
while (p != null && p.right == current) {
current = p;
p = p.parent;
}
return p;
}
private TreeNode leftMostChild(TreeNode n) {
if (n == null) {
return null;
}
while (n.left != null) {
n = n.left;
}
return n;
}
}
Notas del helper:
leftMostChildes la misma idea que "mínimo en un subárbol BST".- El bucle de subida para cuando
p == null(sin sucesor) o cuandocurrentesp.left(encontraste el ancestro siguiente). - No necesitas la raíz como argumento aparte si los padres están completos.
Opcional: si el entrevistador prohíbe padres, buscas desde la raíz con un candidato (último nodo mayor que n al caminar). Es otro planteamiento; este post se queda con enlaces al padre.
5. Tabla de complejidad
| Enfoque | Tiempo | Espacio extra |
|---|---|---|
| Sucesor con padres (esta solución) | O(H) | O(1) |
| Lista in-order completa e índice | O(N) | O(N) |
| Desde la raíz sin padres (candidato) | O(H) | O(1) |
H es la altura. Árbol equilibrado ≈ log N. Sesgado puede ser N. El espacio del paseo con padres es constante.
6. Casos límite y errores comunes
Los entrevistadores tocan esto:
- Entrada null → devuelve null.
- Nodo más a la derecha → sube a la raíz y luego null. El último in-order no tiene sucesor.
- Raíz solo con subárbol izquierdo → si pides el sucesor de la raíz y no tiene derecha, null (la raíz es la última si no hay derecha).
- Hoja que es hija izquierda → el sucesor es su padre (casi sin vueltas del bucle).
- Espina derecha profunda → la subida puede tocar muchos padres; sigue siendo O(H), no es un bug.
Errores comunes:
- Devolver el hijo derecho tal cual en vez del más a la izquierda del subárbol derecho. Te saltas la cadena izquierda bajo ese hijo.
- Subir solo un padre siempre. Hay que iterar mientras sigas siendo hijo derecho.
- Olvidar que parent es null en la raíz y hacer NPE en
p.right. - Confundir sucesor con predecesor. El predecesor es simétrico: sin izquierda → subir mientras seas hijo izquierdo; o el más a la derecha del subárbol izquierdo.
- Asumir árbol equilibrado al citar el tiempo. Di O(H) y el peor caso O(N).
- Mutar el árbol para enhebrar padres al vuelo. No hace falta si ya existen.
Boceto mínimo de uso:
// Construye un árbol pequeño con parents en ambos sentidos, luego:
TreeNode fifteen = /* nodo 15 */;
TreeNode next = new Solution().inOrderSuccessor(fifteen); // 20 en el esquema de arriba
7. Resumen para contárselo a un amigo
Successor es "quién va después en orden / in-order" para un nodo del BST:
- Si el nodo tiene hijo derecho, ve una vez a la derecha y luego a la izquierda hasta no poder. Ese nodo es el siguiente.
- Si no, sube padres mientras sigas siendo el hijo derecho. El primer padre al que llegaste desde la izquierda es el siguiente.
- Si se acaban los padres, no hay siguiente.
- Los punteros parent dan O(altura) y O(1) de espacio extra. Sin volcar el árbol.
Si dibujas los dos casos en la pizarra y recorres 15 → 20 y 40 → null en un árbol de ejemplo, dominas el 4.6.
Serie
- Guía: Guía de la serie CTCI
- Anterior: Validate BST
- Siguiente: Build Order
