瞭解 JavaScript 的遞歸

簡介

使用遞歸能夠更天然地解決一些問題。例如,像斐波那契數列:數列中的每一個數字都是數列中前兩個數字的和。凡是須要您構建或遍歷樹狀數據結構的問題基本均可以經過遞歸來解決,鍛鍊本身強大的遞歸思惟,你會發現解決這類問題十分容易。javascript

在本文中,我將列舉兩個案例,讓大家瞭解遞歸函數是如何工做的。java

綱要
  • 什麼是遞歸
  • 數字的遞歸
  • 數組的遞歸
  • 總結

什麼是遞歸

函數的遞歸就是在函數中調用自身,看一個簡單的例子:數組

function doA(n) {
    ...
    doA(n-1);
}
複製代碼

爲了理解遞歸在理論上是如何工做的,咱們先舉一個與代碼無關的例子。想象一下,你是一家公司的話務員。因爲這是一家業務繁忙的公司,你的座機鏈接多條線路,所以你能夠同時處理多個電話。每條線路對應接收器上的一個按鈕,當有來電時,該按鈕將閃爍。今天當你到達公司開始工做時,發現有四條線路對應的按鈕正在閃爍,因此你須要接聽全部這些電話。數據結構

你接通線路一,並告訴他「請稍等」,而後你接通線路二,並告訴他「請稍等」,接着,你接通線路三,也告知他「請稍等」,最後,你接通線路四,並與其通話。當你結束了與線路四的通話以後,你回過頭來接通線路三,當你結束了與線路三的通話以後,你接通線路二,結束以後,你再接通線路一,當與線路一的這位客戶結束通話後,你終於能夠放下電話了。函數

這個例子中的每一通電話就像某函數中的一個遞歸調用。當你接到一個電話且不能當即處理時,這個電話將被擱置;當你有一個不須要當即觸發的函數調用時,它將停留在調用棧上。當你能夠接聽一個電話時,這個線路會被接通;當你的代碼可以觸發一個函數調用時,它會從調用棧中彈出。在你看到以後的代碼案例有些發懵時,請回想一下這個比喻。測試

數字的遞歸

每一個遞歸函數都須要一個終止條件,從而使其不會無休止地循環下去。然而,僅僅加一個終止條件,是不足以免其無限循環的。該函數必須一步一步地接近終止條件。在遞歸步驟中,問題會逐步簡化爲更小的問題。spa

假設有一個函數:從1加到n。例如,當n = 4,它實現的就是「1 + 2 + 3 + 4」。code

首先,咱們須要尋找終止條件。這一步能夠認爲是找到那個不經過遞歸就直接結束該問題的條件。當n等於0時,無法再拆分了,因此咱們的遞歸在到達0時中止。對象

在每一步中,你將從當前數字減去1。什麼是遞歸條件?就是用減小的數字調用函數sum遞歸

function sum(num){
    if (num === 0) {
        return 0;
    } else {
        return num + sum(--num)
    }
}
 
sum(4);     //10
複製代碼

每一步過程以下:

  • 執行sum(4)。
  • 4等於0麼?否,把sum(4)保留並執行sum(3)。
  • 3等於0麼?否,把sum(3)保留並執行sum(2)。
  • 2等於0麼?否,把sum(2)保留並執行sum(1)。
  • 1等於0麼?否,把sum(1)保留並執行sum(0)。
  • 0等於0麼?是,計算sum(0)。
  • 提取sum(1)。
  • 提取sum(2)。
  • 提取sum(3)。
  • 提取sum(4)。

這是查看函數如何處理每一個調用的另外一種方式:

sum(4)
4 + sum(3)
4 + ( 3 + sum(2) )
4 + ( 3 + ( 2 + sum(1) ))
4 + ( 3 + ( 2 + ( 1 + sum(0) )))
4 + ( 3 + ( 2 + ( 1 + 0 ) ))
4 + ( 3 + ( 2 + 1 ) )
4 + ( 3 +  3 ) 
4 + 6 
10
複製代碼

