This commit is contained in:
Ilya Kantor 2016-07-13 10:39:15 +03:00
parent 75cdcb406c
commit 4bdc4cd5aa
43 changed files with 1746 additions and 171 deletions

View file

@ -0,0 +1,42 @@
Решение **с использованием цикла**:
```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,34 @@
importance: 5
---
# Вычислить сумму чисел до данного
Напишите функцию `sumTo(n)`, которая для данного `n` вычисляет сумму чисел от 1 до `n`, например:
```js no-beautify
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
```
Сделайте три варианта решения:
1. С использованием цикла.
2. Через рекурсию, т.к. `sumTo(n) = n + sumTo(n-1)` для `n > 1`.
3. С использованием формулы для суммы [арифметической прогрессии](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).
Пример работы вашей функции:
```js
function sumTo(n) { /*... ваш код ... */ }
alert( sumTo(100) ); // 5050
```
**Какой вариант решения самый быстрый? Самый медленный? Почему?**
**Можно ли при помощи рекурсии посчитать `sumTo(100000)`? Если нет, то почему?**

View file

@ -0,0 +1,23 @@
По свойствам факториала, как описано в условии, `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,31 @@
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,93 @@
# Вычисление рекурсией (медленное)
Решение по формуле, используя рекурсию:
```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 no-beautify
...
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 no-beautify
var a = 1, b = 1; // начальные значения
var c = a + b; // 2
/* переменные на начальном шаге:
a b c
1, 1, 2
*/
```
Теперь следующий шаг, присвоим `a` и `b` текущие 2 числа и получим новое следующее в `c`:
```js no-beautify
a = b, b = c;
c = a + b;
/* стало так (ещё число):
a b c
1, 1, 2, 3
*/
```
Следующий шаг даст нам ещё одно число последовательности:
```js no-beautify
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,24 @@
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 no-beautify
function fib(n) { /* ваш код */ }
alert( fib(3) ); // 2
alert( fib(7) ); // 13
alert( fib(77)); // 5527939700884757
```
**Все запуски функций из примера выше должны срабатывать быстро.**

View file

