Front end learning data structure and algorithm quick start series - recursion

Keywords: data structure

recursion

The concept of recursion

Recursion is a method to solve a problem. It starts from solving each small part of the problem to solving the initial big problem.

Recursion usually involves calling the function itself, directly calling itself, or indirectly calling itself, which are recursive functions. Like this:

// Call itself directly
function fn1(){
    fn1()
}
// Call itself indirectly
function fn2(){
    fn3()
}

function fn3(){
    fn2()
}

Now fn1() will be executed all the time, so each recursive function must have a condition that is not called recursively (i.e. baseline condition) to prevent infinite recursion.

There is a famous saying: to understand recursion, we must first understand recursion. We translate it into javascript code:

<script>
    function Understanding recursion() {
        const answer = confirm('Do you understand recursion?')
        if (answer) {
            return
        }
        Understanding recursion()
    }
    Understanding recursion()
</script>

When you execute this code in the browser, you will keep asking if you understand recursion?, It will not terminate until you click OK.

Calculate the factorial of a number

A positive integer Factorial (factorial) is the product of all positive integers less than or equal to the number, and the factorial of 0 is 1. Factorial writing of natural number n n!

That is n= one × two × three ×...× (n-1) × n

The factorial of 5 is expressed as 5!, Equal to 1 * 2 * 3 * 4 * 5, i.e. 120

See recursive implementation:

// The default n is a positive integer greater than or equal to 0
function factorial(n) {
  // Baseline conditions
  if (n <= 1) {
    return 1
  }
  // Recursive call
  return n * factorial(n - 1)
}

console.log(factorial(5)) // 120

Maximum call stack size exceeded

What happens if you forget to add a stop condition to a recursive function? Like this:

<script>
    let i = 0
    function fn4() {
        i++
        return fn4()
    }

    try {
        fn4()
    } catch (e) {
        console.log(`i : ${i}   error : ${e}`)
    }
</script>

Test:

// Google Chrome v95
i : 13955   error : RangeError: Maximum call stack size exceeded

// Microsoft Edge v95
i : 13948   error : RangeError: Maximum call stack size exceeded

In chrome v95, the function executes 13955 times and finally throws an error: RangeError: the maximum call stack size is exceeded. Therefore, it is very important to have a baseline condition to stop recursion.

Tip: es6 has tail call optimization, which means that this code will be executed all the time. see Compatibility table You will find that most browsers do not support proper tail calls (tail call optimization), so they are not expanded.

Fibonacci number

Fibonacci sequence (Fibonacci sequence) refers to such a sequence: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

The number 2 is obtained by 1 + 1, the number 3 is obtained by 2 + 1, the number 5 is obtained by 3 + 2, and so on.

Fiboracci sequence is defined as follows:

  • The fiboracci number at position 0 is 0
  • The fiboracci number of positions 1 and 2 is 1
  • The Fibonacci number at position n (where n > 2) is the Fibonacci number of (n - 1) plus the Fibonacci number of (n - 2).

See recursive implementation:

function fibonacci(val) {
  if (val <= 1) {
    return val
  }

  return fibonacci(val - 1) + fibonacci(val - 2)
}

// 0 1 1 2 3 5
for (let i = 0; i <= 5; i++) {
  console.log(fibonacci(i))
}

Is recursion faster

We use console.time() To detect two versions of fibonacci function (iterative implementation vs recursive implementation):

// Iterative calculation of fiboracci number
function fibonacciIterative(n) {
  let pre = 1
  let prePre = 0
  let result = n

  for (let i = 2; i <= n; i++) {
    result = pre + prePre;
    [prePre, pre] = [pre, pre + prePre]
  }
  return result
}

Test:

console.time('fibonacciIterative()')
console.log(fibonacciIterative(45))
console.timeEnd('fibonacciIterative()')

console.time('fibonacci()')
console.log(fibonacci(45))
console.timeEnd('fibonacci()')

// 1134903170
// fibonacciIterative(): 0.579ms
// 1134903170
// fibonacci(): 8.260s

Tests show that the iterative version is much faster than the recursive version.

However, the iterative version is easier to understand and requires less code. In addition, for some algorithms, the iterative solution may not be available.

Memory optimization technology

Since it took 8 seconds to execute fibonacci(45), where was the time spent?

If we want to calculate fibonacci(5), the call is as follows:

graph TD a["fibonacci(5)"] --> b["fibonacci(4)"] a["fibonacci(5)"] --> c["fibonacci(3)"] b["fibonacci(4)"] --> d["fibonacci(3)"] b["fibonacci(4)"] --> e["fibonacci(2)"] c["fibonacci(3)"] --> f["fibonacci(2)"] c["fibonacci(3)"] --> g["fibonacci(1)"] d["fibonacci(3)"] --> h["fibonacci(2)"] d["fibonacci(3)"] --> i["fibonacci(1)"]

fibonacci(3) was called twice, fibonacci(2) was called three times, and fibonacci(1) was called five times.

We can save the result. When we need to calculate it again, we don't need to repeat the calculation and return the result directly. Rewrite fibonacci() as follows:

const fibonacciMemoization = (function () {
  const mem = [0, 1]
  function fibonacci(val) {
    // In the cache, it returns directly
    if (mem[val]) {
      return mem[val]
    }
    if (val <= 1) {
      return val
    }
    const result = fibonacci(val - 1) + fibonacci(val - 2)
    // Store in cache
    mem.push(result)
    return result
  }
  return fibonacci
}())

Test:

let num = 45
console.time('fibonacci()')
console.log(fibonacci(num))
console.timeEnd('fibonacci()')

console.time('fibonacciIterative()')
console.log(fibonacciIterative(num))
console.timeEnd('fibonacciIterative()')

console.time('fibonacciMemoization()')
console.log(fibonacciMemoization(num))
console.timeEnd('fibonacciMemoization()')

// 1134903170
// fibonacci(): 10.590s
// 1134903170
// fibonacciIterative(): 0.513ms
// 1134903170
// fibonacciMemoization(): 0.506ms

Although the iterative version takes 10s, the time spent memorizing the optimized version and the iterative version is almost the same (0.5ms).

Posted by n14charlie on Tue, 23 Nov 2021 02:42:53 -0800