Ты работаешь с Notion или файловой системой: папка содержит подпапки, те — ещё подпапки, и так до любой глубины. Как обойти всё дерево, не зная заранее его глубину? Цикл for здесь бесполезен — он плоский.
Рекурсия — функция вызывает саму себя. Это идеальный инструмент для структур, которые по природе своей вложены: деревья, вложенные категории, DOM, JSON произвольной глубины.
Каждая рекурсивная функция должна иметь:
1. Базовый случай (base case) — условие остановки. Без него — бесконечный цикл.
2. Рекурсивный случай — вызов себя с упрощённым аргументом.
function factorial(n) {
if (n <= 1) return 1 // базовый случай — СТОП
return n * factorial(n - 1) // рекурсивный случай
}
// Как это работает:
// 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))) ← базовый случай
// = 120Это главный практический сценарий:
// Вложенные категории (как в интернет-магазине)
const categories = [
{ id: 1, name: 'Электроника', children: [
{ id: 2, name: 'Ноутбуки', children: [] },
{ id: 3, name: 'Смартфоны', children: [
{ id: 4, name: 'Apple', children: [] },
]},
]},
]
// Плоский обход дерева — цикл не знает глубины, рекурсия — знает
function flatten(nodes, depth = 0) {
return nodes.reduce((acc, node) => {
acc.push({ ...node, depth, children: undefined })
if (node.children?.length) {
acc.push(...flatten(node.children, depth + 1))
}
return acc
}, [])
}
flatten(categories).forEach(c =>
console.log(' '.repeat(c.depth) + c.name)
)
// 'Электроника'
// ' Ноутбуки'
// ' Смартфоны'
// ' Apple'Наивная рекурсия для Фибоначчи — экспоненциальное время O(2^n):
// Медленно: fib(40) делает ~300 миллионов вызовов!
function fib(n) {
if (n <= 1) return n
return fib(n - 1) + fib(n - 2) // одни и те же вычисления повторяются
}
// Мемоизация — кешируем уже вычисленные значения: 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 — мгновенноКаждый рекурсивный вызов добавляет кадр в стек. При глубине > ~10000 — переполнение:
// Опасно для больших n:
function sumTo(n) {
if (n === 0) return 0
return n + sumTo(n - 1) // при n = 100000 — Stack Overflow!
}
// Итеративный вариант — без ограничений стека:
function sumToIter(n) {
let result = 0
for (let i = 1; i <= n; i++) result += i
return result
}1. Нет базового случая — бесконечная рекурсия:
// Сломано — Stack Overflow:
function countdown(n) {
console.log(n)
countdown(n - 1) // нет условия остановки!
}
// Исправлено:
function countdown(n) {
if (n <= 0) return // базовый случай
console.log(n)
countdown(n - 1)
}2. Базовый случай недостижим:
// Сломано — n всегда увеличивается, никогда не достигнет 1:
function bad(n) {
if (n === 1) return // базовый случай есть, но...
return bad(n + 1) // n растёт! Stack Overflow при любом n > 1
}
// Исправлено — рекурсивный вызов должен УПРОЩАТЬ задачу:
function good(n) {
if (n <= 1) return
return good(n - 1) // n уменьшается → придём к базовому случаю
}3. Используют рекурсию там, где нужна итерация:
// Избыточно — простой цикл лучше:
function sumArray(arr, i = 0) {
if (i === arr.length) return 0
return arr[i] + sumArray(arr, i + 1)
}
// Проще и быстрее:
const sum = arr.reduce((a, b) => a + b, 0)fs.readdirSync рекурсивно)Рекурсивный обход дерева категорий и поиск по вложенной структуре
// Дерево категорий (как в Ozon или Wildberries)
const catalog = [
{
id: 1, name: 'Электроника',
children: [
{ id: 2, name: 'Ноутбуки', children: [
{ id: 3, name: 'Apple MacBook', children: [] },
{ id: 4, name: 'Lenovo ThinkPad', children: [] },
]},
{ id: 5, name: 'Смартфоны', children: [
{ id: 6, name: 'iPhone', children: [] },
{ id: 7, name: 'Samsung Galaxy', children: [] },
]},
],
},
{
id: 8, name: 'Одежда',
children: [
{ id: 9, name: 'Мужская', children: [
{ id: 10, name: 'Футболки', children: [] },
]},
],
},
]
// 1. Плоский список с уровнями вложенности
function flattenTree(nodes, level = 0) {
return nodes.reduce((acc, node) => {
acc.push({ id: node.id, name: node.name, level })
if (node.children?.length) {
acc.push(...flattenTree(node.children, level + 1))
}
return acc
}, [])
}
console.log('=== Дерево категорий ===')
flattenTree(catalog).forEach(c =>
console.log(' '.repeat(c.level) + c.name)
)
// 2. Поиск категории по id (любая глубина)
function findById(nodes, id) {
for (const node of nodes) {
if (node.id === id) return node
const found = findById(node.children, id)
if (found) return found
}
return null
}
const iphone = findById(catalog, 6)
console.log('\nНашли:', iphone?.name) // 'iPhone'
const missing = findById(catalog, 999)
console.log('Не найдено:', missing) // null
// 3. Подсчёт листьев (категорий без детей)
function countLeaves(nodes) {
return nodes.reduce((count, node) => {
if (!node.children?.length) return count + 1
return count + countLeaves(node.children)
}, 0)
}
console.log('\nКонечных категорий:', countLeaves(catalog)) // 7Ты работаешь с Notion или файловой системой: папка содержит подпапки, те — ещё подпапки, и так до любой глубины. Как обойти всё дерево, не зная заранее его глубину? Цикл for здесь бесполезен — он плоский.
Рекурсия — функция вызывает саму себя. Это идеальный инструмент для структур, которые по природе своей вложены: деревья, вложенные категории, DOM, JSON произвольной глубины.
Каждая рекурсивная функция должна иметь:
1. Базовый случай (base case) — условие остановки. Без него — бесконечный цикл.
2. Рекурсивный случай — вызов себя с упрощённым аргументом.
function factorial(n) {
if (n <= 1) return 1 // базовый случай — СТОП
return n * factorial(n - 1) // рекурсивный случай
}
// Как это работает:
// 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))) ← базовый случай
// = 120Это главный практический сценарий:
// Вложенные категории (как в интернет-магазине)
const categories = [
{ id: 1, name: 'Электроника', children: [
{ id: 2, name: 'Ноутбуки', children: [] },
{ id: 3, name: 'Смартфоны', children: [
{ id: 4, name: 'Apple', children: [] },
]},
]},
]
// Плоский обход дерева — цикл не знает глубины, рекурсия — знает
function flatten(nodes, depth = 0) {
return nodes.reduce((acc, node) => {
acc.push({ ...node, depth, children: undefined })
if (node.children?.length) {
acc.push(...flatten(node.children, depth + 1))
}
return acc
}, [])
}
flatten(categories).forEach(c =>
console.log(' '.repeat(c.depth) + c.name)
)
// 'Электроника'
// ' Ноутбуки'
// ' Смартфоны'
// ' Apple'Наивная рекурсия для Фибоначчи — экспоненциальное время O(2^n):
// Медленно: fib(40) делает ~300 миллионов вызовов!
function fib(n) {
if (n <= 1) return n
return fib(n - 1) + fib(n - 2) // одни и те же вычисления повторяются
}
// Мемоизация — кешируем уже вычисленные значения: 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 — мгновенноКаждый рекурсивный вызов добавляет кадр в стек. При глубине > ~10000 — переполнение:
// Опасно для больших n:
function sumTo(n) {
if (n === 0) return 0
return n + sumTo(n - 1) // при n = 100000 — Stack Overflow!
}
// Итеративный вариант — без ограничений стека:
function sumToIter(n) {
let result = 0
for (let i = 1; i <= n; i++) result += i
return result
}1. Нет базового случая — бесконечная рекурсия:
// Сломано — Stack Overflow:
function countdown(n) {
console.log(n)
countdown(n - 1) // нет условия остановки!
}
// Исправлено:
function countdown(n) {
if (n <= 0) return // базовый случай
console.log(n)
countdown(n - 1)
}2. Базовый случай недостижим:
// Сломано — n всегда увеличивается, никогда не достигнет 1:
function bad(n) {
if (n === 1) return // базовый случай есть, но...
return bad(n + 1) // n растёт! Stack Overflow при любом n > 1
}
// Исправлено — рекурсивный вызов должен УПРОЩАТЬ задачу:
function good(n) {
if (n <= 1) return
return good(n - 1) // n уменьшается → придём к базовому случаю
}3. Используют рекурсию там, где нужна итерация:
// Избыточно — простой цикл лучше:
function sumArray(arr, i = 0) {
if (i === arr.length) return 0
return arr[i] + sumArray(arr, i + 1)
}
// Проще и быстрее:
const sum = arr.reduce((a, b) => a + b, 0)fs.readdirSync рекурсивно)Рекурсивный обход дерева категорий и поиск по вложенной структуре
// Дерево категорий (как в Ozon или Wildberries)
const catalog = [
{
id: 1, name: 'Электроника',
children: [
{ id: 2, name: 'Ноутбуки', children: [
{ id: 3, name: 'Apple MacBook', children: [] },
{ id: 4, name: 'Lenovo ThinkPad', children: [] },
]},
{ id: 5, name: 'Смартфоны', children: [
{ id: 6, name: 'iPhone', children: [] },
{ id: 7, name: 'Samsung Galaxy', children: [] },
]},
],
},
{
id: 8, name: 'Одежда',
children: [
{ id: 9, name: 'Мужская', children: [
{ id: 10, name: 'Футболки', children: [] },
]},
],
},
]
// 1. Плоский список с уровнями вложенности
function flattenTree(nodes, level = 0) {
return nodes.reduce((acc, node) => {
acc.push({ id: node.id, name: node.name, level })
if (node.children?.length) {
acc.push(...flattenTree(node.children, level + 1))
}
return acc
}, [])
}
console.log('=== Дерево категорий ===')
flattenTree(catalog).forEach(c =>
console.log(' '.repeat(c.level) + c.name)
)
// 2. Поиск категории по id (любая глубина)
function findById(nodes, id) {
for (const node of nodes) {
if (node.id === id) return node
const found = findById(node.children, id)
if (found) return found
}
return null
}
const iphone = findById(catalog, 6)
console.log('\nНашли:', iphone?.name) // 'iPhone'
const missing = findById(catalog, 999)
console.log('Не найдено:', missing) // null
// 3. Подсчёт листьев (категорий без детей)
function countLeaves(nodes) {
return nodes.reduce((count, node) => {
if (!node.children?.length) return count + 1
return count + countLeaves(node.children)
}, 0)
}
console.log('\nКонечных категорий:', countLeaves(catalog)) // 7Ты разрабатываешь бэкенд для Notion-подобного приложения. Страницы имеют вложенную структуру: каждая страница может содержать дочерние страницы произвольной глубины. Реализуй: 1. `countPages(pages)` — рекурсивно считает общее количество страниц (включая вложенные) 2. `findPage(pages, id)` — рекурсивно ищет страницу по id, возвращает её или null 3. `getPath(pages, id)` — возвращает массив названий от корня до найденной страницы
countPages: count + 1 + countPages(page.children). findPage: findPage(page.children, id). getPath: getPath(page.children, id, current). Для getPath — передавай накапливаемый массив current в рекурсию.