前端學習 數據結構與算法 快速入門 系列 —— 遞歸


其他章節請看:

前端學習 數據結構與算法 快速入門 系列

遞歸

遞歸的概念

遞歸是一種解決問題的方法,它從解決問題的各個小部分開始,直到解決最初的大問題。

遞歸通常涉及調用函數本身,直接調用自身,亦或者間接調用自身,都是遞歸函數。就像這樣:

// 直接調用自身
function fn1(){
    fn1()
}
// 間接調用自身
function fn2(){
    fn3()
}

function fn3(){
    fn2()
}

現在執行 fn1() 會一直執行下去,所以每個遞歸函數都必須有一個不在遞歸調用的條件(即基線條件),以防止無限遞歸。

有句名言:要理解遞歸,首先要理解遞歸。我們將其翻譯成 javascript 代碼:

<script>
    function 理解遞歸() {
        const answer = confirm('你理解遞歸了嗎?')
        if (answer) {
            return
        }
        理解遞歸()
    }
    理解遞歸()
</script>

將這段代碼在瀏覽器中執行,會不斷詢問 你理解遞歸了嗎?,直到你點擊 確認 才會終止。

計算一個數的階乘

一個正整數的 階乘(factorial)是所有小於及等於該數的正整數的積,並且0的階乘為1。自然數n的階乘寫作n!

亦即n!=1×2×3×...×(n-1)×n

5 的階乘表示為 5!,等於 1*2*3*4*5,即 120

請看遞歸實現:

// 默認 n 是大於等於0的正整數
function factorial(n) {
  // 基線條件
  if (n <= 1) {
    return 1
  }
  // 遞歸調用
  return n * factorial(n - 1)
}

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

超出最大調用堆棧大小

如果忘記給遞歸函數添加停止的條件,會發生什么?就像這樣:

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

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

測試:

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

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

在 chrome v95 中,該函數執行了 13955 次,最后拋出錯誤:RangeError:超出最大調用堆棧大小,因此,具有停止遞歸的基線條件非常重要。

Tip:es6 有尾調用優化,也就是說這段代碼會一直執行下去。查看 兼容表 你會發現絕大多數瀏覽器都不支持尾調用(proper tail calls (tail call optimisation)),故不在展開。

斐波那契數

斐波那契數列(Fibonacci sequence)指的是這樣一個數列:0、1、1、2、3、5、8、13、21、34、……

數 2 由 1 + 1 得到,數 3 由 2 + 1 得到,數 5 由 3 + 2 得到,以此類推。

斐波拉契數列定義如下:

  • 位置 0 的斐波拉契數是 0
  • 位置 1 和 2 的斐波拉契數為 1
  • 位置 n(此處 n > 2)的斐波拉契數是 (n - 1) 的斐波拉契數加上 (n - 2) 的斐波拉契數。

請看遞歸實現:

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))
}

遞歸更快嗎

我們使用 console.time() 來檢測兩個版本的 fibonacci 函數(迭代實現 vs 遞歸實現):

// 迭代求斐波拉契數
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
}

測試:

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

測試表明迭代版本比遞歸版本要快很多。

但是迭代版本更容易理解,所需的代碼也更少,此外,對於某些算法,迭代的解法可能不可用。

記憶化的優化技術

執行 fibonacci(45) 既然花費了 8 秒,時間花在哪里?

假如我們要計算 fibonacci(5),調用情況如下:

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) 被調用 2 次,fibonacci(2) 被調用 3 次,fibonacci(1) 調用了 5 次。

我們可以將結果存下來,當需要再次計算它的時候,我們就無需重復計算,直接返回結果即可。重寫 fibonacci() 如下:

const fibonacciMemoization = (function () {
  const mem = [0, 1]
  function fibonacci(val) {
    // 在緩存中則直接返回
    if (mem[val]) {
      return mem[val]
    }
    if (val <= 1) {
      return val
    }
    const result = fibonacci(val - 1) + fibonacci(val - 2)
    // 存入緩存中
    mem.push(result)
    return result
  }
  return fibonacci
}())

測試:

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

雖然遞歸版本花費 10s,但是記憶化優化版本只花費 0.5ms,和迭代版本所花時間幾乎相同。

其他章節請看:

前端學習 數據結構與算法 快速入門 系列


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM