JavaScript的递归之递归与循环示例介绍_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); 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);
}

时间: 2024-08-29 13:17:44

JavaScript的递归之递归与循环示例介绍_javascript技巧的相关文章

JavaScript四种调用模式和this示例介绍_javascript技巧

JavaScript调用时除了声明时定义的形参外,每个函数接受两个附加参数:this 和arguments,this在面向对象编程中非常重要,它取决于调用模式. JavaScript有四种调用模式,方法调用模式,函数调用模式,构造器调用模式和apply调用模式.这些模式在初始化关键参数this上存在差异. 方法调用模式:当一个函数被保存为对象的一个属性时,我们称它为一个方法,当一个方法被调用时,this被绑定到该对象上.如果调用表达式包含一个属性取表达式(即一个.点表达式或[script]下标表

javascript ready和load事件的区别示例介绍_javascript技巧

ready,是在DOM加载完成就触发.Jquery中 复制代码 代码如下: $(document).ready(function(){}) ; //或者 $().ready(function(){}); //或者 $(function(){}): load,是在加载完所有页面内容才会触发,所有内容包括图片,flash等.如果页面的这些内容很多会让用户等待很长时间.

javascript 循环调用示例介绍_javascript技巧

复制代码 代码如下: function checksdzt(){ sdzt = $("#viewObj_zt_text").val(); //循环调用,如果已经获取到了结果,则退出循环 loopgetinfo = setInterval("checksdztsub()",50); //这里循环调用,间隔50毫秒 } function checksdztsub(){ if ($("#viewObj_zt_text").val() !="&

JavaScript实现图片滑动切换的代码示例分享_javascript技巧

假设我们这里有1到5五张bmp图片,那么控制图片切换显示的核心代码可以为: <script> var i=1; var img = new Array(); img[0] = "1.bmp"; img[1] = "2.bmp"; img[2] = "3.bmp"; img[3] = "4.bmp"; img[4] = "5.bmp"; function playImg(){ i=i+1; var

JavaScript中for-in遍历方式示例介绍_javascript技巧

摘要:for-in遍历方式的循环计数器是字符串类型,遍历对象时为对象属性/方法名,遍历数组时为数组元素下标索引,与普通的for循环不同,for-in会将继承的属性/方法列出,这一点在使用时需要特别关注. 除了传统的for循环,JavaScript为遍历操作定义了for-in方式,根据数据源的不同,在使用时存在差异. (1)遍历对象: 复制代码 代码如下: var fish = { head : 1, tail : 1, } for(var prop in fish) { console.log(

JavaScript中cookie工具函数封装的示例代码_javascript技巧

一. 语法 1.1 获取当前页面的所有cookie: var allCookies = document.cookie; allCookies 是一个字符串,其中包含了以分号分隔的cookie列表字符串 (即 key=value 键值对). 1.2 写一个新cookie: document.cookie = updatedCookie; updatedCookie是一个键值对形式的字符串.只能用这个方法一次设置或更新一个cookie,而且写入并不是覆盖,而是添加.例如: document.coo

javascript获取下拉列表框当中的文本值示例代码_javascript技巧

近日碰到一个问题,就是需要将用户点击下拉列表当中某个选项后,将其所选的内容保存起来,例如下面的HTML代码: 复制代码 代码如下: <select onchange="isSelected(this.value);" id="city"> <option value="1">北京</option> <option value="2" >上海</option> <

JavaScript的null和undefined区别示例介绍_javascript技巧

先说说undefined: Javascript中的变量是弱类型的, 所以声明变量的时候只需使用var关键字即可.如果是像C这样的强类型语言, 声明变量的时候如果没有指定初始值,那么会给他一个默认值,比如int变量的默认值是0.但是在Javascript这样的弱类型语言中,没有办法确定到底该给这样的变量一个什么样的默认值,比如我声明一个变量 var v1; 是给他false还是0,或者是'' ? 因为没有类型,所以无法确定. 在Javascript中对于这种生命后没有给定初始值的变量,就给他一个

在JavaScript中判断整型的N种方法示例介绍_javascript技巧

整数类型(Integer)在JavaScript经常会导致一些奇怪的问题.在ECMAScript的规范中,他们只存在于概念中: 所有的数字都是浮点数,并且整数只是没有一组没有小数的数字. 在这篇博客中,我会解释如何去检查某个值是否为整型. ECMAScript 5 在ES5中有很多方法你可以使用.有时侯,你可能想用自己的方法:一个isInteger(x)的函数,如果是整型返回true,否则返回false. 让我们看看一些例子. 通过余数检查 你可以使用余数运算(%),将一个数字按1求余,看看余数