Skip to content
Ayotl

← Data structures from scratch · Lesson 18/32

Depth-first traversals

✕
1/8

Three orders, one idea

Walking a tree depth-first (DFS) means going down one branch all the way before moving to the next. With recursion, at each node there are three things to do: visit the node, walk its left side and walk its right side. Depending on when you visit the node, you get a different order:

  • Preorder: node, left, right. Boss first, then the team.
  • Inorder: left, node, right.
  • Postorder: left, right, node. Children first, parent last.
83101614
leccion.js

$ node leccion.js

Press RUN (or Ctrl+Enter).

What to look at

  • Preorder starts with the root (8) and postorder ends with it.
  • Inorder comes out sorted: 1, 3, 6, 8, 10, 14. Not by chance: in this tree everything left of each node is smaller and everything right is bigger. That's a search tree, the next unit.
  • The three functions are identical except for where the push goes.
The whole lesson on one page

Three orders, one idea

Walking a tree depth-first (DFS) means going down one branch all the way before moving to the next. With recursion, at each node there are three things to do: visit the node, walk its left side and walk its right side. Depending on when you visit the node, you get a different order:

  • Preorder: node, left, right. Boss first, then the team.
  • Inorder: left, node, right.
  • Postorder: left, right, node. Children first, parent last.
83101614
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("preorder: ", preorden(raiz));
console.log("inorder:  ", inorden(raiz));
console.log("postorder:", postorden(raiz));
  • Preorder starts with the root (8) and postorder ends with it.
  • Inorder comes out sorted: 1, 3, 6, 8, 10, 14. Not by chance: in this tree everything left of each node is smaller and everything right is bigger. That's a search tree, the next unit.
  • The three functions are identical except for where the push goes.

Height in one line

Many tree problems are solved with the same shape: "ask your left, ask your right, and combine". A tree's height (or maximum depth) is 1 plus the larger height of its two subtrees; an empty tree's is 0.

That's postorder: the node answers after hearing from its children. It's the "Maximum Depth of Binary Tree" challenge.

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])));   // a stick leaning left
console.log(altura(arbol([])));

3 for the usual tree; 4 for the stick (four nodes, one per level), and 0 for the empty one.

The mirror and the common ancestor

Inverting a tree (mirroring it) means swapping the two children of every node. Recursively: invert the left, invert the right and swap them. The mirror's inorder comes out exactly reversed.

The lowest common ancestor of two nodes is the deepest node that has both below it. Same shape: if the node is one of the two, it answers itself; otherwise it asks both sides. If both sides found something, the nodes are split up and the ancestor is this one.

351620874
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;      // one on each side: it's me
  return izq ?? der;                // both on the same side
}

const raiz = arbol([3, 5, 1, 6, 2, 0, 8, null, null, 7, 4]);
console.log("ancestor of 5 and 1:", ancestro(raiz, 5, 1).valor);
console.log("ancestor of 7 and 4:", ancestro(raiz, 7, 4).valor);
console.log("ancestor of 5 and 4:", ancestro(raiz, 5, 4).valor);

console.log("before:", inorden(raiz));
invertir(raiz);
console.log("mirror:", inorden(raiz));
  • 5 and 1 are on either side of the root: their ancestor is 3. 7 and 4 hang from 2.
  • For 5 and 4 you get 5: a node counts as its own ancestor, and 4 is below it.
  • The mirror's inorder is the previous one, read back to front.

Practice with a video challenge

Further reading