273 lines
No EOL
14 KiB
Markdown
273 lines
No EOL
14 KiB
Markdown
# Рекурсия, стек
|
||
|
||
В коде функции могут вызывать другие функции для выполнения подзадач. Частный случай подвызова -- когда функция вызывает сама себя. Это называется *рекурсией*.
|
||
|
||
В этой главе мы рассмотрим, как рекурсия устроена изнутри, и как её можно использовать.
|
||
[cut]
|
||
|
||
|
||
## Реализация pow(x, n) через рекурсию
|
||
|
||
Чтобы возвести `x` в натуральную степень `n` -- можно умножить его на себя `n` раз в цикле:
|
||
|
||
```js
|
||
function pow(x, n) {
|
||
var result = x;
|
||
for(var i=1; i<n; i++) {
|
||
result *= x;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
```
|
||
|
||
А можно поступить проще.
|
||
|
||
Ведь <code>x<sup>n</sup> = x * x<sup>n-1</sup></code>, т.е. можно вынести один `x` из-под степени. Иначе говоря, значение функции `pow(x,n)` получается из `pow(x, n-1)` умножением на `x`.
|
||
|
||
Этот процесс можно продолжить. Например, вычислим `pow(2, 4)`:
|
||
|
||
```js
|
||
pow(2, 4) = 2 * pow(2, 3) = 2 * 2 * pow(2, 2) = 2 * 2 * 2 * pow(2, 1) = 2 * 2 * 2 * 2;
|
||
```
|
||
|
||
Процесс перехода от `n` к `n-1` останавливается на `n==1`, так как очевидно, что `pow(x,1) == x`.
|
||
|
||
Код для такого вычисления:
|
||
|
||
```js
|
||
//+ run
|
||
function pow(x, n) {
|
||
if (n != 1) { // пока n!=1 сводить вычисление pow(..n) к pow(..n-1)
|
||
return x * pow(x, n-1);
|
||
} else {
|
||
return x;
|
||
}
|
||
}
|
||
|
||
alert( pow(2, 3) ); // 8
|
||
```
|
||
|
||
Говорят, что "функция `pow` *рекурсивно вызывает сама себя*" при `n != 1`.
|
||
|
||
Значение, на котором рекурсия заканчивается называют *базисом рекурсии*. В примере выше базисом является `1`.
|
||
|
||
Общее количество вложенных вызовов называют *глубиной рекурсии*. В случае со степенью, всего будет `n` вызовов. Максимальная глубина рекурсии ограничена и составляет около `10000`, но это число зависит от конкретного интерпретатора JavaScript и может быть в 10 раз меньше.
|
||
|
||
**Рекурсию используют, когда вычисление функции можно свести к её более простому вызову, а его -- еще к более простому, и так далее, пока значение не станет очевидно.**
|
||
|
||
## Контекст выполнения, стек
|
||
|
||
Теперь мы посмотрим, как работают рекурсивные вызовы. Для этого мы рассмотрим, как вообще работают функции, что происходит при вызове.
|
||
|
||
**У каждого вызова функции есть свой "контекст выполнения" (execution context).**
|
||
|
||
Контекст выполнения -- это служебная информация, которая соответствует текущему запуску функции. Она включает в себя локальные переменные функции.
|
||
|
||
Например, для вызова:
|
||
|
||
```js
|
||
//+ run
|
||
function pow(x, n) {
|
||
if (n != 1) { // пока n!=1 сводить вычисление pow(..n) к pow(..n-1)
|
||
return x * pow(x, n-1);
|
||
} else {
|
||
return x;
|
||
}
|
||
}
|
||
|
||
*!*
|
||
alert( pow(2, 3) ); // (*)
|
||
*/!*
|
||
```
|
||
|
||
При запуске функции `pow` в строке `(*)` будет создан контекст выполнения, который будет хранить переменные `x = 2, n = 3`. Мы схематично обозначим его так:
|
||
|
||
<ul class="function-execution-context">
|
||
<li>Контекст: { x: 2, n: 3 }</li>
|
||
</ul>
|
||
|
||
Далее функция `pow` начинает выполняться. Вычисляется выражение `n != 1` -- оно равно `true`, ведь в текущем контексте `n=3`. Поэтому задействуется первая ветвь `if` :
|
||
|
||
```js
|
||
if (n != 1) { // пока n!=1 сводить вычисление pow(..n) к pow(..n-1)
|
||
*!*
|
||
return x * pow(x, n-1);
|
||
*/!*
|
||
} else {
|
||
return x;
|
||
}
|
||
```
|
||
|
||
Чтобы вычислить выражение `x * pow(x, n-1)`, требуется произвести запуск `pow` с новыми аргументами.
|
||
|
||
**При любом вложенном вызове JavaScript запоминает место, где он остановился в текущей функции в специальной внутренней структуре данных -- "стеке контекстов".**
|
||
|
||
Это как если бы мы куда-то ехали, но очень захотелось поесть. Можно остановиться у кафе, оставить машину, отойти, а потом, через некоторое время, вернуться к ней и продолжить дорогу.
|
||
|
||
Так и здесь -- мы запомним, где остановились в этой функции, пойдём выполним вложенный вызов, затем вернёмся и продолжим дорогу.
|
||
|
||
**После того, как текущий контекст выполнения сохранён в стеке контекстов, JavaScript приступает к выполнению вложенного вызова.**
|
||
|
||
В данном случае вызывается та же `pow`, однако, это абсолютно неважно. Для любых функций процесс одинаков.
|
||
|
||
**Создаётся новый контекст выполнения, и управление переходит в подвызов, а когда он завершён -- старый контекст достаётся из стека и выполнение внешней функции возобновляется.**
|
||
|
||
## Разбор примера
|
||
|
||
Разберём происходящее более подробно, начиная с вызова `(*)`:
|
||
|
||
```js
|
||
//+ run
|
||
function pow(x, n) {
|
||
if (n != 1) { // пока n!=1 сводить вычисление pow(..n) к pow(..n-1)
|
||
return x * pow(x, n-1);
|
||
} else {
|
||
return x;
|
||
}
|
||
}
|
||
|
||
*!*
|
||
alert( pow(2, 3) ); // (*)
|
||
*/!*
|
||
```
|
||
|
||
<dl>
|
||
<dt>`pow(2, 3)`</dt>
|
||
<dd>Запускается функция `pow`, с аргументами `x=2`, `n=3`. Эти переменные хранятся в контексте выполнения, схематично изображённом ниже:
|
||
|
||
<ul class="function-execution-context">
|
||
<li>Контекст: { x: 2, n: 3 }</li>
|
||
</ul>
|
||
Выполнение в этом контексте продолжается, пока не встретит вложенный вызов в строке 3.
|
||
</dd>
|
||
<dt>`pow(2, 2)`</dt>
|
||
<dd>В строке `3` происходит вложенный вызов `pow` с аргументами `x=2`, `n=2`. Для этой функции создаётся новый текущий контекст (выделен красным), а предыдущий сохраняется в "стеке":
|
||
|
||
<ul class="function-execution-context">
|
||
<li>Контекст: { x: 2, n: 3 }</li>
|
||
<li>Контекст: { x: 2, n: 2 }</li>
|
||
</ul>
|
||
|
||
</dd>
|
||
<dt>`pow(2, 1)`</dt>
|
||
<dd>Опять вложенный вызов в строке `3`, на этот раз -- с аргументами `x=2`, `n=1`. Создаётся новый текущий контекст, предыдущий добавляется в стек:
|
||
<ul class="function-execution-context">
|
||
<li>Контекст: { x: 2, n: 3 }</li>
|
||
<li>Контекст: { x: 2, n: 2 }</li>
|
||
<li>Контекст: { x: 2, n: 1 }</li>
|
||
</ul>
|
||
</dd>
|
||
<dt>Выход из `pow(2, 1)`.</dt>
|
||
<dd>При выполнении `pow(2, 1)`, в отличие от предыдущих запусков, выражение `n != 1` будет равно `false`, поэтому сработает вторая ветка `if..else`:
|
||
|
||
```js
|
||
function pow(x, n) {
|
||
if (n != 1) {
|
||
return x * pow(x, n-1);
|
||
} else {
|
||
*!*
|
||
return x; // первая степень числа равна самому числу
|
||
*/!*
|
||
}
|
||
}
|
||
```
|
||
|
||
Здесь вложенных вызовов нет, так что функция заканчивает свою работу, возвращая `2`. Текущий контекст больше не нужен и удаляется из памяти, из стека восстанавливается предыдущий:
|
||
|
||
<ul class="function-execution-context">
|
||
<li>Контекст: { x: 2, n: 3 }</li>
|
||
<li>Контекст: { x: 2, n: 2 }</li>
|
||
</ul>
|
||
Возобновляется обработка внешнего вызова `pow(2, 2)`.
|
||
</dd>
|
||
<dt>Выход из `pow(2, 2)`.</dt>
|
||
<dd>...И теперь уже `pow(2, 2)` может закончить свою работу, вернув `4`. Восстанавливается контекст предыдущего вызова:
|
||
<ul class="function-execution-context">
|
||
<li>Контекст: { x: 2, n: 3 }</li>
|
||
</ul>
|
||
Возобновляется обработка внешнего вызова `pow(2, 3)`.
|
||
</dd>
|
||
|
||
<dt>Выход из `pow(2, 3)`.</dt>
|
||
<dd>Самый внешний вызов заканчивает свою работу, его результат: `pow(2, 3) = 8`.</dd>
|
||
</dl>
|
||
|
||
Глубина рекурсии в данном случае составила: **3**.
|
||
|
||
Как видно из иллюстраций выше, глубина рекурсии равна максимальному числу контекстов, одновременно хранимых в стеке.
|
||
|
||
[smart]
|
||
В самом конце, как и в самом начале, выполнение попадает во внешний код, который находится вне любых функций.
|
||
|
||
Контекст, который соответствует самому внешнему коду, называют *"глобальный контекст"*. Естественно, он является начальной и конечной точкой любых вложенных подвызовов.
|
||
[/smart]
|
||
|
||
Обратим внимание на требования к памяти. Рекурсия приводит к хранению всех данных для неоконченных внешних вызовов в стеке, в данном случае это приводит к тому, что возведение в степень `n` хранит в памяти `n` различных контекстов.
|
||
|
||
Реализация степени через цикл гораздо более экономна:
|
||
|
||
```js
|
||
function pow(x, n) {
|
||
var result = x;
|
||
for(var i=1; i<n; i++) {
|
||
result *= x;
|
||
}
|
||
return result;
|
||
}
|
||
```
|
||
|
||
У такой функции `pow` будет один контекст, в котором будут последовательно меняться значения `i` и `result`.
|
||
|
||
**Любая рекурсия может быть переделана в цикл. Как правило, вариант с циклом будет эффективнее.**
|
||
|
||
...Но зачем тогда нужна рекурсия? Да просто затем, что рекурсивный код может быть гораздо проще и понятнее!
|
||
|
||
Переделка в цикл может быть нетривиальной, особенно когда в функции, в зависимости от условий, используются разные рекурсивные подвызовы.
|
||
|
||
В программировании мы в первую очередь стремимся сделать сложное простым, а повышенная производительность нужна... Лишь там, где она действительно нужна. Поэтому красивое рекурсивное решение во многих случаях лучше.
|
||
|
||
Недостатки и преимущества рекурсии:
|
||
|
||
[compare]
|
||
-Требования к памяти.
|
||
-Ограничена максимальная глубина стека.
|
||
+Краткость и простота кода.
|
||
[/compare]
|
||
|
||
|
||
## Итого
|
||
|
||
Рекурсия -- это когда функция вызывает сама себя, с другими аргументами.
|
||
|
||
Существуют много областей применения рекурсивных вызовов. Здесь мы посмотрели на один из них -- решение задачи путём сведения её к более простой (с меньшими аргументами), но также рекурсия используется для работы с "естественно рекурсивными" структурами данных, такими как HTML-документы, для "глубокого" копирования сложных объектов.
|
||
|
||
Есть и другие применения, с которыми мы встретимся по мере изучения JavaScript.
|
||
|
||
Здесь мы постарались рассмотреть происходящее достаточно подробно, однако, если пожелаете, допустимо временно забежать вперёд и открыть главу [](/debugging-chrome), с тем чтобы при помощи отладчика построчно пробежаться по коду и посмотреть стек.
|
||
|
||
|
||
|
||
|
||
[head]
|
||
<style>
|
||
.function-execution-context {
|
||
margin: 0;
|
||
padding: 0;
|
||
overflow: auto;
|
||
}
|
||
|
||
.function-execution-context li {
|
||
float: left;
|
||
clear: both;
|
||
border: 1px solid black;
|
||
font-family: "PT Mono", monospace;
|
||
padding: 3px 5px;
|
||
}
|
||
|
||
|
||
.function-execution-context li:last-child {
|
||
color: red;
|
||
}
|
||
</style>
|
||
[/head] |