Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recursion and stack #199

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
12 changes: 6 additions & 6 deletions 1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
The solution using a loop:
Giải pháp sử dụng vòng lặp:

```js run
function sumTo(n) {
Expand All @@ -12,7 +12,7 @@ function sumTo(n) {
alert( sumTo(100) );
```

The solution using recursion:
Giải pháp sử dụng đệ quy:

```js run
function sumTo(n) {
Expand All @@ -23,7 +23,7 @@ function sumTo(n) {
alert( sumTo(100) );
```

The solution using the formula: `sumTo(n) = n*(n+1)/2`:
Giải pháp sử dụng công thức: `sumTo(n) = n*(n+1)/2`:

```js run
function sumTo(n) {
Expand All @@ -33,8 +33,8 @@ function sumTo(n) {
alert( sumTo(100) );
```

P.S. Naturally, the formula is the fastest solution. It uses only 3 operations for any number `n`. The math helps!
Tái bút: Đương nhiên, công thức là giải pháp nhanh nhất. Nó chỉ sử dụng 3 thao tác cho bất kỳ số `n` nào. Có toán giúp đỡ!

The loop variant is the second in terms of speed. In both the recursive and the loop variant we sum the same numbers. But the recursion involves nested calls and execution stack management. That also takes resources, so it's slower.
Biến thể vòng lặp là biến thể thứ hai về tốc độ. Trong cả biến thể đệ quy và vòng lặp, chúng ta tính tổng các số giống nhau. Nhưng đệ quy liên quan đến các cuộc gọi lồng nhau và quản lý ngăn xếp thực thi. Điều đó cũng cần tài nguyên, vì vậy nó chậm hơn.

P.P.S. Some engines support the "tail call" optimization: if a recursive call is the very last one in the function (like in `sumTo` above), then the outer function will not need to resume the execution, so the engine doesn't need to remember its execution context. That removes the burden on memory, so counting `sumTo(100000)` becomes possible. But if the JavaScript engine does not support tail call optimization (most of them don't), there will be an error: maximum stack size exceeded, because there's usually a limitation on the total stack size.
Tái bút nữa: Một số engine hỗ trợ tối ưu hóa "cuộc gọi đuôi": nếu một cuộc gọi đệ quy là cuộc gọi cuối cùng trong hàm (như trong `sumTo` ở trên), thì hàm bên ngoài sẽ không cần tiếp tục thực thi, vì vậy engine không cần để ghi nhớ bối cảnh thực hiện của nó. Điều đó loại bỏ gánh nặng cho bộ nhớ, vì vậy việc đếm `sumTo(100000)` trở nên khả thi. Nhưng nếu JavaScript engine không hỗ trợ tối ưu hóa lệnh gọi đuôi (hầu hết chúng không hỗ trợ), sẽ có lỗi: vượt quá kích thước ngăn xếp tối đa, vì thường có giới hạn về tổng kích thước ngăn xếp.
22 changes: 11 additions & 11 deletions 1-js/06-advanced-functions/01-recursion/01-sum-to/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ importance: 5

---

# Sum all numbers till the given one
# Tính tổng tất cả các số cho đến một số đã cho

Write a function `sumTo(n)` that calculates the sum of numbers `1 + 2 + ... + n`.
Viết hàm `sumTo(n)` để tính tổng các số `1 + 2 + ... + n`.

For instance:
Ví dụ:

```js no-beautify
sumTo(1) = 1
Expand All @@ -17,20 +17,20 @@ sumTo(4) = 4 + 3 + 2 + 1 = 10
sumTo(100) = 100 + 99 + ... + 2 + 1 = 5050
```

Make 3 solution variants:
Thực hiện 3 biến thể giải pháp:

