什么是递归
所谓递归,就是在程序中函数直接或间接地调用了自己,听起来好像会导致无限重复,但只要定义适当,就不会这样。一般来说,一个递归函数的定义有两个部分。首先,至少要有一个底线,就是一个临界条件,越过此处,则递归
那么如何写出递归呢,无非分以下几步:
- 写出递归公式
- 找到临界条件
举个🌰
Fibonacci数列
1 1 2 3 5 8 13 ... 求第n项
按照之前说的方法,首先,找出递归关系
f(n) = f(n - 1) + f(n - 2)
显然,临界条件为
f(1) = 1, f(2) = 1
最后得到方法
function fib(n){
if(n == 0 || n ==1) return 1;
return fib(n-1) + fib(n-2);
}
如上所示,能完成一个简单的递归函数,不过要写好递归还是需要注意很多东西的
递归的注意事项
爆栈
函数调用会使用栈来保存临时变量,而栈的数据结构为先进后出,每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成返回时才出栈。系统栈或者虚拟机栈空间一般都不大。如果递归求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险。
限制递归层级
如上述的fib(n)
函数,如果递归的层级过深,则会导致爆栈,为了防止爆栈,其一的方法就是限制其递归的层级,如下
let depth = 0
function fib(n){
++depth
if (depth > 1000) throw exception
if(n == 0 || n ==1) return 1;
return fib(n-1) + fib(n-2);
}
尾递归优化
另外一种方法则是尾递归优化以上述fib(n)
函数为例,加入两个临时变量来存储得到的值
function fib(n, ac1 = 0, ac2 = 1){
if(n == 0) {
return ac1
} else {
return (n - 1, ac2 , ac1 + ac2)
}
}
但尾递归优化的提案并没有完全通过,而且尾递归优化存在下述两个问题
- 在引擎层面进行尾调优化是一个隐式行为,如果代码存在死循环尾递归调用,可能因为优化后没有爆栈报错提示而无法被开发者察觉
- 优化后,调用堆栈信息会丢失,造成调试困难
Trampolining(蹦床函数)
在没有对尾递归优化支持的情况下,可以使用蹦床函数替代,什么是蹦床函数呢,举个🌰
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}
trampoline 方法中,如果 f 是个函数就一直调用到返回不是函数为止,注意这种方式不是递归调用,而是循环,不会增加调用栈。
我们尝试把上边的例子改写成使用 Trampolining
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}
function fTrampoline (n, ac1=0, ac2=1){
if(n===0){
return ac1
}else{
return fTrampoline.bind(null, n-1, ac2, ac1+ac2)
}
}
// 结合两个函数进行调用
trampoline(fTrampoline(10000))
重复计算
还是上面的那个🌰,fib(n -1) + fib(n -2)
由这个递归公式不难看出f(5) = f(4) + f(3)
,而f(4) = f(3) + f(2)
由此可见,会出现很多类似于f(3)
这样的重复计算,那么如何避免此类的重复计算呢,就JS而言,可以使用set weakMap
此类数据结构,判断数据结构中是否存在,如果有直接从散列表中取值返回,不需要重复计算,这就避免了重复计算问题。 具体代码如下:
let mapData = new Map()
function fib(n){
if(n == 0 || n ==1) return 1;
if(mapData.get(n)){
return mapData.get(n);
}
let value = fib(n-1) + fib(n-2)
mapData.set(n, value)
return value
}
递归算法实战
1.实现深拷贝
function deepClone(target, map = new WeakMap()) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
for (const key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = deepClone(target[key], map);
}
}
return cloneTarget;
} else {
return target;
}
}
2.数组扁平化
function flatten(arr) {
let res = []
arr.forEach((item, index) => {
Array.isArray(item) ? res = res.concat(flatten(item)) : res.push(item)
})
return res
}
如上就实现了一个简单的数组扁平化的方法,现在我们拓展一下,能否实现其扁平化的深度呢,实现如下效果,举个🌰
flatten([1,[2,3,[4,5]]], 2) ---> [1,2,3,[4,5]]
function flatten(arr, depth = 0) {
let res = []
let d = 0
++d
arr.forEach((item, index) => {
(Array.isArray(item) && d <= depth) ? res = res.concat(flatten(item)) : res.push(item)
})
return res
}
3.使用递归实现getElementsByClassName
let arr = [];
function byClass(node, className, arr){
//得到传入节点的所有子节点
var lists = node.childNodes;
for(var i = 0;i< lists.length;i++){
//判断是否有相同className元素
if(arr[i],className == className){
arr.push(arr[i]);
}
//判断子节点是否还有子节点
if(arr[i].childNodes.length > 0){
byClass(arr[i],className,arr);
}
}
}
总结
其实递归被广泛的应用于各个地方,如前端er常用的Vue中Vnode的diff也是通过递归来实现的,以上就是我对递归思想的一个简单总结,2019最后一天,那就提前祝大家新的一年bug-free
吧