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 (atemp=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
}
}
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
这样,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 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);
}

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

C++函数的递归深度受到限制,超过该限制会导致栈溢出错误。限制值因系统和编译器而异,通常在1000到10000之间。解决方法包括:1.尾递归优化;2.尾调用;3.迭代实现。

是的,C++Lambda表达式可以通过使用std::function支持递归:使用std::function捕获Lambda表达式的引用。通过捕获的引用,Lambda表达式可以递归调用自身。

给定两个字符串str_1和str_2。目标是使用递归过程计算字符串str1中子字符串str2的出现次数。递归函数是在其定义中调用自身的函数。如果str1是"Iknowthatyouknowthatiknow",str2是"know"出现次数为-3让我们通过示例来理解。例如输入str1="TPisTPareTPamTP",str2="TP";输出Countofoccurrencesofasubstringrecursi

递归算法通过函数自调用解决结构化的问题,优点是简洁易懂,缺点是效率较低且可能发生堆栈溢出;非递归算法通过显式管理堆栈数据结构避免递归,优点是效率更高且避免堆栈溢出,缺点是代码可能更复杂。选择递归或非递归取决于问题和实现的具体限制。

递归函数是一种在字符串处理中反复调用自身来解决问题的技术。它需要一个终止条件以防止无限递归。递归在字符串反转和回文检查等操作中被广泛使用。

递归是一种强大的技术,它允许函数调用自身来解决问题,在C++中,递归函数由两个关键要素构成:基本情况(确定递归何时停止)和递归调用(将问题分解为更小子问题)。通过理解基础知识并练习实战示例(如阶乘计算、斐波那契数列和二叉树遍历),您可以建立递归直觉,并自信地在代码中使用它。

尾递归优化(TRO)可提高特定递归调用的效率。它将尾递归调用转换为跳转指令,并将上下文状态保存在寄存器中,而不是堆栈上,从而消除对堆栈的额外调用和返回操作,提高算法效率。利用TRO,我们可以针对尾递归函数(例如阶乘计算)进行优化,通过将tail递归调用替换为goto语句,编译器会将goto跳转移化为TRO,优化递归算法的执行。

在Linux系统中,“ls”命令是一个非常有用的工具,它提供了对当前目录中文件和文件夹的简洁概述。通过“ls”命令,您可以快速查看文件和文件夹的权限、属性等重要信息。虽然“ls”命令是一个基本的命令,但是通过结合不同的子命令和选项,它可以成为系统管理员和用户的重要工具。通过熟练使用“ls”命令及其各种选项,您可以更高效地管理文件系统,快速定位所需文件,以及执行各种操作。因此,“ls”命令不仅可以帮助您了解当前目录结构,还可以提高您的工作效率。比如,在Linux系统中,通过使用带有递归选项的"ls
