← Estructuras de datos desde cero · Lección 18/32
Recorridos en profundidad
Tres órdenes, una sola idea
Recorrer un árbol en profundidad (DFS, *depth-first search*) es bajar por una rama hasta el fondo antes de pasar a la siguiente. Con recursión, en cada nodo hay tres cosas que hacer: visitar el nodo, recorrer su izquierda y recorrer su derecha. Según cuándo visitas el nodo, sale un orden distinto:
- Preorden: nodo, izquierda, derecha. Primero el jefe, luego los subordinados.
- Inorden: izquierda, nodo, derecha.
- Postorden: izquierda, derecha, nodo. Primero los hijos, al final el padre.
$ node leccion.js
Pulsa CORRER (o Ctrl+Enter).
Qué mirar
- El preorden empieza con la raíz (8) y el postorden termina con ella.
- El inorden sale ordenado: 1, 3, 6, 8, 10, 14. No es casualidad: en este árbol todo lo de la izquierda de cada nodo es menor y todo lo de la derecha, mayor. Eso es un árbol de búsqueda, la siguiente unidad.
- Las tres funciones son iguales salvo por dónde va el
push.
Toda la lección en una página
Tres órdenes, una sola idea
Recorrer un árbol en profundidad (DFS, *depth-first search*) es bajar por una rama hasta el fondo antes de pasar a la siguiente. Con recursión, en cada nodo hay tres cosas que hacer: visitar el nodo, recorrer su izquierda y recorrer su derecha. Según cuándo visitas el nodo, sale un orden distinto:
- Preorden: nodo, izquierda, derecha. Primero el jefe, luego los subordinados.
- Inorden: izquierda, nodo, derecha.
- Postorden: izquierda, derecha, nodo. Primero los hijos, al final el padre.
class NodoArbol {
constructor(valor, izq = null, der = null) {
this.valor = valor;
this.izq = izq;
this.der = der;
}
}
function arbol(niveles) {
if (!niveles.length || niveles[0] === null) return null;
const raiz = new NodoArbol(niveles[0]);
const cola = [raiz];
let i = 1;
while (i < niveles.length) {
const nodo = cola.shift();
if (niveles[i] != null) { nodo.izq = new NodoArbol(niveles[i]); cola.push(nodo.izq); }
i++;
if (i < niveles.length && niveles[i] != null) { nodo.der = new NodoArbol(niveles[i]); cola.push(nodo.der); }
i++;
}
return raiz;
}
function preorden(n, r = []) {
if (!n) return r;
r.push(n.valor); preorden(n.izq, r); preorden(n.der, r);
return r;
}
function inorden(n, r = []) {
if (!n) return r;
inorden(n.izq, r); r.push(n.valor); inorden(n.der, r);
return r;
}
function postorden(n, r = []) {
if (!n) return r;
postorden(n.izq, r); postorden(n.der, r); r.push(n.valor);
return r;
}
const raiz = arbol([8, 3, 10, 1, 6, null, 14]);
console.log("preorden: ", preorden(raiz));
console.log("inorden: ", inorden(raiz));
console.log("postorden:", postorden(raiz));
- El preorden empieza con la raíz (8) y el postorden termina con ella.
- El inorden sale ordenado: 1, 3, 6, 8, 10, 14. No es casualidad: en este árbol todo lo de la izquierda de cada nodo es menor y todo lo de la derecha, mayor. Eso es un árbol de búsqueda, la siguiente unidad.
- Las tres funciones son iguales salvo por dónde va el
push.
La altura en una línea
Muchos problemas de árboles se resuelven con la misma forma: «pregúntale a tu izquierda, pregúntale a tu derecha y combina». La altura (o profundidad máxima) de un árbol es 1 más la mayor altura de sus dos subárboles; la de un árbol vacío es 0.
Es postorden: el nodo contesta después de oír a sus hijos. Es el reto «Maximum Depth of Binary Tree».
class NodoArbol {
constructor(valor, izq = null, der = null) {
this.valor = valor;
this.izq = izq;
this.der = der;
}
}
function arbol(niveles) {
if (!niveles.length || niveles[0] === null) return null;
const raiz = new NodoArbol(niveles[0]);
const cola = [raiz];
let i = 1;
while (i < niveles.length) {
const nodo = cola.shift();
if (niveles[i] != null) { nodo.izq = new NodoArbol(niveles[i]); cola.push(nodo.izq); }
i++;
if (i < niveles.length && niveles[i] != null) { nodo.der = new NodoArbol(niveles[i]); cola.push(nodo.der); }
i++;
}
return raiz;
}
function altura(nodo) {
if (nodo === null) return 0;
return 1 + Math.max(altura(nodo.izq), altura(nodo.der));
}
console.log(altura(arbol([8, 3, 10, 1, 6, null, 14])));
console.log(altura(arbol([1, 2, null, 3, null, 4]))); // una vara hacia la izquierda
console.log(altura(arbol([])));
3 para el árbol de siempre; 4 para la vara (cuatro nodos, uno por nivel), y 0 para el vacío.
El espejo y el ancestro común
Invertir un árbol (hacerlo espejo) es intercambiar los dos hijos de cada nodo. Con recursión: invierte la izquierda, invierte la derecha y cámbialas de lado. El inorden del espejo sale exactamente al revés.
El ancestro común más bajo de dos nodos es el nodo más profundo que tiene a los dos debajo. La misma forma: si el nodo es uno de los dos, contesta él; si no, pregunta a sus dos lados. Si los dos lados encontraron algo, los nodos están separados y el ancestro es él.
class NodoArbol {
constructor(valor, izq = null, der = null) {
this.valor = valor;
this.izq = izq;
this.der = der;
}
}
function arbol(niveles) {
if (!niveles.length || niveles[0] === null) return null;
const raiz = new NodoArbol(niveles[0]);
const cola = [raiz];
let i = 1;
while (i < niveles.length) {
const nodo = cola.shift();
if (niveles[i] != null) { nodo.izq = new NodoArbol(niveles[i]); cola.push(nodo.izq); }
i++;
if (i < niveles.length && niveles[i] != null) { nodo.der = new NodoArbol(niveles[i]); cola.push(nodo.der); }
i++;
}
return raiz;
}
function inorden(n, r = []) {
if (n) { inorden(n.izq, r); r.push(n.valor); inorden(n.der, r); }
return r;
}
function invertir(nodo) {
if (nodo === null) return null;
const izq = invertir(nodo.izq);
const der = invertir(nodo.der);
nodo.izq = der;
nodo.der = izq;
return nodo;
}
function ancestro(nodo, p, q) {
if (nodo === null || nodo.valor === p || nodo.valor === q) return nodo;
const izq = ancestro(nodo.izq, p, q);
const der = ancestro(nodo.der, p, q);
if (izq && der) return nodo; // uno a cada lado: soy yo
return izq ?? der; // los dos del mismo lado
}
const raiz = arbol([3, 5, 1, 6, 2, 0, 8, null, null, 7, 4]);
console.log("ancestro de 5 y 1:", ancestro(raiz, 5, 1).valor);
console.log("ancestro de 7 y 4:", ancestro(raiz, 7, 4).valor);
console.log("ancestro de 5 y 4:", ancestro(raiz, 5, 4).valor);
console.log("antes: ", inorden(raiz));
invertir(raiz);
console.log("espejo:", inorden(raiz));
- El 5 y el 1 están a cada lado de la raíz: su ancestro es el 3. El 7 y el 4 cuelgan del 2.
- Para el 5 y el 4 sale el 5: un nodo cuenta como ancestro de sí mismo, y el 4 está debajo de él.
- El inorden del espejo es el de antes, leído de atrás para adelante.