JavaScript递归与循环的相互转换及性能优化

作者:沉舟时间:2026-10-28 02:59:31浏览:396

本文深入探讨JavaScript中递归与循环的相互转换机制。通过伪代码分析两者结构差异,指出递归在遍历二维结构时的优势及循环在简单重复中的直观性。重点分析递归性能瓶颈,引入记忆化(Memorization)技术优化斐波那契数列计算,并提供基于栈结构的循环实现方案,帮助开发者根据场景选择最优算法。

递归与循环

循环与递归的伪代码结构分析

针对需要重复计算的不同类型问题,循环和递归各有优势,能提供更直观简洁的解决方案。此外,这两种方法可以相互转换:任何循环代码均可改写为递归以达成相同功能,反之亦然。在不丧失通用性的前提下,可将循环和递归分别概括为以下伪代码形式。



伪代码规范说明:循环采用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); 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)。



下面是采用存储技术的求斐波那契数列的递归算法。


代码如下:

 
//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);
} 
作者最新文章