Рекурсия

Рекурсия — вызов функции самой себя. Каждый рекурсивный вызов решает подзадачу исходной задачи до достижения базового случая, после чего результаты собираются обратно.

Зачем нужно

Рекурсия естественно решает задачи с рекурсивной структурой: обход деревьев, вложенные структуры данных, задачи «разделяй и властвуй». Многие алгоритмы (quicksort, mergesort, обход DOM) проще и понятнее в рекурсивной форме.

Где используется

Обход деревьев (DOM, файловая система, JSON), сортировки, поиск путей, фракталы, парсинг вложенных структур, глубокое клонирование, flatMap.

Предпосылки

Function Declaration, Замыкания (Closures)

Структура рекурсии

function recursive(input) {
  // 1. Базовый случай (условие выхода)
  if (условие_выхода) {
    return результат;
  }

  // 2. Рекурсивный случай (уменьшение задачи + вызов себя)
  return recursive(уменьшенный_input);
}

Базовые примеры

Факториал

function factorial(n) {
  // Базовый случай
  if (n <= 1) return 1;
  // Рекурсивный случай
  return n * factorial(n - 1);
}

console.log(factorial(5)); // 120
// 5 * 4 * 3 * 2 * 1 = 120

// Стек вызовов:
// factorial(5) → 5 * factorial(4)
//   factorial(4) → 4 * factorial(3)
//     factorial(3) → 3 * factorial(2)
//       factorial(2) → 2 * factorial(1)
//         factorial(1) → 1 (базовый случай)
//       → 2 * 1 = 2
//     → 3 * 2 = 6
//   → 4 * 6 = 24
// → 5 * 24 = 120

Числа Фибоначчи

// Наивная реализация — O(2^n)
function fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}

console.log(fib(10)); // 55

// Оптимизированная с мемоизацией — O(n)
function fibMemo(n, memo = {}) {
  if (n in memo) return memo[n];
  if (n <= 1) return n;
  memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
  return memo[n];
}

console.log(fibMemo(50)); // 12586269025 — мгновенно

Степень числа

function power(base, exp) {
  if (exp === 0) return 1;
  if (exp < 0) return 1 / power(base, -exp);
  return base * power(base, exp - 1);
}

console.log(power(2, 10)); // 1024
console.log(power(3, 0));  // 1

Обход деревьев

DOM-дерево

function walkDOM(node, callback) {
  callback(node);
  node = node.firstElementChild;
  while (node) {
    walkDOM(node, callback); // рекурсия для каждого дочернего
    node = node.nextElementSibling;
  }
}

// Найти все текстовые узлы
walkDOM(document.body, (node) => {
  if (node.nodeType === Node.TEXT_NODE) {
    console.log(node.textContent.trim());
  }
});

Произвольное дерево

const fileSystem = {
  name: 'root',
  children: [
    {
      name: 'src',
      children: [
        { name: 'index.js', children:  },
        { name: 'utils.js', children:  },
        {
          name: 'components',
          children: [
            { name: 'App.jsx', children:  },
            { name: 'Header.jsx', children:  },
          ]
        }
      ]
    },
    { name: 'package.json', children:  },
  ]
};

function findFile(node, fileName) {
  if (node.name === fileName) return node;
  for (const child of node.children) {
    const found = findFile(child, fileName);
    if (found) return found;
  }
  return null;
}

console.log(findFile(fileSystem, 'App.jsx'));
// { name: 'App.jsx', children:  }

Отображение дерева

function printTree(node, prefix = '', isLast = true) {
  const connector = isLast ? '└── ' : '├── ';
  console.log(prefix + connector + node.name);

  const newPrefix = prefix + (isLast ? '    ' : '│   ');
  const children = node.children || ;
  children.forEach((child, i) => {
    printTree(child, newPrefix, i === children.length - 1);
  });
}

printTree(fileSystem);
// └── root
//     ├── src
//     │   ├── index.js
//     │   ├── utils.js
//     │   └── components
//     │       ├── App.jsx
//     │       └── Header.jsx
//     └── package.json

Глубокое клонирование

