遞迴與迴圈
對於不同類型的需要重複計算的問題,迴圈和遞迴兩種方法各有所長,能給出更直觀簡單的方案。另一方面,迴圈和遞迴的方法可以互相轉換。任何一個迴圈的代碼都可以用遞迴改寫,實現相同的功能;反之亦然。在不失去其普遍性的前提下,可以把迴圈和遞迴分別用下列虛擬碼概括。
虛擬碼格式說明:迴圈採用while形式;變數不加定義;賦值用:=;條件運算式和執行的語句都寫成函數的形式,圓括弧內寫上相關的值。其他文法方面,盡量接近Javascript的規範。
[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 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;
}
同樣我們給出遞迴函式的虛擬碼。
[javascript]
//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;
}
//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;
}籍比較兩段代碼,可以看出迴圈和遞迴具有相似的構成,通過改變順序和適當的變換,任何迴圈都可以用遞迴的方式實現。程式簡單時,這種轉換很容易看出。比如下面這個簡單的累計求和函數:
[javascript]
//loop
function sum(num){
var result=1;
while (num>1){
result+=num;
num--;
}
return result;
}
//loop
function sum(num){
var result=1;
while (num>1){
result+=num;
num--;
}
return result;
}對應的遞迴形式:
[javascript]
//recursion
function sum2(num){
if (num>1){
return num+sum(num-1);
}else{
return 1;
}
}
//recursion
function sum2(num){
if (num>1){
return num+sum(num-1);
}else{
return 1;
}
}反之,大部分遞迴程式也可以直接由迴圈來實現。下面是求最大公約數的迴圈形式的函數。
[javascript]
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;
}
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在內的一些瀏覽器已經支援該方法。下面我們先用遞迴的方法給出一個功能較弱的版本,然後再用迴圈的方法重寫它。
[javascript]
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;
}
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對象支援前兩種方法,我們在為其增加第三個方法即可。
採用迴圈的版本:
[javascript]
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;
}
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); i<n-1
這樣,B(i)形成了一個有趣的逆的斐波那契數列。求A(n)時有:
B(i)=A(n+1-i)
換一個角度來看,令C(i)為求A(i)時需要的加法的次數,則有
C(i)=0; i=0, 1
C(i)=1+C(i-1)+C(i-1); i>1
令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)。
下面是採用儲存技術的求斐波那契數列的遞迴演算法。
[javascript]
//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);
}