edubun
Русский

Рекурсия

Чтобы переложить Ханойскую башню из 64 дисков, нужно 18 446 744 073 709 551 615 ходов; рекурсивная функция описывает решение в четыре строки.

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

Некоторые задачи содержат свою уменьшенную копию. Факториал 5, записывается 5!, равен 5 × 4 × 3 × 2 × 1 = 120. Посмотри ещё раз: это 5 × (4 × 3 × 2 × 1), то есть 5 × 4!. В общем виде n! = n × (n − 1)!, так что для вычисления факториала можно вычислить меньший факториал и умножить. Функция, которая решает задачу, вызывая себя на меньшем варианте, называется рекурсивной.

function factorial(n) {
  if (n <= 1) return 1;          // базовый случай: здесь остановка
  return n * factorial(n - 1);   // рекурсивный случай: задача поменьше
}
console.log(factorial(5)); // 120

Каждой рекурсивной функции нужны две части. Базовый случай представляет собой входные данные, достаточно маленькие, чтобы ответить сразу, без нового вызова: здесь факториал 1 равен 1. Рекурсивный случай уменьшает задачу и снова вызывает функцию. Каждый вызов должен приближаться к базовому случаю. Если нет, например если по ошибке написать factorial(n + 1), вызовы никогда не закончатся.

Стек вызовов

Как компьютер следит за всеми этими незаконченными вызовами? Когда выполняется factorial(5), он доходит до 5 * factorial(4) и должен ждать ответа. Компьютер кладёт ждущий вызов в стек, стопку, из которой первым берут то, что положили последним. factorial(4) ждёт factorial(3), и так далее вниз до factorial(1), который сразу возвращает 1. Затем стек разворачивается: factorial(2) возвращает 2, factorial(3) возвращает 6, factorial(4) возвращает 24, а factorial(5) возвращает 120.

Размер стека ограничен. Рекурсия, которая никогда не доходит до базового случая, всё добавляет вызовы, пока не кончится место, и JavaScript останавливается с ошибкой; в Chrome и Node.js сообщение звучит так: "RangeError: Maximum call stack size exceeded". Из-за того же ограничения очень глубокую рекурсию, в сотни тысяч уровней, лучше писать циклом.

Ханойская башня

Ханойская башня является головоломкой, которую опубликовал французский математик Эдуард Люка в 1883 г. Есть три стержня, а на первом стопка дисков разного размера, самый большой внизу. Цель: переложить всю стопку на третий стержень. Перекладывать можно только по одному диску, только верхний диск стержня и никогда нельзя класть больший диск на меньший.

Если пошагово думать, куда идёт каждый диск, быстро запутаешься. Если думать рекурсивно, всё коротко. Чтобы переложить n дисков с A на C: сначала убери верхние n − 1 дисков с дороги, с A на B. Затем переложи самый большой диск с A на C. Затем переложи n − 1 дисков с B на него, на C. Переложить n − 1 дисков является той же головоломкой на размер меньше, а переложить один диск является базовым случаем. Для 3 дисков получается 7 ходов; лаборатория показывает стопку ждущих вызовов на каждом ходу.

function hanoi(n, from, to, via) {
  if (n === 0) return;
  hanoi(n - 1, from, via, to);
  console.log("disk " + n + ": " + from + " -> " + to);
  hanoi(n - 1, via, to, from);
}
hanoi(3, "A", "C", "B");  // 7 ходов
Загрузка

Перетащи, чтобы повернуть. Колёсико или щипок для масштаба.

Три диска, семь ходов. Стек вызовов справа перечисляет вызовы hanoi, которые ждут окончания этого хода. Попробуй 4 и 5 дисков.

Сколько ходов нужно для n дисков? Переложить n дисков стоит двух перекладываний n − 1 дисков плюс один ход: M(n) = 2M(n − 1) + 1, при M(1) = 1. Это даёт 1, 3, 7, 15, 31, так что M(n) = 2ⁿ − 1, и ни одно решение не обходится меньшим числом. Десять дисков требуют 1023 ходов. Шестьдесят четыре диска требуют 2⁶⁴ − 1 = 18 446 744 073 709 551 615 ходов; по одному ходу в секунду это около 585 миллиардов лет. Это рост O(2ⁿ) из прошлого урока.

Рекурсия, циклы и повторная работа

Всё, что написано рекурсией, можно написать циклом и собственным стеком, а простые случаи вроде факториала часто понятнее в виде цикла. Рекурсия хороша, когда сама задача устроена как дерево: деление списка в сортировке слиянием, обход папок внутри папок или поиск в лабиринте, о котором последний урок. Одна ловушка связана с повторной работой. Числа Фибоначчи 0, 1, 1, 2, 3, 5, 8, … подчиняются правилу fib(n) = fib(n − 1) + fib(n − 2). Если записать это прямо рекурсией, fib(10) = 55 делает 177 вызовов, fib(20) делает 21 891, а fib(30) делает 2 692 537, потому что одни и те же меньшие значения вычисляются снова и снова. Если запоминать уже найденные ответы или строить снизу вверх циклом, эти потери исчезают.

