This commit is contained in:
Ilya Kantor 2014-11-16 01:40:20 +03:00
parent 962caebbb7
commit 87bf53d076
1825 changed files with 94929 additions and 0 deletions

View file

@ -0,0 +1,45 @@
Решение **с использованием цикла**:
```js
//+ run
function sumTo(n) {
var sum = 0;
for(var i=1; i<=n; i++) {
sum += i;
}
return sum;
}
alert( sumTo(100) );
```
Решение через **рекурсию**:
```js
//+ run
function sumTo(n) {
if (n == 1) return 1;
return n + sumTo(n-1);
}
alert( sumTo(100) );
```
Решение **по формуле**: `sumTo(n) = n*(n+1)/2`:
```js
//+ run
function sumTo(n) {
return n*(n+1)/2;
}
alert( sumTo(100) );
```
P.S. Надо ли говорить, что решение по формуле работает быстрее всех? Это очевидно. Оно использует всего три операции для любого `n`, а цикл и рекурсия требуют как минимум `n` операций сложения.
Вариант с циклом -- второй по скорости. Он быстрее рекурсии, так как операций сложения столько же, но нет дополнительных вычислительных затрат на организацию вложенных вызовов.
Рекурсия в данном случае работает медленнее всех.
P.P.S. Существует ограничение глубины вложенных вызовов, поэтому рекурсивный вызов `sumTo(100000)` выдаст ошибку.

View file

