Recursividad


¿Qué es la Recursividad?

La recursividad es una técnica de programación fundamental donde una función se llama a sí misma para resolver un problema complejo dividiéndolo en subproblemas más pequeños pero estructuralmente idénticos. Es como resolver un rompecabezas gigante dividiéndolo en piezas más pequeñas del mismo tipo hasta llegar a piezas tan simples que puedas resolver directamente.

¿Para qué se utiliza la Recursividad?

La recursividad es utilizada por desarrolladores para:

  • Resolver problemas que tienen una estructura repetitiva o fractal.
  • Procesar estructuras de datos como árboles, listas enlazadas y grafos.
  • Implementar algoritmos matemáticos como factorial, Fibonacci y cálculos exponenciales.
  • Navegar sistemas de archivos y directorios anidados.
  • Implementar parsers y compiladores para lenguajes de programación.
  • Resolver problemas de divide y vencerás como quicksort y mergesort.

¿Cómo funciona?

La recursividad funciona como muñecas rusas (matrioshkas) - cada muñeca contiene una versión más pequeña de sí misma hasta llegar a la muñeca más pequeña que ya no se puede abrir. En programación, cada llamada recursiva resuelve una versión más pequeña del problema hasta llegar al “caso base” que se puede resolver directamente.

Anatomía de una función recursiva

Toda función recursiva debe tener dos elementos esenciales:

1. Caso Base (Condición de parada)

¿Qué es? La condición que detiene la recursión cuando el problema se vuelve lo suficientemente simple.

function factorial(n) {
  // CASO BASE: la recursión se detiene aquí
  if (n <= 1) {
    return 1; // El factorial de 0 y 1 es 1
  }

  // CASO RECURSIVO: la función se llama a sí misma
  return n * factorial(n - 1);
}

// Ejemplo de ejecución:
// factorial(5)
// = 5 * factorial(4)
// = 5 * (4 * factorial(3))
// = 5 * (4 * (3 * factorial(2)))
// = 5 * (4 * (3 * (2 * factorial(1))))
// = 5 * (4 * (3 * (2 * 1)))  ← caso base alcanzado
// = 5 * (4 * (3 * 2))
// = 5 * (4 * 6)
// = 5 * 24
// = 120

2. Caso Recursivo (Llamada a sí misma)

¿Qué es? La parte donde la función se llama a sí misma con un problema más pequeño.