рекурсияФункция решает задачу, вызывая себя на меньшем варианте
базовый случайВходные данные, на которые отвечают сразу, и вызовы прекращаются
рекурсивный случайЧасть, которая уменьшает задачу и снова вызывает функцию
стек вызововСтопка вызовов, ждущих ответа: последним пришёл, первым ушёл
переполнение стекаСлишком много ждущих вызовов: RangeError в JavaScript
факториал n!n × (n − 1) × … × 1; 5! = 120
Ханойская башняЛюка, 1883 г.: 2ⁿ − 1 ходов для n дисков
  1. Чему равно 6! (факториал 6)?
  2. Сколько ходов нужно в Ханойской башне с 4 дисками?
  3. Сколько ходов с 10 дисками?
  4. Что делает базовый случай?
    • Сразу отвечает на маленькие входные данные и обрывает цепочку вызовов
    • Ускоряет функцию
    • Вызывает функцию с большими входными данными
    • Печатает результат
  5. Что будет, если рекурсивная функция никогда не дойдёт до базового случая?
    • Она вернёт 0
    • Стек вызовов переполнится, и программа остановится с ошибкой
    • Она будет тихо крутиться вечно без ошибки
    • JavaScript выберет базовый случай за тебя
  6. В каком порядке из стека берут элементы?
    • Последним пришёл, первым ушёл
    • Первым пришёл, первым ушёл
    • Сначала наименьший
    • Случайно
  7. Сколько вызовов factorial лежит в стеке, включая текущий, пока factorial(5) вычисляет factorial(1)?
  8. Какой первый шаг рекурсивного плана, чтобы переложить n дисков с A на C?
    • Переложить верхние n − 1 дисков с A на B
    • Переложить самый большой диск на C
    • Переложить самый маленький диск на C
    • Переложить все диски на B
  9. Кто опубликовал головоломку «Ханойская башня» в 1883 г.?
    • Леонард Эйлер
    • Эдуард Люка
    • Тони Хоар
    • Евклида
  10. Простая рекурсивная fib делает 177 вызовов для fib(10). Чему равно fib(10)?
  11. Почему простая рекурсивная fib медленная?
    • Она много раз вычисляет одни и те же меньшие значения
    • Рекурсия всегда медленная
    • Сложения дорогие
    • У неё нет базового случая
Главное
  • Рекурсивная функция вызывает себя на меньшем варианте задачи.
  • Ей нужен базовый случай, который прекращает вызовы.
  • Ждущие вызовы лежат в стеке вызовов: последним пришёл, первым ушёл.
  • Ханойская башня: 2ⁿ − 1 ходов; 64 диска по одному ходу в секунду заняли бы около 585 миллиардов лет.
  • Следи за повторной работой, как в простой рекурсивной функции Фибоначчи.
Источники курса (11)
  • MIT OpenCourseWare 6.006 Introduction to Algorithms, Spring 2020 (Demaine, Ku, Solomon)
  • lecture notes 1 Introduction, 3 Sorting, 9 Breadth-First Search, 10 Depth-First Search (ocw.mit.edu/courses/6-006-introduction-to-algorithms-spring-2020/pages/lecture-notes) and Fall 2011 lecture 13 BFS notes (ocw.mit.edu/courses/6-006-introduction-to-algorithms-fall-2011)
  • Cormen, Leiserson, Rivest, Stein, Introduction to Algorithms (MIT Press)
  • with the CLRS-based Khan Academy algorithms unit by Thomas Cormen and Devin Balkcom of Dartmouth (khanacademy.org/computing/computer-science/algorithms: binary search, selection sort analysis, merge sort, quicksort overview and analysis)
  • MacTutor History of Mathematics, Al-Khwarizmi (mathshistory.st-andrews.ac.uk/Biographies/Al-Khwarizmi: word algorithm from the Latin title Algoritmi de numero Indorum)
  • D. E. Joyce, Euclid's Elements Book VII Propositions 1 and 2 (mathcs.clarku.edu/~djoyce/elements/bookVII)
  • Wolfram MathWorld, Merge Sort (mathworld.wolfram.com/MergeSort.html: proposed by John von Neumann in 1945, after Knuth 1998 p. 159)
  • Stanford CS "Tony Hoare: Quicksort" (cs.stanford.edu/people/eroberts/courses/soco/projects/2008-09/tony-hoare/quicksort.html: devised while a student at Moscow State University for Russian-English machine translation with a tape dictionary, programmed in Algol 60 and published 1961; the year 1959 from the Wikipedia Quicksort article and its cited sources)
  • Bodleian Libraries, Ada Lovelace (blogs.bodleian.ox.ac.uk/adalovelace: 1843 paper, Note G Bernoulli numbers)
  • Fourmilab text of Menabrea's Sketch with Lovelace's notes (fourmilab.ch/babbage/sketch.html)
  • Seven Bridges of Konigsberg (Euler 1736) and Tower of Hanoi (Edouard Lucas, 1883) checked against the Wikipedia articles and their cited sources.

Проверено: 2026-10-17