前言
最近在讀《JavaScript語言精粹》,對遞歸函數有了進一步的認識,希望總結下來:
遞歸是一種強大的編程技術,他把一個問題分解為一組相似的子問題,每一問題都用一個尋常解去解決。遞歸函數就是會直接或者間接調用自身的一種函數,一般來說,一個遞歸函數調用自身去解決它的子問題。
"漢諾塔"經典遞歸問題
"漢諾塔"是印度的一個古老傳說,也是程序設計中的經典的遞歸問題,是一個著名的益智游戲:
題目如下:
塔上有三根柱子和一套直徑各不相同的空心圓盤,開始時源柱子上的所有圓盤都按從大到小的順序排列。目標是通過每一次移動一個圓盤到另一根柱子上,最終把一堆圓盤移動到目標柱子上,過程中不允許把較大的圓盤放置在較小的圓盤上;
尋找規律(把所有的圓盤移動到C):
1)n(圓盤個數) == 1
第一次:1號盤 A -> C sum(移動次數) = 1
2)n == 2
第一次:1號盤 A -> B
第二次:2號盤 A -> C
第三次:1號盤 B -> C sum = 3
3)n == 3
第一次:1號盤 A -> C
第二次:2號盤 A -> B
第三次:1號盤 C -> B
第四次:3號盤 A -> C
第五次:1號盤 B -> A
第六次:2號盤 B -> C
第七次:1號盤 A -> C sum = 7
以此類推...
故不難發現規律,移動次數為:sum = 2^n - 1
算法分析(遞歸):
把一堆圓盤從一個柱子移動另一根柱子,必要時使用輔助的柱子。可以把它分為三個子問題:
首先,移動一對圓盤中較小的圓盤到輔助柱子上,從而露出下面較大的圓盤,
其次,移動下面的圓盤到目標柱子上
最后,將剛才較小的圓盤從輔助柱子上在移動到目標柱子上
把三個步驟轉化為簡單數學問題:
(1) 把 n-1個盤子由A 移到 B;
(2) 把 第 n個盤子由 A移到 C;
(3) 把n-1個盤子由B 移到 C;
我們創建一個JS函數,當它調用自身的時候,它去處理當前正在處理圓盤之上的圓盤。最后它回一個不存在圓盤去調用,在這種情況下,它不在執行任何操作。
JavaScript源代碼實現
var hanoi = function(disc,src,aux,dst){ if(disc>0){ hanoi(disc-1,src,dst,aux); console.log(' 移動 '+ disc + ' 號圓盤 ' + ' 從 ' + src + ' 移動到 ' + dst); hanoi(disc-1,aux,src,dst) } } hanoi(3,'A','B','C')
整個算法的思路是:
- 將A柱子上的n-1個盤子暫時移到B柱子上
- A柱子只剩下最大的盤子,把它移到目標柱子C上
- 最后再將B柱子上的n-1個盤子移到目標柱子C上
JS遞歸函數遍歷Dom
遞歸函數可以非常高效的操作樹形結構,在JavaScript有一種"天然的樹形結構"瀏覽器端的文檔對象模型(Dom)。每次遞歸調用時處理指定樹的一小段。
/* 我們定義一個walk_the_DOM函數, 1) 它從某個指定的節點開始,按指定HTML源碼的順序,訪問樹的每個節點 2)它會調用一個函數,並依次傳遞每個節點給它,walk_the_DOM調用自身去處理每一個節點 */ var walk_the_DOM = function walk( node , func ) { func(node); node = node.firstChild; while (node) { walk( node , func ); node = node.nextSibling; } } /* 在定義一個getElementByAttribute函數 1) 它以一個屬性名稱字符串和一個可選的匹配值作為參數 2) 它調用walk_the_DOM,傳遞一個用來查找節點屬性名的函數作為參數,匹配得節點都會累加到一個數組中 */ var getElementsByAttribute=function(att,value){ var results=[]; walk_the_DOM(document.body,function(node){ var actual=node.nodeType===1&&node.getAttribute(att); if(typeof actual==='string' &&( actual===value|| typeof value!=='string')){ results.push(node); } }); return results; }
命名函數表達式和遞歸
遞歸問題
求階乘的函數:
function factorial(num){ if(num<=1){ return 1; }else{ return num*factorial(num-1); } }
通過將函數factorial設置為null,使原始函數的引用只剩一個, 此時factorial已不再是函數
arguments.callee實現遞歸
arguments.callee是一個指向正在執行的函數的指針,因此可以用它來實現對函數的遞歸調用
function factorial(num){ if(num<=1){ return 1; }else{ return num*arguments.callee(num-1); } } var anotherFactorial=factorial; factorial=null; anotherFactorial(3) //6
用arguments.callee代替函數名,可以確保無論怎樣調用函數都不會出問題。因此,在編寫遞歸函數時,使用arguments.callee總比使用函數名更保險。
但是在嚴格模式下,不能通過腳本訪問arguments.callee,訪問這個屬性會報錯
命名函數表達式實現遞歸
創建一個名為f()的命名函數表達式,然后賦值給factorial,即使把函數賦值給了另一個變量,函數的名字f仍然有效,所以遞歸調用照樣能正常完成。
這種方式在嚴格模式和非嚴格模式都可行。
var factorial =function f(num){ 'use strict' if(num<=1){ return 1; }else{ return num* f (num-1); } } factorial(3) //6 var anotherFactorial=factorial; factorial=null; anotherFactorial(3) //6
寫在后面
何不在別人去"堅持"的時間,試着讓自己去愛...因為喜愛,所以我們付出,但正是因為付出了,所以我們只能更愛.
大學生一枚才疏學淺,如有紕漏,還望前輩指正。