function deepClone(obj) {
  // Базовые случаи
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj.getTime());
  if (obj instanceof RegExp) return new RegExp(obj);
  if (Array.isArray(obj)) return obj.map(item => deepClone(item));

  // Рекурсивный случай — объект
  const clone = {};
  for (const key of Object.keys(obj)) {
    clone[key] = deepClone(obj[key]);
  }
  return clone;
}

const original = {
  name: 'Иван',
  settings: { theme: 'dark', nested: { level: 3 } },
  tags: ['js', 'react']
};

const copy = deepClone(original);
copy.settings.theme = 'light';
console.log(original.settings.theme); // 'dark' — оригинал не изменился

Выравнивание массивов (flat)

function flatten(arr) {
  const result = ;
  for (const item of arr) {
    if (Array.isArray(item)) {
      result.push(...flatten(item)); // рекурсия для вложенных
    } else {
      result.push(item);
    }
  }
  return result;
}

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

Stack Overflow (переполнение стека)

// Каждый вызов добавляет фрейм в стек
function infiniteRecursion() {
  return infiniteRecursion; // нет базового случая!
}
// infiniteRecursion; // RangeError: Maximum call stack size exceeded

// Типичный лимит стека: ~10000-25000 вызовов
function countStack(n = 0) {
  try {
    return countStack(n + 1);
  } catch (e) {
    return n;
  }
}
console.log(countStack); // ~10000+ (зависит от движка)

Tail Call Optimization (TCO)

Хвостовая рекурсия — рекурсивный вызов является последней операцией:

// НЕ хвостовая рекурсия — после рекурсии ещё операция (умножение)
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1); // нужен результат для умножения
}

// Хвостовая рекурсия — рекурсия последняя операция
function factorialTail(n, acc = 1) {
  if (n <= 1) return acc;
  return factorialTail(n - 1, acc * n); // последняя операция
}

console.log(factorialTail(5)); // 120

Внимание: TCO поддерживается только в Safari. V8 (Chrome, Node.js) НЕ оптимизирует хвостовую рекурсию.

Trampoline — обход лимита стека

function trampoline(fn) {
  return function(...args) {
    let result = fn(...args);
    while (typeof result === 'function') {
      result = result;
    }
    return result;
  };
}

function factorialTramp(n, acc = 1) {
  if (n <= 1) return acc;
  return  => factorialTramp(n - 1, acc * n); // возвращает функцию, не вызов
}

const safeFact = trampoline(factorialTramp);
console.log(safeFact(100000)); // Infinity, но без stack overflow

Итеративная vs рекурсивная

// Рекурсивная сумма массива
function sumRecursive(arr) {
  if (arr.length === 0) return 0;
  return arr[0] + sumRecursive(arr.slice(1));
}

// Итеративная сумма — эффективнее
function sumIterative(arr) {
  let total = 0;
  for (const num of arr) total += num;
  return total;
}

// Используй рекурсию когда:
// - Структура данных рекурсивная (деревья, графы)
// - Задача естественно делится на подзадачи
// - Глубина вложенности небольшая

Частые ошибки

1. Забытый базовый случай

// Бесконечная рекурсия → Stack Overflow
function oops(n) {
  return oops(n - 1); // нет проверки n <= 0
}

2. Базовый случай не достигается

function countdown(n) {
  if (n === 0) return; // базовый случай
  console.log(n);
  countdown(n - 2); // если n нечётное, пропустит 0!
}
// countdown(5); // 5, 3, 1, -1, -3... → stack overflow

// Исправление:
function countdownFixed(n) {
  if (n <= 0) return; // <= вместо ===
  console.log(n);
  countdownFixed(n - 2);
}

3. Неэффективная рекурсия без мемоизации

// fib(40) — миллионы повторных вычислений
// Всегда используй мемоизацию для перекрывающихся подзадач

Практика

  1. Напиши рекурсивную функцию суммы вложенного массива: [1, [2, [3, 4]], 5] → 15
  2. Реализуй рекурсивный поиск по вложенному объекту (findValue)
  3. Напиши функцию deepFreeze(obj), рекурсивно замораживающую объект
  4. Реализуй бинарный поиск рекурсивно
  5. Напиши trampoline-версию подсчёта чисел Фибоначчи

Связанные темы

Ресурсы