@ -0,0 +1,33 @@
# Вычислить сумму чисел до данного
[importance 5]
Напишите функцию `sumTo(n)`, которая для данного `n` вычисляет сумму чисел от 1 до `n`, например:
```js
sumTo(1) = 1
sumTo(2) = 2 + 1 = 3
sumTo(3) = 3 + 2 + 1 = 6
sumTo(4) = 4 + 3 + 2 + 1 = 10
...
sumTo(100) = 100 + 99 + ... + 2 + 1 = 5050
```
Сделайте три варианта решения:
<ol>
<li>С использованием цикла.</li>
<li>Через рекурсию, т.к. `sumTo(n) = n + sumTo(n-1)` для `n > 1`.</li>
<li>С использованием формулы для суммы [арифметической прогрессии](http://ru.wikipedia.org/wiki/%D0%90%D1%80%D0%B8%D1%84%D0%BC%D0%B5%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B5%D1%81%D1%81%D0%B8%D1%8F).</li>
</ol>
Пример работы вашей функции:
```js
function sumTo(n) { /*... ваш код ... */ }
alert( sumTo(100) ); // 5050
```
**Какой вариант решения самый быстрый? Самый медленный? Почему?**
**Можно ли при помощи рекурсии посчитать `sumTo(100000)`? Если нет, то почему?**

View file

@ -0,0 +1,25 @@
По свойствам факториала, как описано в условии, `n!` можно записать как `n * (n-1)!`.
То есть, результат функции для `n` можно получить как `n`, умноженное на результат функции для `n-1`, и так далее до `1!`:
```js
//+ run
function factorial(n) {
return (n!=1) ? n*factorial(n-1) : 1;
}
alert( factorial(5) ); // 120
```
Базисом рекурсии является значение `1`. А можно было бы сделать базисом и `0`. Тогда код станет чуть короче:
```js
//+ run
function factorial(n) {
return n ? n*factorial(n-1) : 1;
}
alert( factorial(5) ); // 120
```
В этом случае вызов `factorial(1)` сведется к `1*factorial(0)`, будет дополнительный шаг рекурсии.

View file

@ -0,0 +1,29 @@
# Вычислить факториал
[importance 4]
*Факториа́л числа* -- это число, умноженное на "себя минус один", затем на "себя минус два" и так далее, до единицы. Обозначается `n!`
Определение факториала можно записать как:
```js
n! = n*(n-1)*(n-2)*...*1
```
Примеры значений для разных `n`:
```js
1! = 1
2! = 2*1 = 2
3! = 3*2*1 = 6
4! = 4*3*2*1 = 24
5! = 5*4*3*2*1 = 120
```
Задача -- написать функцию `factorial(n)`, которая возвращает факториал числа `n!`, используя рекурсивный вызов.
```js
alert( factorial(5) ); // 120
```
Подсказка: обратите внимание, что `n!` можно записать как `n * (n-1)!`, например `3! = 3*2! = 3*2*1! = 6`

View file

@ -0,0 +1,94 @@
# Вычисление рекурсией (медленное)
Решение по формуле, используя рекурсию:
```js
//+ run
function fib(n) {
return n <= 1 ? n : fib(n-1) + fib(n-2);
}
alert( fib(3) ); // 2
alert( fib(7) ); // 13
// fib(77); // не запускаем, подвесит браузер
```
При больших значениях `n` оно будет работать очень медленно. Например, `fib(77)` уже будет вычисляться очень долго.
Это потому, что функция порождает обширное дерево вложенных вызовов. При этом ряд значений вычисляются много раз. Например, посмотрим на отрывок вычислений:
```js
...
fib(5) = fib(4) + fib(3)
fib(4) = fib(3) + fib(2)
...
```
Здесь видно, что значение `fib(3)` нужно одновременно и для `fib(5)` и для `fib(4)`. В коде оно будет вычислено два раза, совершенно независимо.
Можно это оптимизировать, запоминая уже вычисленные значения, получится гораздо быстрее. Альтернативный вариант -- вообще отказаться от рекурсии, а вместо этого в цикле начать с первых значений `1`, `2`, затем из них получить `fib(3)`, далее `fib(4)`, затем `fib(5)` и так далее, до нужного значения.
Это решение будет наиболее эффективным. Попробуйте его написать.
# Алгоритм вычисления в цикле
Будем идти по формуле слева-направо:
```js
var a = 1, b = 1; // начальные значения
var c = a + b; // 2
/* переменные на начальном шаге:
a b c
1, 1, 2
*/
```
Теперь следующий шаг, присвоим `a` и `b` текущие 2 числа и получим новое следующее в `c`:
```js
a = b, b = c;
c = a + b;
/* стало так (еще число):
a b c
1, 1, 2, 3
*/
```
Следующий шаг даст нам еще одно число последовательности:
```js
a = b, b = c;
c = a + b;
/* стало так (еще число):
a b c
1, 1, 2, 3, 5
*/
```
Повторять в цикле до тех пор, пока не получим нужное значение. Это гораздо быстрее, чем рекурсия, хотя бы потому что ни одно из чисел не вычисляется дважды.
P.S. Этот подход к вычислению называется [динамическое программирование снизу-вверх](http://ru.wikipedia.org/wiki/%D0%94%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5).
# Код для вычисления в цикле
```js
//+ run
function fib(n) {
var a = 1, b = 1;
for (var i = 3; i <= n; i++) {
var c = a + b;
a = b;
b = c;
}
return b;
}
alert( fib(3) ); // 2
alert( fib(7) ); // 13
alert( fib(77)); // 5527939700884757
```
Цикл здесь начинается с `i=3`, так как первое и второе числа Фибоначчи заранее записаны в переменные `a=1`, `b=1`.

View file

@ -0,0 +1,23 @@
# Числа Фибоначчи
[importance 5]
Последовательность [чисел Фибоначчи](http://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D0%BB%D0%B0_%D0%A4%D0%B8%D0%B1%D0%BE%D0%BD%D0%B0%D1%87%D1%87%D0%B8) имеет формулу <code>F<sub>n</sub> = F<sub>n-1</sub> + F<sub>n-2</sub></code>. То есть, следующее число получается как сумма двух предыдущих.
Первые два числа равны `1`, затем `2(1+1)`, затем `3(1+2)`, `5(2+3)` и так далее: `1, 1, 2, 3, 5, 8, 13, 21...`.
Числа Фибоначчи тесно связаны с [золотым сечением](http://ru.wikipedia.org/wiki/%D0%97%D0%BE%D0%BB%D0%BE%D1%82%D0%BE%D0%B5_%D1%81%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D0%B5) и множеством природных явлений вокруг нас.
Напишите функцию `fib(n)`, которая возвращает `n-е` число Фибоначчи. Пример работы:
```js
function fib(n) { /* ваш код */ }
alert( fib(3) ); // 2
alert( fib(7) ); // 13
alert( fib(77)); // 5527939700884757
```
**Все запуски функций из примера выше должны срабатывать быстро.**

View file

@ -0,0 +1,273 @@
# Рекурсия, стек
В коде функции могут вызывать другие функции для выполнения подзадач. Частный случай подвызова -- когда функция вызывает сама себя. Это называется *рекурсией*.
В этой главе мы рассмотрим, как рекурсия устроена изнутри, и как её можно использовать.
[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]