函數的遞歸和堆棧


之前學遞歸一直學的迷迷糊糊,感覺懂了又感覺沒懂,今天正好學習到了這一部分。

當函數解決一個任務時,在解決的過程中它可能會調用很多其他函數。當函數調用自身時,就是所謂的遞歸

舉一個例子:

一個函數pow(x,n),計算x的n次方。

遞歸思路:

1 function pow(x, n) {
2   if (n == 1) {
3     return x;
4   } else {
5     return x * pow(x, n - 1);
6   }
7 }
8 
9 alert( pow(2, 3) ); // 8

函數執行分為兩個分支:

1、如果n == 1,函數會立即產生明顯的結果,這叫做基礎的遞歸

2、else,這個分支叫做一個遞歸步驟:將任務轉化為更簡單的行為(x的乘法)和更簡單的同類任務(帶有更小的n的pow運算)的調用。接下來的步驟將其進一步簡化,直到n到達1。

比如,為了計算 pow(2, 4),遞歸變體經過了下面幾個步驟:

  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

 

到這里之前也是懂的,接下來研究一下遞歸調用的工作原理。

函數底層的工作原理:有關正在運行的函數的執行過程的相關信息被存貯在其 執行上下文中。

執行上下文是一個內部數據結構,它包含有關函數執行時的詳細細節:當前控制流所在的位置,當前的變量,this 的值(此處我們不使用它),以及其它的一些內部細節。

一個函數調用僅具有一個與其相關聯的執行上下文。

當一個函數進行嵌套調用時,將發生以下的事兒:

  • 當前函數被暫停;
  • 與它關聯的執行上下文被一個叫做 執行上下文堆棧 的特殊數據結構保存;
  • 執行嵌套調用;
  • 嵌套調用結束后,從堆棧中恢復之前的執行上下文,並從停止的位置恢復外部函數。

讓我們看看 pow(2, 3) 調用期間都發生了什么。

pow(2, 3)

在調用 pow(2, 3) 的開始,執行上下文(context)會存儲變量:x = 2, n = 3,執行流程在函數的第 1 行。

我們將其描繪如下:

  • Context: { x: 2, n: 3, at line 1 } call: pow(2, 3)

這是函數開始執行的時候。條件 n == 1 結果為 false,所以執行流程進入 if 的第二分支。

 

變量相同,但是行改變了,因此現在的上下文是:

  • Context: { x: 2, n: 3, at line 5 } call:pow(2, 3)

為了計算 x * pow(x, n - 1),我們需要使用帶有新參數的新的 pow 子調用 pow(2, 2)

pow(2,2)

為了執行嵌套調用,JavaScript 會在 執行上下文堆棧 中記住當前的執行上下文。

這里我們調用相同的函數 pow,所有函數的處理都是一樣的:

  1. 當前上下文被“記錄”在堆棧的頂部。
  2. 為子調用創建新的上下文。
  3. 當子調用結束后 —— 前一個上下文被從堆棧中彈出,並繼續執行

下面是進入子調用 pow(2, 2) 時的上下文堆棧:

  • Context: { x: 2, n: 2, at line 1 } pow(2, 2)
  • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

新的當前執行上下文位於頂部(粗體顯示),之前記住的上下文位於下方。

當我們完成子調用后 —— 很容易恢復上一個上下文,因為它既保留了變量,也保留了當時所在代碼的確切位置。

pow(2,1)

這是當pow(2,1)時,函數的執行上下文堆棧,現在的參數是x=2,n=1,

新的執行上下文被創建,前一個被壓入堆棧頂部:

  • Context: { x: 2, n: 1, at line 1 } call:pow(2, 1)
  • Context: { x: 2, n: 2, at line 5 } call:pow(2, 2)
  • Context: { x: 2, n: 3, at line 5 } call:pow(2, 3)

此時,有2個舊的上下文和一個當前正在運行的pow(2,1)的上下文。

出口

在執行 pow(2, 1) 時,與之前的不同,條件 n == 1 為 true,因此 if 的第一個分支生效: 

此時不再有更多的嵌套調用,所以函數結束,返回 2

函數完成后,就不再需要其執行上下文了,因此它被從內存中移除。前一個上下文恢復到堆棧的頂部:

  • Context: { x: 2, n: 2, at line 5 }call: pow(2, 2)
  • Context: { x: 2, n: 3, at line 5 }call: pow(2, 3)

恢復執行 pow(2, 2)。它擁有子調用 pow(2, 1) 的結果,因此也可以完成 x * pow(x, n - 1) 的執行,並返回 4

然后,前一個上下文被恢復:

  • Context: { x: 2, n: 3, at line 5 }call: pow(2, 3)

當它結束后,我們得到了結果 pow(2, 3) = 8

 

本示例中的遞歸深度為:3

遞歸深度:最大的嵌套調用次數(包括首次)。

從上面我們可以看出,遞歸深度等於堆棧中上下文的最大數量。

使用遞歸時需要注意內存,上下文占用內存。

 

參考文獻:https://zh.javascript.info/recursion

 


免責聲明!

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



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