JavaScript的递归之递归与循环示例介绍


Posted in Javascript onAugust 05, 2013

递归与循环

对于不同类型的需要重复计算的问题,循环和递归两种方法各有所长,能给出更直观简单的方案。另一方面,循环和递归的方法可以互相转换。任何一个循环的代码都可以用递归改写,实现相同的功能;反之亦然。在不失去其普遍性的前提下,可以把循环和递归分别用下列伪代码概括。

伪代码格式说明:循环采用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); 
}
Javascript 相关文章推荐
锋利的jQuery 要点归纳(三) jQuery中的事件和动画(下:动画篇)
Mar 24 Javascript
JavaScript随机排序(随即出牌)
Sep 17 Javascript
你必须知道的Javascript知识点之&quot;单线程事件驱动&quot;的使用
Apr 23 Javascript
js使用eval解析json实例与注意事项分享
Jan 18 Javascript
jQuery UI Draggable + Sortable 结合使用(实例讲解)
Sep 07 jQuery
vue2.0安装style/css loader的方法
Mar 14 Javascript
Vue实现点击显示不同图片的效果
Aug 10 Javascript
vue路由缓存的几种实现方式小结
Feb 02 Javascript
OpenLayers3实现对地图的基本操作
Sep 28 Javascript
vue实现列表拖拽排序的功能
Nov 02 Javascript
微信小程序实现简单的select下拉框
Nov 23 Javascript
nestjs中异常过滤器Exceptionfilter的具体使用
Feb 07 Javascript
php与js的区别是什么
Aug 05 #Javascript
IE下写xml文件的两种方式(fso/saveAs)
Aug 05 #Javascript
js获取input标签的输入值实现代码
Aug 05 #Javascript
js实现页面转发功能示例代码
Aug 05 #Javascript
Jquery:ajax实现翻页无刷新功能代码
Aug 05 #Javascript
jquery js 重置表单 reset()具体实现代码
Aug 05 #Javascript
JS幻灯片可循环播放可平滑旋转带滚动导航(自写)
Aug 05 #Javascript
You might like
php数组函数序列之array_splice() - 在数组任意位置插入元素
2011/11/07 PHP
异步加载技术实现当滚动条到最底部的瀑布流效果
2014/09/16 PHP
php支付宝接口用法分析
2015/01/04 PHP
PHP消息队列用法实例分析
2016/02/12 PHP
php同时使用session和cookie来保存用户登录信息的实现代码
2016/05/13 PHP
php 解析xml 的四种方法详细介绍
2016/10/26 PHP
JQuery的ajax获取数据后的处理总结(html,xml,json)
2010/07/14 Javascript
javascript中的作用域scope介绍
2010/12/28 Javascript
自己封装的javascript事件队列函数版
2014/06/12 Javascript
jQuery级联操作绑定事件实例
2014/09/02 Javascript
理解javascript中的严格模式
2016/02/01 Javascript
jQuery AjaxUpload 上传图片代码
2016/02/02 Javascript
微信小程序 数组中的push与concat的区别
2017/01/05 Javascript
Angular2中如何使用ngx-translate进行国际化
2017/05/21 Javascript
运用jQuery写的验证表单(实例讲解)
2017/07/06 jQuery
js 显示日期时间的实例(时间过一秒加1)
2017/10/25 Javascript
jQuery超简单遮罩层实现方法示例
2018/09/06 jQuery
js实现无缝轮播图
2020/03/09 Javascript
谈谈JavaScript中的函数
2020/09/08 Javascript
[42:20]2014 DOTA2华西杯精英邀请赛5 24 DK VS NewBee
2014/05/25 DOTA
Python数据结构与算法之图的最短路径(Dijkstra算法)完整实例
2017/12/12 Python
Python 利用内置set函数对字符串和列表进行去重的方法
2018/06/29 Python
python最小生成树kruskal与prim算法详解
2019/01/17 Python
python2 中 unicode 和 str 之间的转换及与python3 str 的区别
2019/07/25 Python
python系列 文件操作的代码
2019/10/06 Python
python文字转语音的实例代码分析
2019/11/12 Python
HTML5 Canvas 绘图——使用 Canvas 绘制图形图文教程 使用html5 canvas 绘制精美的图
2015/08/31 HTML / CSS
Blancsom美国/加拿大:服装和生活用品供应商
2018/07/27 全球购物
Watch Station官方网站:世界一流的手表和智能手表
2020/01/05 全球购物
Linux如何为某个操作添加别名
2013/03/01 面试题
大学毕业生最详细的自我评价分享
2013/11/18 职场文书
3的组成教学反思
2014/04/30 职场文书
七一党日活动总结
2014/07/08 职场文书
报表员工作失误检讨书范文
2014/09/19 职场文书
python 遍历磁盘目录的三种方法
2021/04/02 Python
Python采集股票数据并制作可视化柱状图
2022/04/04 Python