DevSurge 💦

Рекурсия и рекурсивные функции в JavaScript

Cover Image for Рекурсия и рекурсивные функции в JavaScript
Mark Nelyubin
Mark Nelyubin

В статье я разберу стек вызовов, примеры рекурсии в жизни, расскажу про рекурсивные функции и структуры данных в JavaScript.

Рекурсия — это процесс или функция, которая вызывает сама себя
Article Image
Чтобы понять рекурсию, нужно сначала понять рекурсию

Рекурсия в природе

Рекурсивный паттерн встречается везде: в природе, математике, программировании, физике, лингвистике и культуре.

Article Image

Пример из природы — суккуленты. Photo by Martin Rancourt on Unsplash

Article Image

Кадр из фильма Кристофера Нолана "Начало". Классический пример бесконечной рекурсии — два поставленные друг напротив друга зеркала.

Article Image

Треугольник Серпинского — пример геометрического фрактала в форме бесконечной рекурсии

Рекурсивные функции в JavaScript

Рекурсия — функция, которая вызывает сама себя

Article Image

Разберем на примере. Задача — написать функцию, которая суммирует все числа от 1 до n. f(3) —> 1+2+3 = 6

Первый способ — пройтись циклом от 1 до n и суммировать значения в переменную result:

const sumTo = (n) => {
    let result = 0;
    
    for (let i=1; i<=n; i++){
        result += i;
    }
    
    return result;
}

Эту задачу можно решить с помощью рекурсии. Для этого нам нужны две вещи:

  1. функция, которая вызывает сама себя
  2. условие, при котором функция перестает вызывать сама себя (базовый случай)
const sumToN = (n) => {
  // базовый случай, когда функция перестает вызывать сама себя
  if (n === 1) return 1;
  // функция вызывает саму себя с новым аргументом
  return n + sumToN(n-1);
}

sumToN(3); // 6

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

Article Image

При первом вызове функция вернет n + f(n-1), то есть 3 + f(3-1). Этот return будет первой нижней книгой в стопке. Чтобы рассчитать его, нужно взывать функцию еще раз со значением 2.

Так повторяется до тех пор, пока мы не дойдем до базового случая, когда n=1. Теперь наша стопка начнет схлопываться, так как у нас есть значение для расчета предыдущего слоя. В итоге, не останется ни одного слоя, и функция вернет результат 6.

Два элемента рекурсивной функции — Different Input и Base Case

Рекурсия в программировании состоит из двух элементов—повторяющиеся операции с отличающимися вводными (Different Input) и базовый случай(Base Case).

Повторяющиеся операции в нашем примере — это когда функция раз за разом вызывала саму себя, но в качестве аргумента передавала разные значения — 3, 2 и 1.

Базовый случай — условие, при выполнении которого рекурсия заканчивается и функция больше не вызывает саму себя. В нашем примере это условие, когда n === 1. В этот момент функция возвращает значение 1 и не вызывает сама себя.

Если в рекурсивной функции забыть указать базовый случай — произойдет примерно то же самое что, и в случае бесконечного цикла: стек вызовов будет переполнен.

Article ImageArticle Image

Пример: расчет факториала с помощью рекурсии

Факториал — произведение всех натуральных чисел от 1 до n включительно.

5! = 1*2*3*4*5 = 120
0! = 0

Напишем итеративную функцию с использованием цикла:

const factorial = num => {
    let total = 1;
    for(let i = num; i > 1; i--){
        total *= i
    }
    return total;
}

Напишем рекурсивную функцию:

function factorial(n){
  if (n===0) return 0; // на случай, если вызовем factorial(0)
  if (n===1) return 1;
  return n*factorial(n-1);
}

factorial(5); // 120

Рекурсивные структуры данных

Структура данных — это набор значений, отношения между ними, а также функции и операции, которые можно применить к данным

Рекурсивная структура данных — структура, которая повторяет сама себя в своих частях.

Одна из рекурсивных структур данных — деревья. Дерево состоит из поддеревьев, которые тоже состоят из поддеревьев и так далее. Примеры древовидной структуры:

  • DOM-дерево, состоящее из HTML-элементов;
  • Файловая структура компьютера
  • Иерархическая структура компании
<!-- из HTML-разметки вроде такой-->

<html>
  <head>
    <meta charset="utf-8">
    <title>Medium</title>
  </head>
  <body>
    <header>
      <ul class="menu">
        <li>Main</li>
        <li>Articles</li>
        <li>Advertise</li>
      </ul>
    </header>
    <article id="12">
      <h1>Рекурсия и рекурсивные функции в JavaScript</h1>
      <p>Рекурсивная структура данных - структура, которая повторяет сама себя в своих частях.</p>
    </article>
  </body>
</html>

<!-- ...получится DOM дерево:

                          html
                    ______|_______
                    |             |
                  body           head
                ___|____       ___|___
                |       |     |       |
             header  article meta   title
                |     |   |           |
                ul    h1  p      "DevBlast"
...и так далее
-->

Работать с деревьями итеративным способом сложно. Например, хотим получить названия всех элементов на странице. Можно пройтись циклом по первому или второму уровню, но дальше придется как-то определять, где мы прошлись, а где еще нет, куда идти дальше.

В случае с рекурсией можем придумать алгоритм для обхода дерева:

  • базовый случай: если у узла дерева нет дочерних узлов или мы обошли их все — возвращаем название этого элемента;
  • повторяемые операции с различными вводными: рекурсивно обходим каждый дочерний элемент, собирая его название.

Плюсы и минусы рекурсии

У итеративной и рекурсивной реализации есть свои плюсы и минусы:

  • Рекурсивная функция, как правило, быстрее, но её сложнее отлаживать и читать;
  • Рекурсия может приводить к переполнению стека. Например, если мы вызовем factorial(10000) в консоли браузера, увидим ошибку о переполнении стека, тогда как в итеративной функции проблем не будет;
  • Результат выполнения рекурсивной функции проще закэшировать, чтобы ускорить выполнение. Это сложнее сделать в итеративной функции.
  • С помощью рекурсии намного проще работать с рекурсивными структурами данных.

Итог

  • Рекурсия — это процесс, который воспроизводит сам себя. В JS рекурсия — когда функция вызывает сама себя.
  • При вызове функция добавляется в стек (стопку), а при выполнении операций или возврате расчитанного значения в return — исчезает из стека.
  • Рекурсивная функция вызывает сама себя несколько раз, пока на одном из витков не дойдет до расчитанного значения (базового случая), поэтому стек вызовов сначала наполняется, а потом освобождается.
  • В рекурсивной функции обязательно присутствует повторяющаяся операция с новым вводными (different input), и базовый случай (base case).
  • Рекурсивная функция помогает работать с рекурсивными структурами данных, такими как деревья (например, DOM-дерево).

Другие материалы

Cover Image for TypeScript Utility Types — вспомогательные типы и области их применения

TypeScript Utility Types — вспомогательные типы и области их применения

что такое Utility Types в TypeScript, расскажу про основные вспомогательные типы, и покажу, как применять их на реальных проектах.

Mark Nelyubin
Mark Nelyubin
Cover Image for Принципы SOLID с примерами на JS и Vue

Принципы SOLID с примерами на JS и Vue

Расскажу про принципы SOLID с актуальными примерами на JavaScript, Vue, React.

Mark Nelyubin
Mark Nelyubin