![](https://i-blog.csdnimg.cn/blog_migrate/b80fa8e6715f9385cea902c350bc64f8.png)
介绍
使用递归可以更自然地解决一些问题。 例如,像Fibonacci序列这样的序列具有递归定义。 序列中的每个数字都是序列中前两个数字的和。 要求您建立或遍历树状数据结构的问题也可以通过递归解决。 训练自己进行递归思考将为您提供解决此类问题的强大技能。
在本教程中,我将逐步介绍几个递归函数,以了解它们的工作原理,并向您展示可用于系统定义递归函数的技术。
内容:
- 什么是递归?
- 带数字递归
- 列表递归
- 建筑清单
- 评论
什么是递归?
递归定义的函数是根据其自身的简单版本定义的函数。 这是一个简化的示例:
function doA(n) {
...
doA(n-1);
}
为了理解递归在概念上是如何工作的,我们将看一个与代码无关的示例。 想象一下,您有责任在工作中接听电话。 由于这家公司很忙,您的电话有多条电话线,因此您可以同时处理多个电话。 每条电话线都是接收器上的一个按钮,当有来电时,该按钮将闪烁。 今天,当您上班并打开手机电源时,有四条线路同时闪烁。 这样您就可以接听所有电话。
您选择第一行,并告诉他们“请稍等”。 然后,您拿起第二行并将其保留。 接下来,您选择第3行并将其保留。 最后,您在第四行接听电话并与呼叫者通话。 当第四个呼叫者结束时,您会挂断并接听第三个呼叫。 当您完成第三个电话时,您会挂断并接听第二个电话。 完成第二个呼叫后,您会挂断并接听第一个电话。 结束通话后,您终于可以放下电话了。
此示例中的每个电话呼叫都类似于函数中的递归呼叫。 接到电话后,它将被放入调用堆栈中(用代码说)。 如果您无法立即完成通话,则将通话置于保持状态。 如果您有无法立即求值的函数调用,它将保留在调用堆栈中。 当您能够接听电话时,便接听电话。 当您的代码能够评估函数调用时,它就会从堆栈中弹出。 在看下面的代码示例时,请记住这个类比。
带数字递归
所有递归函数都需要基本情况,因此它们将终止。 但是,仅将基本情况添加到我们的函数中并不能阻止它无限运行。 该功能必须采取步骤使我们更接近基本情况。 最后是递归步骤。 在递归步骤中,问题被简化为问题的较小版本。
假设您有一个将1到n的数字相加的函数。 例如,如果n = 4,则总和为1 + 2 + 3 + 4。
首先,我们确定基本情况。 找到基本案例也可以认为是找到无需递归即可解决问题的案例。 在这种情况下,n等于零。 零没有任何部分,因此递归可以在达到0时停止。
在每一步中,您将从当前数字中减去一个。 什么是递归情况? 递归情况是用减少的数字调用的函数和。
function sum(num){
if (num === 0) {
return 0;
} else {
return num + sum(--num)
}
}
sum(4); //10
这是每个步骤正在发生的事情:
- 转到sum(4)。
- 4等于0吗? 否。将sum(4)保留,然后转到sum(3)。
- 3等于0吗? 否。搁置sum(3)并转到sum(2)。
- 2等于0吗? 否。搁置sum(2)并转到sum(1)。
- 1等于0吗? 否。搁置sum(1)并转到sum(0)。
- 0等于0吗? 是。 评估总和(0)。
- 拿起sum(1)。
- 拿起sum(2)。
- 拿起sum(3)。
- 拿起sum(4)。
这是查看函数如何处理每个调用的另一种方式:
sum(4)
4 + sum(3)
4 + ( 3 + sum(2) )
4 + ( 3 + ( 2 + sum(1) ))
4 + ( 3 + ( 2 + ( 1 + sum(0) )))
4 + ( 3 + ( 2 + ( 1 + 0 ) ))
4 + ( 3 + ( 2 + 1 ) )
4 + ( 3 + 3 )
4 + 6
10
在递归情况下,该参数应发生变化,并使您更接近基本情况。 此参数应在基本情况下进行测试。 在前面的示例中,由于在递归情况下要减去1,因此我们在基本情况下测试参数是否等于零。
任务
- 使用循环而不是递归来实现sum函数。
- 创建一个将两个数字递归相乘的函数。 例如,
multiply(2,4)
将返回8。为multiply(2,4)
编写每一步所发生的事情。
列表递归
在列表上重复执行与在编号上重复执行类似,不同之处在于,我们不减少每一步的数目,而是减少每一步的数目,直到得到一个空列表,而不是减少每一步的数目。
考虑以列表为输入并返回列表中所有元素之和的sum函数。 这是函数sum的实现:
function sum(l){
if (empty(l)) {
return 0;
} else {
return car(l) + sum(cdr(l));
}
}
如果列表没有元素,则empty
函数返回true。 car
函数返回列表中的第一个元素。 例如, car([1,2,3,4])
返回cdr
函数返回不包含第一个元素的列表。 例如, cdr([1,2,3,4])
返回[2,3,4]。 当我们执行sum([1,2,3,4])
会发生什么?
sum([1,2,3,4])
1 + sum([2,3,4])
1 + ( 2 + sum([3,4]) )
1 + ( 2 + ( 3 + sum([4]) ))
1 + ( 2 + ( 3 + ( 4 + sum([]) )))
1 + ( 2 + ( 3 + ( 4 + 0 ) ))
1 + ( 2 + ( 3 + 4 ) )
1 + ( 2 + 7 )
1 + 9
10
当重复出现在列表上时,检查它是否为空。 否则,对列表的精简版本执行递归步骤。
任务
- 重写此求和函数,以便它使用循环对列表中的每个项目求和,而不是递归。
- 定义一个名为length的函数,该函数将一个列表作为输入并返回该列表中的元素数。 您不应该使用JavaScript的内置长度函数。 例如,
length(['a', 'b', 'c', 'd'])
应该返回4。写出每个步骤发生的情况。
建筑清单
在最后一个示例中,我们返回一个数字。 但是假设我们想返回一个列表。 这意味着,除了向递归步骤添加数字之外,我们还需要添加列表。 考虑函数remove
,该函数将一个项目和一个列表作为输入,并返回移除了该项目的列表。 仅第一个找到的项目将被删除。
function remove(item, l){
if (empty(l)) {
return [];
} else if (eq(car(l), item)) {
return cdr(l);
} else {
return cons(car(l), remove(item, cdr(l)));
}
}
remove('c', ['a', 'b', 'c', 'd']) //[‘a’, ‘b’, ‘d’]
在此,如果两个输入相同,则eq
函数返回true。 cons
函数将一个元素和一个列表作为输入,并返回一个新列表,并将该元素添加到其开头。
我们将检查列表中的第一项是否等于我们要删除的项。 如果是,请从列表中删除第一个元素,然后返回新列表。 如果第一项与我们要删除的项不相等,我们将列表中的第一个元素添加到递归步骤中。 递归步骤将包含删除了第一个元素的列表。
我们将继续删除元素,直到达到基本情况为止,这是一个空列表。 空列表表示我们遍历了列表中的所有项目。 remove('c', ['a', 'b', 'c', 'd'])
什么作用?
remove('c', ['a', 'b', 'c', 'd'])
cons( ‘a’, remove(‘c’, [‘b’, ‘c’, ‘d’]) )
cons( ‘a’ , cons( ‘b’, remove(‘c’, [‘c’, ‘d’]) ))
cons( ‘a’, cons( ‘b’, [‘d’] )
cons( ‘a’, [‘b’, ‘d’])
[‘a’, ‘b’, ‘d’]
在需要构建列表的情况下,我们采用第一个元素并将其添加到列表的递归部分。
任务
- 重写remove函数,以便它使用循环而不是递归从列表中删除元素。
- 更改删除功能,以便从列表中删除所有出现的项目。 例如,
remove('c', ['a', 'b', 'c', 'd', 'c']
返回['a','b','d']。逐步写出发生的情况步。
评论
递归函数包括三个部分。 首先是基本情况,这是终止条件。 第二步是使我们更接近基本情况的步骤。 第三步是递归步骤,其中函数使用减少的输入来调用自身。
递归就像迭代。 您可以递归定义的任何函数也可以使用循环来定义。 使用递归时要考虑的其他事项是在嵌套列表上重现并优化递归调用。
《小计划者 》一书是继续学习递归的重要资源。 它教您如何使用问答格式进行递归思考。
翻译自: https://code.tutsplus.com/tutorials/understanding-recursion-with-javascript--cms-30346