聊聊js递归的使用及优化

764 阅读3分钟

这是我参与2022首次更文挑战的第17天,活动详情查看:2022首次更文挑战

介绍

本期我们开始讲述程序基础里比较常用的一个写法——递归,相信在解决一些算法问题和一些像素处理业务上经常会用到它,现在就给大家梳理一下,它的一些使用和优化方法。

概念

递归是一种函数调用自身的操作。递归被用于处理包含有更小的子问题的一类问题。

一个递归函数可以接受两个输入参数:一个最终状态(终止递归)或一个递归状态(继续递归)。

简而言之,调用自身的函数称之为递归递归函数。

事实上,递归本身使用了一个栈——函数栈。

function loop(n) {
    if (n < 0) return console.log("over");
    console.log("begin:", n);
    loop(n - 1);
    console.log("end:", n);
}
loop(5)

微信截图_20220202201008.png

根据打印可以看出它有着类似堆栈的行为——先进后出。

经典案例

阶乘算法

例如: 5! = 1 * 2 * 3 * 4 * 5 = 120, 0! = 1

function factorialize(n) {
    if (n === 1) return 1;
    return n * factorialize(n - 1);
}

斐波那契数列

满足F0=0,F1=1,Fn=Fn-1+Fn-2(n>=2,n∈N*)

function fibonacci(n) {
    if (n <= 1) return 1;
    return fibonacci(n - 2) + fibonacci(n - 1);
}

深度拷贝

在复杂对象里,拷贝多层对象。

function deepClone(obj1, obj2 = {}) {
    let toStr = Object.prototype.toString;
    let arrStr = toStr.call([]);
    for (let prop in obj1) {
        if (obj1.hasOwnProperty(prop)) {
            if (obj1[prop] !== null && typeof (obj1[prop]) == "object") {
                obj2[prop] = toStr.call(obj1[prop]) == arrStr ? [] : {};
                deepClone(obj1[prop], obj2[prop]);
            }
            else {
                obj2[prop] = obj1[prop];
            }
        }
    }
    return obj2;
}

这里只要发现下面有参数类型为对象就会在这个对象上递归再创建空对象进行拷贝直到下面的类型不是对象为止。这样所有对象的地址都与原对象无关了。

let obj1 = {
    bag: {
        banana: null,
        apple: {
            type: "fruit",
            num: 1,
        }
    },
    compass: true,
    map: {
        x: 5,
        y: 18
    }
}
let obj2 = {};
deepClone(obj1,obj2)
console.log(obj2)
console.log(obj2.bag.apple == obj1.bag.apple);

微信截图_20220202180746.png

尾递归优化

因为递归非常耗费内存,如果同时保存成千上百个调用帧,而递归产生的这些调用帧就形成了调用栈,虽然调用帧不断的增加,很容易发生“栈溢出”错误。这时候就应该考虑尾递归做其优化了。函数递归最后一步调用调用自身,称之为尾递归。它属于尾调用。

其目的是在函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

其特点为:

  • 位置在函数最后一步操作(不一定必须在尾部,保证最后一步)

  • 必须要出现return

  • 返回的必须是函数本身,不产生新的调用帧

我们做个例子,同样刚才那个斐波那契数列:

// 普通递归
function fibonacci(n) {
    if (n <= 1) return 1;
    return fibonacci(n - 2) + fibonacci(n - 1);
}
// 尾递归
function fibonacci2(n, f1 = 1, f2 = 1) {
    if (n <= 1) return 1
    return fibonacci2(n - 1, f2, f1 + f2)
}

这两种形式都是两行代码来实现,但消耗可是天差地别,下面我们使用他俩打印一下时间更加直观的看下。

console.time("普通递归-斐波那契")
console.log(fibonacci(36))
console.timeEnd("普通递归-斐波那契")
console.log("-------------")
console.time("尾递归-斐波那契")
console.log(fibonacci2(36))
console.timeEnd("尾递归-斐波那契")

微信截图_20220202194201.png

如果数值继续变大那么普通递归耗时就会指数级增长,内存开支巨大,甚至出现“栈溢出”。而用了尾递归的方式去优化,那么这个问题将迎刃而解。