function fibonacci(n) {
  // Casos base
  if (n <= 0) return 0;
  if (n === 1) return 1;

  // Caso recursivo: suma de los dos números anteriores
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// Árbol de llamadas para fibonacci(5):
//                    fibonacci(5)
//                   /            \
//              fibonacci(4)    fibonacci(3)
//              /         \        /         \
//         fibonacci(3) fibonacci(2)  fibonacci(2) fibonacci(1)
//         /       \       /    \        /    \
//    fibonacci(2) fibonacci(1) fibonacci(1) fibonacci(0) fibonacci(1) fibonacci(0)
//    /       \
// fibonacci(1) fibonacci(0)

Ejemplos prácticos de recursividad

// Recorrer un árbol binario
class TreeNode {
  constructor(value, left = null, right = null) {
    this.value = value;
    this.left = left;
    this.right = right;
  }
}

function traverseTree(node) {
  // Caso base: nodo vacío
  if (!node) {
    return [];
  }

  // Caso recursivo: procesar nodo actual y sus hijos
  return [
    node.value, // Nodo actual
    ...traverseTree(node.left), // Subárbol izquierdo
    ...traverseTree(node.right), // Subárbol derecho
  ];
}

// Uso
const tree = new TreeNode(
  1,
  new TreeNode(2, new TreeNode(4), new TreeNode(5)),
  new TreeNode(3, null, new TreeNode(6))
);

console.log(traverseTree(tree)); // [1, 2, 4, 5, 3, 6]

Procesamiento de arrays anidados

// Aplanar arrays multidimensionales
function flattenArray(arr) {
  const result = [];

  for (const element of arr) {
    if (Array.isArray(element)) {
      // Caso recursivo: elemento es array
      result.push(...flattenArray(element));
    } else {
      // Caso base: elemento no es array
      result.push(element);
    }
  }

  return result;
}

// Ejemplos
console.log(flattenArray([1, [2, 3], [4, [5, 6]]]));
// Resultado: [1, 2, 3, 4, 5, 6]

console.log(flattenArray([1, [2, [3, [4, [5]]]]]));
// Resultado: [1, 2, 3, 4, 5]

// Versión más concisa con reduce
function flattenRecursive(arr) {
  return arr.reduce((flat, item) => {
    return flat.concat(Array.isArray(item) ? flattenRecursive(item) : item);
  }, []);
}

Búsqueda en estructuras complejas

// Encontrar un valor en un objeto anidado
function findInObject(obj, searchValue, path = []) {
  // Caso base: valor encontrado
  if (obj === searchValue) {
    return path;
  }

  // Caso base: no es objeto
  if (typeof obj !== 'object' || obj === null) {
    return null;
  }

  // Caso recursivo: buscar en propiedades
  for (const [key, value] of Object.entries(obj)) {
    const result = findInObject(value, searchValue, [...path, key]);
    if (result) {
      return result; // Valor encontrado en esta rama
    }
  }

  return null; // Valor no encontrado
}

// Uso
const complexObject = {
  user: {
    profile: {
      settings: {
        theme: 'dark',
        notifications: {
          email: true,
          push: false,
        },
      },
    },
    posts: [
      { id: 1, title: 'Hello World' },
      { id: 2, title: 'Recursion Explained' },
    ],
  },
};

console.log(findInObject(complexObject, 'dark'));
// Resultado: ['user', 'profile', 'settings', 'theme']

console.log(findInObject(complexObject, 'Recursion Explained'));
// Resultado: ['user', 'posts', '1', 'title']

Algoritmos de ordenamiento recursivos

// Quick Sort - Divide y vencerás
function quickSort(arr) {
  // Caso base: array vacío o con un elemento
  if (arr.length <= 1) {
    return arr;
  }

  const pivot = arr[Math.floor(arr.length / 2)];
  const left = [];
  const right = [];
  const equal = [];

  // Dividir array en tres partes
  for (const element of arr) {
    if (element < pivot) {
      left.push(element);
    } else if (element > pivot) {
      right.push(element);
    } else {
      equal.push(element);
    }
  }

  // Caso recursivo: ordenar partes y combinar
  return [
    ...quickSort(left), // Ordenar menores recursivamente
    ...equal, // Elementos iguales al pivot
    ...quickSort(right), // Ordenar mayores recursivamente
  ];
}

// Merge Sort - Más estable
function mergeSort(arr) {
  // Caso base
  if (arr.length <= 1) {
    return arr;
  }

  // Dividir en mitades
  const mid = Math.floor(arr.length / 2);
  const left = arr.slice(0, mid);
  const right = arr.slice(mid);

  // Caso recursivo: ordenar mitades y combinar
  return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
  const result = [];
  let i = 0,
    j = 0;

  while (i < left.length && j < right.length) {
    if (left[i] <= right[j]) {
      result.push(left[i++]);
    } else {
      result.push(right[j++]);
    }
  }

  return result.concat(left.slice(i), right.slice(j));
}

// Prueba
const numbers = [64, 34, 25, 12, 22, 11, 90];
console.log('QuickSort:', quickSort(numbers));
console.log('MergeSort:', mergeSort(numbers));

Generación de estructuras fractales

// Generar un árbol fractal de directorios
function generateFileTree(depth, maxDepth = 3, baseName = 'folder') {
  // Caso base: profundidad máxima alcanzada
  if (depth >= maxDepth) {
    return {
      name: `${baseName}_${depth}`,
      type: 'file',
      size: Math.floor(Math.random() * 1000) + 100,
    };
  }

  // Caso recursivo: crear subdirectorios
  const children = [];
  const numChildren = Math.floor(Math.random() * 4) + 2; // 2-5 hijos

  for (let i = 0; i < numChildren; i++) {
    children.push(generateFileTree(depth + 1, maxDepth, `${baseName}_${depth}_${i}`));
  }

  return {
    name: `${baseName}_${depth}`,
    type: 'directory',
    children: children,
    totalSize: children.reduce((sum, child) => sum + (child.size || child.totalSize || 0), 0),
  };
}

// Uso
const fileSystem = generateFileTree(0, 4, 'project');
console.log(JSON.stringify(fileSystem, null, 2));

Recursividad con memoización (optimización)

// Fibonacci optimizado con memoización
function fibonacciMemoized() {
  const cache = new Map();

  function fibonacci(n) {
    // Verificar cache primero
    if (cache.has(n)) {
      return cache.get(n);
    }

    // Casos base
    if (n <= 0) return 0;
    if (n === 1) return 1;

    // Calcular y guardar en cache
    const result = fibonacci(n - 1) + fibonacci(n - 2);
    cache.set(n, result);

    return result;
  }

  return fibonacci;
}

// Comparación de rendimiento
const fibMemo = fibonacciMemoized();

console.time('Fibonacci sin memoización');
console.log('fibonacci(40):', fibonacci(40));
console.timeEnd('Fibonacci sin memoización');

console.time('Fibonacci con memoización');
console.log('fibMemo(40):', fibMemo(40));
console.timeEnd('Fibonacci con memoización');

// Resultado típico:
// Fibonacci sin memoización: ~1000ms
// Fibonacci con memoización: ~1ms

Recursión mutua (funciones que se llaman entre sí)

// Determinar si un número es par o impar usando recursión mutua
function isEven(n) {
  if (n === 0) return true; // Caso base
  return isOdd(n - 1); // Llamada a función hermana
}

function isOdd(n) {
  if (n === 0) return false; // Caso base
  return isEven(n - 1); // Llamada a función hermana
}

// Uso
console.log(isEven(10)); // true
console.log(isOdd(7)); // true

// Parser simple con recursión mutua
function parseExpression(tokens, pos = 0) {
  let result = parseTerm(tokens, pos);

  while (pos < tokens.length && (tokens[pos] === '+' || tokens[pos] === '-')) {
    const operator = tokens[pos++];
    const term = parseTerm(tokens, pos);
    result = operator === '+' ? result + term : result - term;
  }

  return result;
}

function parseTerm(tokens, pos) {
  let result = parseFactor(tokens, pos);

  while (pos < tokens.length && (tokens[pos] === '*' || tokens[pos] === '/')) {
    const operator = tokens[pos++];
    const factor = parseFactor(tokens, pos);
    result = operator === '*' ? result * factor : result / factor;
  }

  return result;
}

function parseFactor(tokens, pos) {
  if (tokens[pos] === '(') {
    pos++; // consumir '('
    const result = parseExpression(tokens, pos);
    pos++; // consumir ')'
    return result;
  }

  return parseInt(tokens[pos++]);
}

Problemas clásicos de recursividad

// Torre de Hanoi
function hanoi(n, source, destination, auxiliary) {
  const moves = [];

  function solve(disks, src, dest, aux) {
    if (disks === 1) {
      // Caso base: mover un disco
      moves.push(`Move disk from ${src} to ${dest}`);
      return;
    }

    // Caso recursivo:
    // 1. Mover n-1 discos a auxiliar
    solve(disks - 1, src, aux, dest);

    // 2. Mover disco más grande a destino
    moves.push(`Move disk from ${src} to ${dest}`);

    // 3. Mover n-1 discos de auxiliar a destino
    solve(disks - 1, aux, dest, src);
  }

  solve(n, source, destination, auxiliary);
  return moves;
}

// Resolver Torre de Hanoi con 3 discos
const solution = hanoi(3, 'A', 'C', 'B');
solution.forEach((move, index) => {
  console.log(`${index + 1}. ${move}`);
});

// Generar todas las permutaciones de un array
function generatePermutations(arr) {
  // Caso base: array vacío o con un elemento
  if (arr.length <= 1) {
    return [arr];
  }

  const permutations = [];

  for (let i = 0; i < arr.length; i++) {
    const current = arr[i];
    const remaining = arr.slice(0, i).concat(arr.slice(i + 1));

    // Caso recursivo: generar permutaciones del resto
    const subPermutations = generatePermutations(remaining);

    // Agregar elemento actual al inicio de cada permutación
    for (const perm of subPermutations) {
      permutations.push([current, ...perm]);
    }
  }

  return permutations;
}

// Uso
console.log(generatePermutations([1, 2, 3]));
// Resultado: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]

Recursividad en el mundo real: DOM y React

// Renderizado recursivo de componentes anidados
function renderComponent(component) {
  // Caso base: componente sin hijos
  if (!component.children || component.children.length === 0) {
    return `<${component.tag}>${component.text || ''}</${component.tag}>`;
  }

  // Caso recursivo: renderizar hijos
  const childrenHTML = component.children
    .map((child) => renderComponent(child)) // Llamada recursiva
    .join('');

  return `<${component.tag}>${childrenHTML}</${component.tag}>`;
}

// Estructura de componente
const app = {
  tag: 'div',
  children: [
    {
      tag: 'header',
      children: [
        { tag: 'h1', text: 'Mi App' },
        { tag: 'nav', text: 'Navegación' },
      ],
    },
    {
      tag: 'main',
      children: [
        {
          tag: 'article',
          children: [
            { tag: 'h2', text: 'Título del artículo' },
            { tag: 'p', text: 'Contenido del artículo...' },
          ],
        },
      ],
    },
  ],
};

console.log(renderComponent(app));
// Genera HTML anidado completo

// Búsqueda recursiva en el DOM
function findElementsWithClass(element, className) {
  const results = [];

  // Caso base: verificar elemento actual
  if (element.classList && element.classList.contains(className)) {
    results.push(element);
  }

  // Caso recursivo: buscar en hijos
  for (const child of element.children) {
    results.push(...findElementsWithClass(child, className));
  }

  return results;
}

// Uso en el navegador
// const elements = findElementsWithClass(document.body, 'highlight');

Detección y prevención de errores comunes

// Error común: Stack Overflow (sin caso base o caso base incorrecto)
function infiniteRecursion(n) {
  console.log(n);
  return infiniteRecursion(n + 1); // ¡Nunca se detiene!
}

// ¡NO EJECUTAR! Causará stack overflow

// Versión segura con límite
function safeRecursion(n, limit = 1000) {
  if (n >= limit) {
    console.log('Límite alcanzado');
    return;
  }

  console.log(n);
  return safeRecursion(n + 1, limit);
}

// Detectar recursión infinita con contador
function fibonacciSafe(n, depth = 0) {
  const MAX_DEPTH = 10000; // Límite de seguridad

  if (depth > MAX_DEPTH) {
    throw new Error(`Recursión demasiado profunda. Depth: ${depth}`);
  }

  if (n <= 0) return 0;
  if (n === 1) return 1;

  return fibonacciSafe(n - 1, depth + 1) + fibonacciSafe(n - 2, depth + 1);
}

// Convertir recursión a iteración (cuando sea necesario)
function fibonacciIterative(n) {
  if (n <= 0) return 0;
  if (n === 1) return 1;

  let a = 0,
    b = 1;

  for (let i = 2; i <= n; i++) {
    [a, b] = [b, a + b]; // Intercambio destructuring
  }

  return b;
}

// Comparación de rendimiento
console.time('Recursivo');
console.log('fibonacci(35) recursivo:', fibonacci(35));
console.timeEnd('Recursivo');

console.time('Iterativo');
console.log('fibonacci(35) iterativo:', fibonacciIterative(35));
console.timeEnd('Iterativo');

Técnicas avanzadas de recursividad

// Tail Call Optimization (optimización de llamada en cola)
// Nota: No todos los engines de JS soportan TCO completamente

function factorialTailRecursive(n, accumulator = 1) {
  // La llamada recursiva es la última operación
  if (n <= 1) {
    return accumulator;
  }

  // Tail call - no hay operaciones después de la llamada
  return factorialTailRecursive(n - 1, n * accumulator);
}

// Trampolining - evitar stack overflow en engines sin TCO
function trampoline(fn) {
  while (typeof fn === 'function') {
    fn = fn();
  }
  return fn;
}

function factorialTrampoline(n, acc = 1) {
  if (n <= 1) {
    return acc;
  }

  // Retornar función en lugar de llamar directamente
  return () => factorialTrampoline(n - 1, n * acc);
}

// Uso con trampoline
const result = trampoline(() => factorialTrampoline(10000));
console.log(
  'Factorial de 10000 (primeros 50 dígitos):',
  result.toString().substring(0, 50) + '...'
);

// Recursión con continuaciones
function fibonacciCPS(n, cont = (x) => x) {
  if (n <= 0) return cont(0);
  if (n === 1) return cont(1);

  return fibonacciCPS(n - 1, (a) => fibonacciCPS(n - 2, (b) => cont(a + b)));
}

// Uso
fibonacciCPS(10, (result) => console.log('Fibonacci CPS:', result));

Herramientas de debugging para recursión

// Visualizador de llamadas recursivas
function debugRecursion(fn, name = 'function') {
  let depth = 0;

  return function (...args) {
    const indent = '  '.repeat(depth);
    console.log(`${indent}→ ${name}(${args.join(', ')})`);

    depth++;
    const result = fn.apply(this, args);
    depth--;

    console.log(`${indent}← ${name} returns: ${result}`);
    return result;
  };
}

// Wrapper para debugging
const debugFactorial = debugRecursion(factorial, 'factorial');
console.log('\n=== Debug Factorial ===');
debugFactorial(5);

// Profiler de recursión
class RecursionProfiler {
  constructor() {
    this.stats = new Map();
  }

  profile(fn, name) {
    return (...args) => {
      const key = `${name}(${args.join(',')})`;

      if (!this.stats.has(key)) {
        this.stats.set(key, { calls: 0, totalTime: 0 });
      }

      const stats = this.stats.get(key);
      stats.calls++;

      const start = performance.now();
      const result = fn(...args);
      const end = performance.now();

      stats.totalTime += end - start;

      return result;
    };
  }

  getReport() {
    const report = [];

    for (const [key, stats] of this.stats.entries()) {
      report.push({
        function: key,
        calls: stats.calls,
        totalTime: stats.totalTime.toFixed(2) + 'ms',
        avgTime: (stats.totalTime / stats.calls).toFixed(2) + 'ms',
      });
    }

    return report.sort((a, b) => b.calls - a.calls);
  }
}

// Uso del profiler
const profiler = new RecursionProfiler();
const profiledFib = profiler.profile(fibonacci, 'fibonacci');

profiledFib(10);
console.table(profiler.getReport());

Cuándo usar y cuándo evitar recursión

// ✅ BUENOS casos para recursión:

// 1. Estructuras naturalmente recursivas
const processNestedComments = (comments) => {
  return comments.map((comment) => ({
    ...comment,
    replies: comment.replies ? processNestedComments(comment.replies) : [],
  }));
};

// 2. Algoritmos divide y vencerás
const binarySearch = (arr, target, left = 0, right = arr.length - 1) => {
  if (left > right) return -1;

  const mid = Math.floor((left + right) / 2);

  if (arr[mid] === target) return mid;
  if (arr[mid] > target) return binarySearch(arr, target, left, mid - 1);
  return binarySearch(arr, target, mid + 1, right);
};

// 3. Backtracking
const solveMaze = (maze, x, y, path = []) => {
  // Casos base
  if (x < 0 || x >= maze.length || y < 0 || y >= maze[0].length) return false;
  if (maze[x][y] === 'wall' || maze[x][y] === 'visited') return false;
  if (maze[x][y] === 'exit') return [...path, [x, y]];

  // Marcar como visitado
  maze[x][y] = 'visited';

  // Probar todas las direcciones
  const directions = [
    [0, 1],
    [1, 0],
    [0, -1],
    [-1, 0],
  ];

  for (const [dx, dy] of directions) {
    const result = solveMaze(maze, x + dx, y + dy, [...path, [x, y]]);
    if (result) return result;
  }

  // Backtrack
  maze[x][y] = 'path';
  return false;
};

// ❌ MALOS casos para recursión:

// 1. Simple iteración (usar bucles)
// Malo:
function sumArrayRecursive(arr, index = 0) {
  if (index >= arr.length) return 0;
  return arr[index] + sumArrayRecursive(arr, index + 1);
}

// Bueno:
function sumArrayIterative(arr) {
  return arr.reduce((sum, num) => sum + num, 0);
}

// 2. Fibonacci sin memoización (exponencial)
// Malo para números grandes:
// fibonacci(40) // Muy lento

// Bueno:
const fibIterative = (n) => {
  if (n <= 1) return n;
  let a = 0,
    b = 1;
  for (let i = 2; i <= n; i++) {
    [a, b] = [b, a + b];
  }
  return b;
};

Conclusión

La recursividad es una herramienta poderosa que puede hacer que problemas complejos se vuelvan elegantemente simples, pero debe usarse con sabiduría. Como un bisturí en manos de un cirujano, puede ser increíblemente precisa y efectiva cuando se usa correctamente, pero peligrosa si se aplica incorrectamente.

Principios clave para recursividad efectiva:

  1. Siempre define un caso base claro - tu función debe saber cuándo parar
  2. Asegúrate de progresar hacia el caso base - cada llamada debe acercarte a la solución
  3. Considera la complejidad - algunos problemas recursivos son exponenciales sin optimización
  4. Usa memoización cuando tengas subproblemas repetidos
  5. Prefiere iteración para problemas simples de contador o acumulación

Cuándo brillan las soluciones recursivas:

  • Estructuras de datos anidadas (árboles, JSON, DOM)
  • Algoritmos divide y vencerás
  • Problemas con definición matemática recursiva
  • Backtracking y exploración de espacios de solución

Recuerda: La recursividad no es solo una técnica de programación - es una forma de pensar sobre problemas complejos. Te entrena para descomponer grandes desafíos en problemas más pequeños y manejables, una habilidad valiosa tanto en programación como en la vida.

La recursividad bien implementada es poesía en código - elegante, expresiva y poderosa. Pero como toda poesía, requiere práctica y comprensión profunda para dominarla.