對於不同類型的需要重複計算的問題,循環和遞歸兩種方法各有所長,能給出更直觀簡單的方案,下面為大家詳細的介紹下JavaScript的遞歸與循環,有興趣的朋友可以了解下
遞歸與循環
對於不同類型的需要重複計算的問題,循環和遞歸兩種方法各有所長,能給出更直觀簡單的方案。另一方面,循環和遞歸的方法可以互相轉換。任何一個循環的程式碼都可以用遞歸改寫,實現相同的功能;反之亦然。在不失去其普遍性的前提下,可以把循環和遞歸分別用下列偽代碼概括。
偽代碼格式說明:迴圈採用while形式;變數不加定義;賦值用:=;條件式和執行的語句都寫成函數的形式,圓括號內寫上相關的值。其他語法方面,盡量接近Javascript的規格。
程式碼如下:
//pseudo code of a loop //while形式 function loop(arguments){ //结果的初始值 result:=initial_value; while(condition(variable, arguments)){//循环条件,可能只需arguments,也可能为了方便引入循环变量 //计算结果。参数包括之前的结果、当前循环变量和外部变量 result:=calculate(result, variable, extern_variables); //影响函数的外部环境,即修改外部变量 changeStatus(result, variable, extern_variables); //执行完循环体中的语句后,修改参数或循环变量。 modify_arguments_variable(arguments, variable); } //返回结果 return result; }
同樣我們給出遞歸函數的偽代碼。
代碼如下:
//pseudo code of a recursion function recursion(arguments){ //以下代码为控制函数重复调用的结构部分。 //获得再次调用此函数的新的参数,可能为多组arguments值。 //对应于循环中的condition(variable, arguments)和modify_arguments_variable(arguments, variable)。 new_arguments:=conditional_get_next(arguments); //对新参数的每一组,调用函数自身。 results:=recursion(new_arguments); //以下的代码为每次调用都运行的功能部分 //计算结果。涉及到之前的结果、当前循环变量和外部变量。 //对应于循环中的result:=calculate(result, variable, extern_variables)。 result:=calculate(arguments, extern_variables); result:=combine(result, results); //影响函数的外部环境,即修改外部变量 changeStatus(result, arguments, extern_variables); return result; }
籍比較兩段代碼,可以看出循環和遞歸具有相似的構成,透過改變順序和適當的變換,任何循環都可以用遞歸的方式實現。程序簡單時,這種轉換很容易看出。例如下面這個簡單的累計求和函數:
程式碼如下:
//loop function sum(num){ var result=1; while (num>1){ result+=num; num--; } return result; }
對應的遞歸形式:
程式碼如下:
//recursion function sum2(num){ if (num>1){ return num+sum(num-1); }else{ return 1; } }
反之,大部分遞歸程式也可以直接由迴圈來實現。下面是求最大公約數的循環形式的函數。
程式碼如下:
function gcd2(a, b){ var temp; if (a<b){ temp=a; a=b; b=temp; } var c=a%b; while (c!==0){ a=b; b=c; c=a%b; } return b; }
不過,從遞歸到循環的轉換並不是都這麼容易。遞歸偽程式碼中的產生再次呼叫此函數的新參數部分
new_arguments:=conditional_get_next(arguments);
較之循環的對應部分更為靈活。可以依照新產生的參數組數(函數所需的所有參數為一組)將遞歸分為兩類。第一類為參數組數固定,此遞歸可以轉換為循環,例如斐波那契數列和最大公約數的例子;第二類為參數組數不確定——就像在遍歷一個圖或樹的時候那樣,每個點都有任意個相鄰的點-該遞迴不能直接轉換為迴圈。
因為循環只能做一維的重複,而遞歸可以遍歷二維的結構。例如一棵樹中,一個節點既有它的子節點,也有同級的節點,簡單的一維循環不能夠在兩個方向上遍歷。
但是如果我們在循環中藉助某種資料結構記住有關節點位置的一些信息,第二類遞歸也可以用循環實現。
我們再透過一個例子來實踐上面觀察得出的結論。 HTML5為Document和Element新定義了一個方法getElementsByClassName(names),傳回具有給定class值的所有elements。包括Firefox3在內的一些瀏覽器已經支援此方法。下面我們先用遞歸的方法給一個功能較弱的版本,然後再用循環的方法重寫它。
程式碼如下:
var getElementsByClass={}; //elem为一个HTMLElement //name为单个class名 //返回包含elem下所有class属性包含给定名称的element的数组 getElementsByClass.recursion1=function (elem, name){ var list=[]; function getElements(el){ if (el.className.split(' ').indexOf(name)>-1){ list.push(el); } for (var i=0, c=el.children; i<c.length; i++){ getElements(c[i]); } } getElements(elem); return list; }
如前所述,在循環中為了記住節點的位置信息,我們需要一個能實現以下方法的資料結構。
push(object) //寫入一個物件。
objectpop() //讀出最近寫入的一個對象,並將其從資料結構中刪除。
objectget() //讀出最近寫入的一個對象,不改變資料結構的內容。
堆疊正是這樣一個後進先出的資料結構。 Javascript中的Array物件支援前兩種方法,我們在為其增加第三個方法即可。
採用循環的版本:
程式碼如下:
getElementsByClass.loop1 = function(elem, name){ //use a js array as the basis of a needed stack var stack = []; stack.get = function(){ return stack[stack.length - 1]; } var list = []; //the business logic part. put the eligible element to the list. function testElem(el){ if (el.className.split(' ').indexOf(name) > -1) { list.push(el); } } //check the root element testElem(elem); //initialize the stack stack.push({ pointer: elem, num: 0 }); var parent, num, el; while (true) { parent = stack.get(); el = parent.pointer.children[parent.num]; if (el) {//enter a deeper layer of the tree testElem(el); stack.push({ pointer: el, num: 0 }); } else {//return to the upper layer if (stack.pop().pointer === elem) { break; } else { stack.get().num += 1; } } } return list; }
归纳起来。所有循环都可以用递归实现;所有递归都可以用循环实现。采用哪种方法,由具体问题下哪种思路更方便直观和使用者的喜好决定。
效率
性能方面,递归不比循环有优势。除了多次函数调用的开销,在某些情况下,递归还会带来不必要的重复计算。以计算斐波那契数列的递归程序为例。求第n项A(n)时,从第n-2项起,每一项都被重复计算。项数越小,重复的次数越多。令B(i)为第i项被计算的次数,则有
B(i)=1; i=n, n-1
B(i)=B(i+1)+B(i+2); ia37026b3b3430a8f991b4321a39b15391
令D(i)=C(i)+1,有
D(i)=1; i=0, 1
D(i)=D(i-1)+D(i-1)
所以D(i)又形成一个斐波那契数列。并可因此得出:
C(n)=A(n+1)-1
而A(n)是以几何级数增长,这种多余的重复在n较大时会变得十分惊人。与之相对应的采用循环的程序,有
B(n)=1; n为任意值
C(n)=0; n=0, 1
C(n)=n-1; n>1
因而当n较大时,前面给出的采用循环的程序会比采用递归的程序快很多。
如上一节中的循环一样,递归中的这个缺陷也是可以弥补的。我们只需要记住已经计算出来的项,求较高项时,就可以直接读取以前的项。这种技术在递归中很普遍,被称为“存储”(memorization)。
下面是采用存储技术的求斐波那契数列的递归算法。
代码如下:
//recursion with memorization function fibonacci4(n){ var memory = []; //used to store each calculated item function calc(n){ var result, p, q; if (n < 2) { memory[n] = n; return n; } else { p = memory[n - 1] ? memory[n - 1] : calc(n - 1); q = memory[n - 2] ? memory[n - 2] : calc(n - 2); result = p + q; memory[n] = result; return result; } } return calc(n); }
更多JavaScript的递归之递归与循环示例介绍相关文章请关注PHP中文网!