@ -0,0 +1,277 @@
# Recursion and stack
A function solves a task. In the process it can call many other functions. A partial case of this is when a function calls *itself*.
That's called *a recursion*.
Recursion is useful in situations when a task can be naturally split into several tasks of the same kind, but simpler. Or when a task can be simplified into an easy action plus a simpler variant of the same task.
Soon we'll see examples.
Recursion is a common programming topic, not specific to Javascript. If you developed in other languages or studied algorithms, then you probably already know what's this.
The chapter is mostly meant for those who are unfamiliar with the topic and want to study better how functions work.
[cut]
## Recursive thinking
For something simple to start with -- let's consider a task of raising `x` into a natural power of `n`. In other words, we need `x` multiplied by itself `n` times.
There are two ways to solve it.
1. Iterative thinking -- the `for` loop:
```js run
function pow(x, n) {
let result = 1;
// multiply result by x n times in the loop
for(let i = 0; i < n; i++) {
result *= x;
}
return result
}
alert( pow(2, 3) ); // 8
```
2. Recursive thinking -- simplify the task:
```js run
function pow(x, n) {
if (n == 1) {
return x;
} else {
return x * pow(x, n - 1);
}
}
alert( pow(2, 3) ); // 8
```
The recursive variant represents the function as a simple action plus a simplified task:
```js
if n==1 = x
/
pow(x, n) =
\ = x * pow(x, n - 1)
else
```
Here we have two branches.
1. The `n==1` is the trivial variant. It is called *the base* of recursion, because it immediately produces the obvious result: `pow(x, 1)` equals `x`.
2. Otherwise, we can represent `pow(x, n)` as `x * pow(x, n-1)`. In maths, one would write <code>x<sup>n</sup> = x * x<sup>n-1</sup></code>. This is called *a recursive step*, because we transform the task into a simpler action (multiplication by `x`) and a simpler call of the same task (`pow` with lower `n`). Next steps simplify it further and further untill `n` reaches `1`.
We can also say that `pow` *recursively calls itself* till `n == 1`.
For example, to calculate `pow(2, 4)` the recursive variant does these steps:
1. `pow(2, 4) = 2 * pow(2, 3)`
2. `pow(2, 3) = 2 * pow(2, 2)`
3. `pow(2, 2) = 2 * pow(2, 1)`
4. `pow(2, 1) = 2`
````smart header="Recursion is usually shorter"
A recursive solution is usually shorter.
Here we can write the same using the ternary `?` operator instead of `if` to make it even more terse and still very readable:
```js run
function pow(x, n) {
return (n == 1) ? x : (x * pow(x, n-1));
}
```
````
So, the recursion can be used when a function call can be reduced to a simpler call, and then -- to even more simpler, and so on, until the result becomes obvious.
The maximal number of nested calls (including the first one) is called *recursion depth*. In our case, it there will be exactly `n`.
The maximal recursion depth is limited by Javascript engine. We can make sure about 10000, some engines allow more, but 100000 is probably out of limit for the majority of them. There are automatic optimizations that help to alleviate this ("tail calls optimizations"), but they are not yet supported everywhere and work only in simple cases.
That limits the application of recursion, but it still remains very wide. There are many tasks where a recursive way of thinking gives simpler code, easier to maintain.
## The execution stack
Now let's examine how recursive calls work. For that we'll look under the hood of functions, how they work.
The information about a function run is stored in its *execution context*.
The [execution context](https://tc39.github.io/ecma262/#sec-execution-contexts) is an internal data structure that contains details about where the execution flow is now, current variables, the value of `this` (we don't use it here) and few other internal details.
### pow(2, 3)
For instance, for the beginning of the call `pow(2, 3)` the execution context will store variables: `x = 2, n = 3`, the execution flow is at line `1` of the function.
We can sketch it as:
<ul class="function-execution-context-list">
<li>
<span class="function-execution-context">Context: { x: 2, n: 3, at line 1 }</span>
<span class="function-execution-context-call">pow(2, 3)</span>
</li>
</ul>
That's when the function starts to execute. The condition `n == 1` is falsy, so the flow continues into the second branch of `if`:
```js run
function pow(x, n) {
if (n == 1) {
return x;
} else {
*!*
return x * pow(x, n - 1);
*/!*
}
}
alert( pow(2, 3) );
```
The line changes, so the context is now:
<ul class="function-execution-context-list">
<li>
<span class="function-execution-context">Context: { x: 2, n: 3, at line 5 }</span>
<span class="function-execution-context-call">pow(2, 3)</span>
</li>
</ul>
To calculate `x * pow(x, n-1)`, we need to run `pow` witn new arguments.
### pow(2, 2)
To do a nested call, Javascript remembers the current execution context in the *execution context stack*.
Here we call the same function `pow`, but it absolutely doesn't matter. The process is the same for all functions:
1. The current context is "laid away" on top of the stack.
2. The new context is created for the subcall.
3. When the subcall is finished -- the previous context is popped from the stack, and its execution continues.
The context stack as a list where the current execution context is always on-top, and previous remembered contexts are below:
<ul class="function-execution-context-list">
<li>
<span class="function-execution-context">Context: { x: 2, n: 2, at line 1 }</span>
<span class="function-execution-context-call">pow(2, 2)</span>
</li>
<li>
<span class="function-execution-context">Context: { x: 2, n: 3, at line 5 }</span>
<span class="function-execution-context-call">pow(2, 3)</span>
</li>
</ul>
Here, the new current execution context is also made bold for clarity.
Note that the context of the previous call not only includes the variables, but also the place in the code, so when the new call finishes -- it would be easy to resume. We use a word "line" in the illustrations for that, but of course in fact it's more precise.
### pow(2, 1)
The process repeats: a new subcall is made at line `5`, now with arguments `x=2`, `n=1`.
A new execution context is created, the previous one is pushed on top of the stack:
<ul class="function-execution-context-list">
<li>
<span class="function-execution-context">Context: { x: 2, n: 1, at line 1 }</span>
<span class="function-execution-context-call">pow(2, 1)</span>
</li>
<li>
<span class="function-execution-context">Context: { x: 2, n: 2, at line 5 }</span>
<span class="function-execution-context-call">pow(2, 2)</span>
</li>
<li>
<span class="function-execution-context">Context: { x: 2, n: 3, at line 5 }</span>
<span class="function-execution-context-call">pow(2, 3)</span>
</li>
</ul>
The stack has 2 old contexts now.
### The exit
During the execution of `pow(2, 1)`, unlike before, the condition `n == 1` is truthy, so the first branch of `if` works:
```js
function pow(x, n) {
if (n == 1) {
*!*
return x;
*/!*
} else {
return x * pow(x, n - 1);
}
}
```
There are no more nested calls, so the function finishes, returning `2`.
As the function finishes, its execution context is not needed any more, so it's removed from the memory. The previous one is restored off-top of the stack:
<ul class="function-execution-context-list">
<li>
<span class="function-execution-context">Context: { x: 2, n: 2, at line 5 }</span>
<span class="function-execution-context-call">pow(2, 2)</span>
</li>
<li>
<span class="function-execution-context">Context: { x: 2, n: 3, at line 5 }</span>
<span class="function-execution-context-call">pow(2, 3)</span>
</li>
</ul>
The execution of `pow(2, 2)` is resumed. It has the result of the subcall `pow(2, 1)`, so it also can finish the evaluation of `x * pow(x, n-1)`, returning `4`.
Then the previous context is restored:
<ul class="function-execution-context-list">
<li>
<span class="function-execution-context">Context: { x: 2, n: 3, at line 5 }</span>
<span class="function-execution-context-call">pow(2, 3)</span>
</li>
</ul>
When it finishes, we have a result of `pow(2, 3) = 8`.
The recursion depth in this case was: **3**.
As we can see from the illustrations above, recursion depth equals the maximal number of context in the stack.
Note the memory requirements. Contexts take memory. In our case, raising to the power of `n` actually requires the memory for `n` contexts, for all lower values of `n`.
A loop-based algorithm is much memory-saving:
```js
function pow(x, n) {
let result = 1;
for(let i = 0; i < n; i++) {
result *= x;
}
return result
}
```
The iterative `pow` uses a single context changing `i` and `result` in the process.
**Any recursion can be rewritten as a loop. The loop variant is usually more effective.**
...But sometimes the rewrite can be non-trivial, especially when function uses different recursive subcalls, when the branching is more intricate. And the optimization benefits may be unneeded and totally not worth that.
## Итого
Рекурсия -- это когда функция вызывает сама себя, как правило, с другими аргументами.
Существуют много областей применения рекурсивных вызовов. Здесь мы посмотрели на один из них -- решение задачи путём сведения её к более простой (с меньшими аргументами), но также рекурсия используется для работы с "естественно рекурсивными" структурами данных, такими как HTML-документы, для "глубокого" копирования сложных объектов.
Есть и другие применения, с которыми мы встретимся по мере изучения JavaScript.
Здесь мы постарались рассмотреть происходящее достаточно подробно, однако, если пожелаете, допустимо временно забежать вперёд и открыть главу <info:debugging-chrome>, с тем чтобы при помощи отладчика построчно пробежаться по коду и посмотреть стек на каждом шаге. Отладчик даёт к нему доступ.

View file

@ -0,0 +1,26 @@
<style>
.function-execution-context-list {
margin: 0;
padding: 0;
overflow: auto;
}
.function-execution-context {
border: 1px solid black;
font-family: "Consolas", monospace;
padding: 5px 8px;
}
.function-execution-context-call {
color: gray;
padding-left: 20px;
}
.function-execution-context-call::before {
content: '@';
}
.function-execution-context-list li:first-child {
font-weight: bold;
}
</style>