Рекурсия
Чтобы переложить Ханойскую башню из 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 ходовПеретащи, чтобы повернуть. Колёсико или щипок для масштаба.
Сколько ходов нужно для 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 дисков |
- Чему равно 6! (факториал 6)?
- Сколько ходов нужно в Ханойской башне с 4 дисками?
- Сколько ходов с 10 дисками?
- Что делает базовый случай?
- Сразу отвечает на маленькие входные данные и обрывает цепочку вызовов
- Ускоряет функцию
- Вызывает функцию с большими входными данными
- Печатает результат
- Что будет, если рекурсивная функция никогда не дойдёт до базового случая?
- Она вернёт 0
- Стек вызовов переполнится, и программа остановится с ошибкой
- Она будет тихо крутиться вечно без ошибки
- JavaScript выберет базовый случай за тебя
- В каком порядке из стека берут элементы?
- Последним пришёл, первым ушёл
- Первым пришёл, первым ушёл
- Сначала наименьший
- Случайно
- Сколько вызовов factorial лежит в стеке, включая текущий, пока factorial(5) вычисляет factorial(1)?
- Какой первый шаг рекурсивного плана, чтобы переложить n дисков с A на C?
- Переложить верхние n − 1 дисков с A на B
- Переложить самый большой диск на C
- Переложить самый маленький диск на C
- Переложить все диски на B
- Кто опубликовал головоломку «Ханойская башня» в 1883 г.?
- Леонард Эйлер
- Эдуард Люка
- Тони Хоар
- Евклида
- Простая рекурсивная fib делает 177 вызовов для fib(10). Чему равно fib(10)?
- Почему простая рекурсивная 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