Доброго времени суток, друзья!
Рекурсия — одна из основных концепций программирования. Выражаясь лаконично, рекурсия — это когда функция вызывает сама себя.
Простым примером рекурсии является вычисление факториала натурального числа:
function factorial(n) { return (n != 1) ? n * factorial(n - 1) : 1; }
Специально для визуалов:

Понимание рекурсии позволяет писать более качественный код.
Преимущества рекурсии
Преимуществами рекурсии является следующее:
- Меньше кода.
- Код выглядит чище.
- Меньше времени на написание кода и устранение ошибок.
- Меньше времени на запуск алгоритма (временная сложность алгоритма).
- Легче решать задачи при работе с древовидными структурами данных.
- Легче визуализировать алгоритмы.
Недостатки рекурсии
К недостаткам рекурсии можно отнести следующее:
- Может быть медленной.
- Может привести к переполнению стека вызовов.
- Потребляет больший объем памяти, чем цикл (если не используется оптимизация хвостовой рекурсии).
Зачем нам рекурсия?
На практике любой алгоритм можно реализовать с помощью цикла. Штука в том, чтобы знать, когда лучше использовать рекурсию — только в этом случае рекурсия будет лучшим выбором по сравнению с циклом.
И только тогда вы сможете оценить всю мощь рекурсии (например, при решении задачи про Ханойскую башню).
Примеры
Лучший способ понять рекурсию — рассмотреть парочку примеров.
Перебор объектов (проход по объектам, траверс объектов)
Как упоминалось ранее, рекурсия облегчает работу с древовидными структурами данных. Глубоко вложенных объект — это, по сути, древовидная структура данных.
Представим, что у нас имеется некий объект, представляющий элемент HTML DOM. Данный элемент имеет вложенные объекты. Каждый вложенный объект также может иметь дочерние элементы. Другими словами, каждый «ребенок» является элементом HTML DOM и может иметь «детей», так что в зависимости от количества потомков это может быть по-настоящему большой объект.
Наша задача — получить все объекты, независимо от «вложенности».
Мы будем искать свойство «style» (представленное атрибутами элемента) и менять значения свойств «border», «textColor» и «width» так, чтобы их можно было использовать в JavaScript.
Вот пример стилей объекта, которые необходимо изменить:
{ "border": { "color": "hotpink", "width": "2px" }, "textColor": "paleviolet", "width": "0.45" }
Для изменения цвета текста в HTML используется свойство «color», поэтому textColor должно быть преобразовано в color.
Допустим, что значение width — это проценты от области просмотра клиента (должно быть преобразовано в 45vw), а border должно быть преобразовано в { borderColor: ‘hotpink’, borderWidth: ‘2px’ }.
Наш «древовидный» объект выглядит так:
{ "type": "div", "style": {}, "children": [{ "type": "div", "style": { "backgroundColor": "black", "border": { "color": "hotpink", "width": "2px", "style": "dashed" }, "fontStyle": "italic", "padding": "20px 25px", "textColor": "white" }, "children": [{ "type": "button", "style": { "backgroundColor": "#fda512", "border": { "color": "red" }, "textColor": "#fff" } }, { "type": "label", "style": { "height": "0.04", "width": "0.04" }, "children": [{ "type": "label", "style": { "border": { "style": "solid", "width": "5px" }, "fontStyle": "italic" }, "children": [{ "type": "span", "style": { "backgroundColor": "#039392", "borderRadius": "10px", "height": "0.03", "outline": "none", "width": "0.783" } }] }] } ] }] }
Итак, мы имеем древовидную структуру данных, где каждый вложенный объект начинается со свойства «children».
Создадим функцию «transformStyleObject», принимающую стили объекта и исправляющую их — возвращающую новый объект, который можно использовать в JS и DOM:
function transformStyleObject(styleObj) { const result = {} const keys = Object.keys(styleObj) keys.forEach(key => { if (key === 'border') { const { color, width, style } = styleObj.border if (color) result.borderColor = color if (width) result.borderWidth = width if (style) result.borderStyle = style } else if (key === 'textColor') { result['color'] = styleObj.textColor } else if (key === 'width') { result['width'] = `${Number(styleObj.width) * 100}vw` } else if (key === 'height') { result['height'] = `${Number(styleObj.height) * 100}vh` } else { result[key] = styleObj[key] } }) return result } const result = transformStyleObject({ border: { width: '2px', style: 'dashed', }, height: '0.42', }) console.log(result) // result: { borderWidth: '2px', borderStyle: 'dashed', height: '42vh'}
Для прохода по объекту мы можем использовать стандартный цикл:
function transformAll({ type = '', style = {}, children = [] }) { const result = { type, style: transformStyleObject(style), children } if (Array.isArray(result.children)) { for (let i = 0; i < result.children.length; i++) { const child = result.children[i] child.style = transformStyleObject(child.style) if (Array.isArray(child.children)) { for (let i = 0; i < child.children.length; i++) { const childsChild = child.children[i] childsChild.style = transformStyleObject(childsChild.style) if (Array.isArray(childsChildren.children)) { for (let i = 0; i < childsChildren.children.length; i++) { const childsChildsChild = childsChild.children[i] // и т.д. } } } } } } return result }
Недостатки такого подхода очевидны и состоят в следующем:
- Код становится длинным
- Его становится трудно читать и поддерживать
- Он становится чувствительным к изменениям
- Его труднее тестировать
- Может возникнуть путаница с переменными
Используем рекурсию для решения этих проблем:
function transformAll({ type = '', style = {}, children = [] }) { const result = { type, style: transformStyleObject(style), children } if (Array.isArray(result.children)) { result.children = result.children.map(transformAll) } return result }
Результат выглядит так:
{ "type": "div", "style": {}, "children": [{ "type": "div", "style": { "backgroundColor": "black", "borderColor": "hotpink", "borderWidth": "2px", "borderStyle": "dashed", "fontStyle": "italic", "padding": "20px 25px", "color": "white" }, "children": [{ "type": "button", "style": { "backgroundColor": "#fda512", "borderColor": "red", "color": "#ffffff" }, "children": [] }, { "type": "label", "style": { "height": "4vh", "width": "4vw" }, "children": [{ "type": "label", "style": { "borderWidth": "5px", "borderStyle": "solid", "fontStyle": "italic" }, "children": [{ "type": "span", "style": { "backgroundColor": "#039392", "borderRadius": "10px", "height": "3vh", "outline": "none", "width": "78.3vw" }, "children": [] }] }] } ] }] }
Согласитесь, теперь код выглядит гораздо лучше! Вот как работает эта рекурсия:
- transformAll принимает объект, представляющий HTML DOM элемент
- Преобразует стили элемента
- Проверяет, имеются ли вложенные элементы по свойству «children»
- Если вложенные элементы имеются, функция «transformStyleObject» вызывается для каждого из них
- Рекурсия позволяет обработать каждый объект, независимо от уровня его вложенности
Работаем с файлами и папками
Лично я люблю писать функциональный код. И рекурсия отлично для этого подходит.
Напишем программу, которая будет проверять каждую директорию с именем "__test__" и определять, какие из модульных тестов не были использованы (по окончанию имени на ".test.js").
Каждая папка будет «модулем». Если в ней отсутствует директория с именем "__test__" или файл, имя которого заканчивается на ".test.js", значит, он не содержит тестов.
При обнаружении теста будет возвращаться объект, содержащий информацию о директории, например:
{ "../javascript-algorithms/src/algorithms/math/linked-list": { "name": "linked-list", "category": "algorithms", "subcategory": "math", "totalFiles": 0, "filesList": [] } }
Конечным результатом будет массив таких объектов, где каждый объект представляет собой папку, не содержащую модульных тестов.
Для этого отлично подойдет рекурсия.
Для примера возьмем этот репозиторий с JS-алгоритмами. Скачиваем его. Заходим в папку «src» и удаляем из парочки примеров модульные тесты.
Для импорта модулей используем Node.js.
Импортируем fs и определяем корневую директорию:
import fs from 'fs' const rootDir = '../javascript-algorithms/src'
Для определения того, что мы зашли в директорию, воспользуемся методом isDirectory из модуля fs. Я предпочитаю оборачивать его в функцию, чтобы не писать весь метод:
function isDirectory(filepath) { return fs.statSync(filepath).isDirectory() }
Cоздадим функцию «hasTest», принимающую массив строк, проходящую по нему и определяющую наличие тестов. При наличии теста возвращается true, иначе — false:
function hasTest(testDir) { for (let i = 0; i < testDir.length; i++) { const filename = testDir[i] if (filename.endsWith('.test.js')) { return true } } return false }
Теперь создадим главную функцию «findEmptyTests», аккумулирующую модули без тестов:
function findEmptyTests(basepath) { let emptyTests = {} if (isDirectory(basepath)) { const dir = fs.readdirSync(basepath) for (let i = 0; i < dir.length; i++) { const filename = dir[i] const filepath = `${basepath}/${filename}` if (isDirectory(filepath)) { if (filename === '__test__') { const testDir = fs.readdirSync(filepath) if (!hasTest(testDir)) { emptyTests[filepath] = createMissingTestsObject(basepath, testDir) } } else { emptyTests = { ...emptyTests, ...findEmptyTests(filepath) } } } } } return emptyTests }
В этой строчке мы наблюдаем, как функция вызывает сама себя:
emptyTests = { ...emptyTests, ...findEmptyTests(filepath) }
Это самая важная часть!
Мы начинаем с того, что передаем функции «findEmptyTests» начальную точку (путь файла).
Функция просматривает директории и записывает их имена в массив «dir».
Затем выполняется цикл, в ходе которого проверяется следующее:
- Если текущая директория называется "__test__", проверяется наличие файлов, имена которых заканчиваются на ".test.js". Если таких файлов нет, информация о модуле помещается в объект «emptyTests».
- Если текущая директория не __test__, заходим в нее и снова запускаем функцию и т.д.
Наконец, возвращается результат.
Функция «createMissingTestsObject» собирает информацию о пути и директории:
function createMissingTestsObject(str, dir) { const indexToSrc = str.indexOf('src') let category = str.substring(indexToSrc + 4) let subcategory = category.substring(category.indexOf('/') + 1) subcategory = subcategory.substring(0, subcategory.indexOf('/')) category = category.substring(0, category.indexOf('/')) return { name: str.substring(str.lastIndexOf('/') + 1), category, subcategory, totalFiles: dir.length, filesList: dir } }
Наша программа работает как ожидается — возвращает объект с директориями без тестов!
{ "../javascript-algorithms/src/algorithms/math/fourier-transform/__test__": { "name": "fourier-transform", "category": "algorithms", "subcategory": "math", "totalFiles": 1, "filesList": ["FourierTester.js"] }, "../javascript-algorithms/src/algorithms/sets/cartesian-product/__test__": { "name": "cartesian-product", "category": "algorithms", "subcategory": "sets", "totalFiles": 0, "filesList": [] }, "../javascript-algorithms/src/algorithms/sets/combination-sum/__test__": { "name": "combination-sum", "category": "algorithms", "subcategory": "sets", "totalFiles": 0, "filesList": [] } }
Благодарю за внимание. Надеюсь, вы нашли для себя что-то полезное.
ссылка на оригинал статьи https://habr.com/ru/post/498266/
Добавить комментарий