咱們能夠發現,遞歸條件中的參數不斷改變,並逐漸接近並最終符合終止條件。在上面的案例中,咱們在遞歸條件中的每一步都將參數減1,最後在終止條件中測試參數是否等於0。

任務
  1. 使用常規循環方法而不是遞歸來寫一個數字求和的sum函數。
  2. 寫一個遞歸函數來實現兩數相乘。例如:multiply(2,4) 將返回8,寫出multiply(2,4)的每一步發生的狀況。

數組的遞歸

數組的遞歸和數字的遞歸類似,相似於數字的遞減,咱們在每一步遞減數組中的元素個數,直到得到一個空數組。

考慮使用數組做爲求和函數的參數,並返回數組中全部元素的總和。求和函數以下:

function sum(arr) {
    var len = arr.length;
    if (len == 0) {
        return 0;
    } else {
        return arr[0] + sum(arr.slice(1));
    }
}
複製代碼

若是數組長度等於0,則返回0,arr[0]表示數組的第一位,arr.slice(1)表示從第一位開始截取arr數組,並返回截取以後的數組。例如var arr = [1,2,3,4];arr[0]爲1,arr.slice(1)[2,3,4]。當咱們執行sum([1,2,3,4])時,都發生了一些什麼?

sum([1,2,3,4])
1 + sum([2,3,4])
1 + ( 2 + sum([3,4]) )
1 + ( 2 + ( 3 + sum([4]) ))
1 + ( 2 + ( 3 + ( 4 + sum([]) )))
1 + ( 2 + ( 3 + ( 4 + 0 ) ))
1 + ( 2 + ( 3 + 4 ) )
1 + ( 2 + 7 ) 
1 + 9
10
複製代碼

每一次執行都檢查數組是否爲空,不然,對元素數量逐漸遞減的該數組執行遞歸。

任務
  1. 使用常規循環方法而不是遞歸來寫一個數組求和的sum函數。
  2. 定義一個length()函數,數組做爲參數,返回數組長度(不可使用Javascript Array對象內置的length屬性)。例如:length(['a', 'b', 'c', 'd']),並寫出每一步發生的事情。

總結

一個過程或函數在其定義或說明中有直接或間接調用自身的一種方法,它一般把一個大型複雜的問題層層轉化爲一個與原問題類似的規模較小的問題來求解,遞歸策略只需少許的程序就可描述出解題過程所須要的屢次重複計算,大大地減小了程序的代碼量。

本文只列舉兩個小案例,只爲說明遞歸是怎麼回事,上述兩個案例的公式都是變量+函數的形式,固然也有不少函數+函數的形式的案例,例如文章開頭提到的著名的斐波那契數列,代碼以下:

function func( n ) { 
    if (n == 0 || n == 1) {
        return 1;
    }
        return func(n-1) + func(n-2);
}
    
複製代碼

下面來講一下使用遞歸的步驟及優缺點。

步驟
  1. 找規律,將這個規律轉換成一個公式return出來。
  2. 找出口,出口即終止條件,它必定是一個已知的條件。
優勢
  1. 代碼異常簡潔。
  2. 符合人類思惟。
缺點
  1. 因爲遞歸是調用函數自身,而函數調用須要消耗時間和空間:每次調用,都要在內存棧中分配空間以存儲參數、臨時變量、返回地址等,往棧中壓入和彈出數據都須要消耗時間。這勢必致使執行效率大打折扣。
  2. 遞歸中的計算大都是重複的,其本質是把一個問題拆解成多個小問題,小問題之間存在互相重疊的部分,這樣的重複計算也會致使效率的低下。
  3. 調用棧可能會溢出。棧是有容量限制的,當調用層次過多,就會超出棧的容量限制,從而致使棧溢出!

可見遞歸的缺點仍是很明顯的,在實際開發中,在可控的狀況下,合理使用。

感謝您的閱讀!

相關文章
相關標籤/搜索