1. Using a for loop.
2. Using a recursion, cause `sumTo(n) = n + sumTo(n-1)` for `n > 1`.
3. Using the [arithmetic progression](https://en.wikipedia.org/wiki/Arithmetic_progression) formula.
1. Sử dụng vòng lặp for.
2. Sử dụng đệ quy, gây ra `sumTo(n) = n + sumTo(n-1)` cho `n > 1`.
3. Sử dụng công thức [cấp số cộng](https://vi.wikipedia.org/wiki/C%E1%BA%A5p_s%E1%BB%91_c%E1%BB%99ng).

An example of the result:
Một ví dụ về kết quả:

```js
function sumTo(n) { /*... your code ... */ }
function sumTo(n) { /*... mã của bạn ... */ }

alert( sumTo(100) ); // 5050
```

P.S. Which solution variant is the fastest? The slowest? Why?
Tái bút: Biến thể giải pháp nào là nhanh nhất? Chậm nhất? Tại sao?

P.P.S. Can we use recursion to count `sumTo(100000)`?
Tái bút nữa: Chúng ta có thể sử dụng đệ quy để đếm `sumTo(100000)` không?
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
By definition, a factorial `n!` can be written as `n * (n-1)!`.
Theo định nghĩa, giai thừa `n!` có thể được viết là `n * (n-1)!`.

In other words, the result of `factorial(n)` can be calculated as `n` multiplied by the result of `factorial(n-1)`. And the call for `n-1` can recursively descend lower, and lower, till `1`.
Nói cách khác, kết quả của `giai thừa(n)` có thể được tính bằng `n` nhân với kết quả của `giai thừa(n-1)`. Và lệnh gọi `n-1` có thể đệ quy giảm xuống thấp hơn và thấp hơn cho đến `1`.

```js run
function factorial(n) {
Expand All @@ -10,7 +10,7 @@ function factorial(n) {
alert( factorial(5) ); // 120
```

The basis of recursion is the value `1`. We can also make `0` the basis here, doesn't matter much, but gives one more recursive step:
Cơ sở của đệ quy là giá trị `1`. Chúng ta cũng có thể đặt `0` làm cơ sở ở đây, không quan trọng lắm, nhưng cung cấp thêm một bước đệ quy:

```js run
function factorial(n) {
Expand Down
12 changes: 6 additions & 6 deletions 1-js/06-advanced-functions/01-recursion/02-factorial/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ importance: 4

---

# Calculate factorial
# Tính giai thừa

The [factorial](https://en.wikipedia.org/wiki/Factorial) of a natural number is a number multiplied by `"number minus one"`, then by `"number minus two"`, and so on till `1`. The factorial of `n` is denoted as `n!`
[Giai thừa](https://vi.wikipedia.org/wiki/Giai_th%E1%BB%ABa) của một số tự nhiên là một số nhân với `"số trừ một"`, sau đó nhân với `"số trừ hai"`, v.v. `1`. Giai thừa của `n` được ký hiệu là `n!`

We can write a definition of factorial like this:
Chúng ta có thể viết một định nghĩa về giai thừa như thế này:

```js
n! = n * (n - 1) * (n - 2) * ...*1
```

Values of factorials for different `n`:
Giá trị của giai thừa cho `n` khác nhau:

```js
1! = 1
Expand All @@ -22,10 +22,10 @@ Values of factorials for different `n`:
5! = 5 * 4 * 3 * 2 * 1 = 120
```

The task is to write a function `factorial(n)` that calculates `n!` using recursive calls.
Nhiệm vụ là viết một hàm `factorial(n)` để tính toán `n!` bằng cách gọi đệ quy.

```js
alert( factorial(5) ); // 120
```

P.S. Hint: `n!` can be written as `n * (n-1)!` For instance: `3! = 3*2! = 3*2*1! = 6`
Tái bút: Gợi ý: `n!` có thể được viết là `n * (n-1)!` Ví dụ: `3! = 3*2! = 3*2*1! = 6`
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The first solution we could try here is the recursive one.
Giải pháp đầu tiên chúng ta có thể thử ở đây là giải pháp đệ quy.

Fibonacci numbers are recursive by definition:
Các số Fibonacci được đệ quy theo định nghĩa:

```js run
function fib(n) {
Expand All @@ -12,11 +12,11 @@ alert( fib(7) ); // 13
// fib(77); // will be extremely slow!
```

...But for big values of `n` it's very slow. For instance, `fib(77)` may hang up the engine for some time eating all CPU resources.
...Nhưng đối với các giá trị lớn của `n` thì tốc độ rất chậm. Chẳng hạn, `fib(77)` có thể làm treo engine trong một thời gian và ăn hết tài nguyên CPU.

That's because the function makes too many subcalls. The same values are re-evaluated again and again.
Đó là bởi vì hàm thực hiện quá nhiều cuộc gọi phụ. Các giá trị tương tự được đánh giá lại nhiều lần.

For instance, let's see a piece of calculations for `fib(5)`:
Chẳng hạn, chúng ta hãy xem một đoạn tính toán cho `fib(5)`:

```js no-beautify
...
Expand All @@ -25,68 +25,68 @@ fib(4) = fib(3) + fib(2)
...
```

Here we can see that the value of `fib(3)` is needed for both `fib(5)` and `fib(4)`. So `fib(3)` will be called and evaluated two times completely independently.
Ở đây chúng ta có thể thấy rằng giá trị của `fib(3)` là cần thiết cho cả `fib(5)` `fib(4)`. Vì vậy, `fib(3)` sẽ được gọi và đánh giá hai lần hoàn toàn độc lập.

Here's the full recursion tree:
Đây là cây đệ quy đầy đủ:

![fibonacci recursion tree](fibonacci-recursion-tree.svg)

We can clearly notice that `fib(3)` is evaluated two times and `fib(2)` is evaluated three times. The total amount of computations grows much faster than `n`, making it enormous even for `n=77`.
Chúng ta có thể nhận thấy rõ ràng rằng `fib(3)` được đánh giá hai lần và `fib(2)` được đánh giá ba lần. Tổng lượng tính toán tăng nhanh hơn nhiều so với `n`, khiến nó trở nên khổng lồ ngay cả đối với `n=77`.

We can optimize that by remembering already-evaluated values: if a value of say `fib(3)` is calculated once, then we can just reuse it in future computations.
Chúng ta có thể tối ưu hóa điều đó bằng cách ghi nhớ các giá trị đã được đánh giá: nếu giá trị nói `fib(3)` được tính một lần, thì chúng ta chỉ có thể sử dụng lại giá trị đó trong các tính toán trong tương lai.

Another variant would be to give up recursion and use a totally different loop-based algorithm.
Một biến thể khác là từ bỏ đệ quy và sử dụng thuật toán dựa trên vòng lặp hoàn toàn khác.

Instead of going from `n` down to lower values, we can make a loop that starts from `1` and `2`, then gets `fib(3)` as their sum, then `fib(4)` as the sum of two previous values, then `fib(5)` and goes up and up, till it gets to the needed value. On each step we only need to remember two previous values.
Thay vì đi từ `n` xuống các giá trị thấp hơn, chúng ta có thể tạo một vòng lặp bắt đầu từ `1` `2`, sau đó lấy `fib(3)` làm tổng, rồi `fib(4)` làm tổng của hai giá trị trước đó, sau đó là `fib(5)` và tăng dần cho đến khi đạt giá trị cần thiết. Trên mỗi bước chúng ta chỉ cần nhớ hai giá trị trước đó.

Here are the steps of the new algorithm in details.
Dưới đây là các bước của thuật toán mới một cách chi tiết.

The start:
Bắt đầu:

```js
// a = fib(1), b = fib(2), these values are by definition 1
// a = fib(1), b = fib(2), các giá trị này theo định nghĩa 1
let a = 1, b = 1;

// get c = fib(3) as their sum
// lấy c = fib(3) làm tổng của chúng
let c = a + b;

/* we now have fib(1), fib(2), fib(3)
/* bây giờ chúng ta có fib(1), fib(2), fib(3)
a b c
1, 1, 2
*/
```

Now we want to get `fib(4) = fib(2) + fib(3)`.
Bây giờ chúng ta muốn có `fib(4) = fib(2) + fib(3)`.

Let's shift the variables: `a,b` will get `fib(2),fib(3)`, and `c` will get their sum:
Hãy dịch chuyển các biến: `a,b` sẽ nhận được `fib(2),fib(3)`, `c` sẽ nhận được tổng của chúng:

```js no-beautify
a = b; // now a = fib(2)
b = c; // now b = fib(3)
a = b; // bây giờ a = fib(2)
b = c; // bây giờ b = fib(3)
c = a + b; // c = fib(4)

/* now we have the sequence:
/* bây giờ chúng ta có trình tự:
a b c
1, 1, 2, 3
*/
```

The next step gives another sequence number:
Bước tiếp theo đưa ra một số thứ tự khác:

```js no-beautify
a = b; // now a = fib(3)
b = c; // now b = fib(4)
a = b; // bây giờ a = fib(3)
b = c; // bây giờ b = fib(4)
c = a + b; // c = fib(5)

/* now the sequence is (one more number):
/* bây giờ trình tự là (thêm một số):
a b c
1, 1, 2, 3, 5
*/
```

...And so on until we get the needed value. That's much faster than recursion and involves no duplicate computations.
...Và cứ như vậy cho đến khi chúng ta nhận được giá trị cần thiết. Điều đó nhanh hơn nhiều so với đệ quy và không cần tính toán trùng lặp.

The full code:
Mã đầy đủ:

```js run
function fib(n) {
Expand All @@ -105,6 +105,6 @@ alert( fib(7) ); // 13
alert( fib(77) ); // 5527939700884757
```

The loop starts with `i=3`, because the first and the second sequence values are hard-coded into variables `a=1`, `b=1`.
Vòng lặp bắt đầu với `i=3`, bởi vì các giá trị chuỗi thứ nhất và thứ hai được mã hóa cứng thành các biến `a=1`, `b=1`.

The approach is called [dynamic programming bottom-up](https://en.wikipedia.org/wiki/Dynamic_programming).
Cách tiếp cận này được gọi là [quy hoạch động](https://vi.wikipedia.org/wiki/Quy_ho%E1%BA%A1ch_%C4%91%E1%BB%99ng).
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@ importance: 5

---

# Fibonacci numbers
# Dãy Fibonacci

The sequence of [Fibonacci numbers](https://en.wikipedia.org/wiki/Fibonacci_number) has the formula <code>F<sub>n</sub> = F<sub>n-1</sub> + F<sub>n-2</sub></code>. In other words, the next number is a sum of the two preceding ones.
Dãy [số Fibonacci](https://vi.wikipedia.org/wiki/D%C3%A3y_Fibonacci) có công thức <code>F<sub>n</sub> = F<sub>n-1</sub> + F<sub>n-2</sub></code>. Nói cách khác, số tiếp theo là tổng của hai số trước.

First two numbers are `1`, then `2(1+1)`, then `3(1+2)`, `5(2+3)` and so on: `1, 1, 2, 3, 5, 8, 13, 21...`.
Hai số đầu tiên là `1`, sau đó là `2(1+1)`, sau đó là `3(1+2)`, `5(2+3)`, v.v.: `1, 1, 2, 3, 5 , 8, 13, 21...`.

Fibonacci numbers are related to the [Golden ratio](https://en.wikipedia.org/wiki/Golden_ratio) and many natural phenomena around us.
Các số Fibonacci có liên quan đến [Tỷ lệ vàng](https://vi.wikipedia.org/wiki/T%E1%BB%B7_l%E1%BB%87_v%C3%A0ng) và nhiều hiện tượng tự nhiên xung quanh chúng ta.

Write a function `fib(n)` that returns the `n-th` Fibonacci number.
Viết hàm `fib(n)` trả về số Fibonacci thứ n-th`.

An example of work:
Một ví dụ về công việc:

```js
function fib(n) { /* your code */ }
function fib(n) { /* mã của bạn */ }

alert(fib(3)); // 2
alert(fib(7)); // 13
alert(fib(77)); // 5527939700884757
```

P.S. The function should be fast. The call to `fib(77)` should take no more than a fraction of a second.
Tái bút: Các hàm nên được nhanh chóng. Lệnh gọi `fib(77)` sẽ mất không quá một phần giây.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Loop-based solution
# Giải pháp dựa trên vòng lặp

The loop-based variant of the solution:
Biến thể dựa trên vòng lặp của giải pháp:

```js run
let list = {
Expand Down Expand Up @@ -30,7 +30,7 @@ function printList(list) {
printList(list);
```

Please note that we use a temporary variable `tmp` to walk over the list. Technically, we could use a function parameter `list` instead:
Hãy lưu ý rằng chúng ta sử dụng một biến tạm thời `tmp` để duyệt qua danh sách. Về mặt kỹ thuật, chúng ta có thể sử dụng tham số chức năng `list` để thay thế:

```js
function printList(list) {
Expand All @@ -43,15 +43,15 @@ function printList(list) {
}
```

...But that would be unwise. In the future we may need to extend a function, do something else with the list. If we change `list`, then we lose such ability.
...Nhưng đó sẽ là không khôn ngoan. Trong tương lai, chúng ta có thể cần mở rộng một hàm, làm điều gì đó khác với danh sách. Nếu chúng ta thay đổi `list`, thì chúng ta sẽ mất khả năng đó.

Talking about good variable names, `list` here is the list itself. The first element of it. And it should remain like that. That's clear and reliable.
Nói về tên biến tốt, `list` ở đây chính là danh sách. Yếu tố đầu tiên của nó. Và nó nên giữ nguyên như vậy. Điều đó rõ ràng và đáng tin cậy.

From the other side, the role of `tmp` is exclusively a list traversal, like `i` in the `for` loop.
Mặt khác, vai trò của `tmp` chỉ là duyệt danh sách, giống như `i` trong vòng lặp `for`.

# Recursive solution
# Giải pháp đệ quy

The recursive variant of `printList(list)` follows a simple logic: to output a list we should output the current element `list`, then do the same for `list.next`:
Biến thể đệ quy của `printList(list)` tuân theo logic đơn giản: để xuất danh sách, chúng ta nên xuất phần tử hiện tại `list`, sau đó thực hiện tương tự cho `list.next`:

```js run
let list = {
Expand All @@ -70,19 +70,19 @@ let list = {

function printList(list) {

alert(list.value); // output the current item
alert(list.value); // xuất mục hiện tại

if (list.next) {
printList(list.next); // do the same for the rest of the list
printList(list.next); // làm tương tự cho phần còn lại của danh sách
}

}

printList(list);
```

Now what's better?
Bây giờ cái nào tốt hơn?

Technically, the loop is more effective. These two variants do the same, but the loop does not spend resources for nested function calls.
Về mặt kỹ thuật, vòng lặp hiệu quả hơn. Hai biến thể này thực hiện tương tự, nhưng vòng lặp không dành tài nguyên cho các lệnh gọi hàm lồng nhau.

From the other side, the recursive variant is shorter and sometimes easier to understand.
Mặt khác, biến thể đệ quy ngắn hơn và đôi khi dễ hiểu hơn.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ importance: 5

---

# Output a single-linked list
# Xuất danh sách liên kết đơn

Let's say we have a single-linked list (as described in the chapter <info:recursion>):
Giả sử chúng ta có một danh sách liên kết đơn (như được mô tả trong chương <info:recursion>):

```js
let list = {
Expand All @@ -22,8 +22,8 @@ let list = {
};
```

Write a function `printList(list)` that outputs list items one-by-one.
Viết một hàm `printList(list)` để xuất từng mục một trong danh sách.

Make two variants of the solution: using a loop and using recursion.
Thực hiện hai biến thể của giải pháp: sử dụng vòng lặp và sử dụng đệ quy.

What's better: with recursion or without it?
Điều gì tốt hơn: đệ quy hay không đệ quy?
Loading