递归改循环方案,栈溢出的解决方案,用循环遍历未知深度的树形结构

递归改循环方案,用循环遍历未知深度的树形结构

在很多时候,我们在遍历的时候会选择用递归来实现,比如深拷贝两个对象,拍平数组啥的,大多数情况下,我们的栈的深度不会太深,使用递归并没用什么问题。

下面,我们来讨论下特殊场景下,如何用循环来代替递归。

递归的一些优缺点

缺点: 1.递归在不同的浏览器,栈溢出的极限值不一样,谷歌78达到3万多,ie11还不到1万
缺点: 2.递归要保存函数的很多信息,理论上来说,空间上是要占用更多的储存空间,执行效率上是会略慢一些(没有实测)
优点: 递归更加通俗易懂(相比较改循环而言)

查看当前浏览器调用栈的长度可以用以下代码

(()=>{
    let maxStackNumber = 0;
    function test() {
        return test(maxStackNumber++)
    }
    try {
        test()
    } catch (err) {
        console.log(maxStackNumber)
    }
})()

如何用递归遍历未知深度的树形结构

我们以实现一个“简单”的深拷贝代码为例(实际业务中会考虑很多类型,本文的重点不在深拷贝如何实现上,在这里我们只考虑最简单的)

(function () {
    function isObj(o) {
        return Object.prototype.toString.call(o)==="[object Object]"
    }
    function isArray(o) {
        return Object.prototype.toString.call(o)==="[object Array]"
    }
    function assign(target,obj) {
        for(let key in obj){
            if(Object.hasOwnProperty.call(obj,key)){
                if(isObj(obj[key])){
                    target[key]=target[key]||{};
                    assign(target[key],obj[key])
                }else if(isArray(obj[key])){
                    target[key]=target[key]||[];
                    assign(target[key],obj[key])
                }else {
                    target[key]=obj[key]
                }
            }
        }
        return target
    }
    const obj1={a:1,b:{c:2}};
    const obj2={a:2,b:{d:3}};
    return assign(obj1,obj2);
})();

如何将上述递归改造成循环?

递归改循环方案

栈中信息模拟

栈中保存函数的信息,如参数,返回值,作用域,函数体

根据具体的使用场景来选择保存哪些信息,这里以深拷贝为例,我们保存以下函数信息

  1. 参数
  2. 函数体中遍历的键集合(因为我们的for循环可能会在中间的时候暂停,进入下一级循环,所以在这里保存遍历的键集合)
  3. 函数体中遍历的键的索引(配合上面的键集合来使用)
class FnData{
    setArgs(...args){//存储参数集合
        this.args=args;
    }
    setKeys(keys){//存储当前对象的键集合
        this.keys=keys
    }
    setIndex(index){//存储遍历位置
        this.index=index
    }
}

进栈出栈模拟

我们使用递归会自动进栈,自动出栈,所以我们需要模拟同样的行为,但for循环是从上至下执行,而递归是从外往内执行,这其实是有本质上的区别,
但我们可以用跳转来实现同样的效果,类似与goto语句,但js没有goto语句,幸运的是这不重要,我们可以用标签来模拟。

function assign(target,obj) {
    //新建是一个栈集合
    const stack = new Array(0);
    //存储上述递归函数的相关信息
    let fnData = new FnData();
    fnData.setIndex(0);
    fnData.setArgs(target,obj);
    fnData.setKeys(Object.keys(obj));
    //将信息推入栈
    stack.push(fnData);
    //while加标签,模拟goto,只要栈内有信息,则一直执行
    outside:
    while (stack.length>0){
        //取出栈顶元素
        let curFnData = stack[stack.length-1];
        let [_target,_obj]=curFnData.args;
        let keys = curFnData.keys;
        //取出栈中遍历的索引,从上次没执行完的地方继续执行
        for(let i=curFnData.index;i<keys.length;i++){
            let key = keys[i];
            //设置当前键已经遍历过,索引后移
            curFnData.setIndex(i+1);
            if(Object.prototype.hasOwnProperty.call(_obj,key)){
                if(isObj(_obj[key])||isArray(_obj[key])){
                    _target[key]=_target[key]||(isArray(_obj[key])?[]:{});
                    //如果是对象,表明我们要进入下一层了,保存所需要的信息
                    let _fnData = new FnData();
                    _fnData.setIndex(0);
                    _fnData.setArgs(_target[key],_obj[key]);
                    _fnData.setKeys(Object.keys(_obj[key]));
                    //入栈
                    stack.push(_fnData);
                    /*
                    * 核心是这个
                    * 我们将下一层的信息推入栈中后,会中断当前的遍历,强制跳转至外层while循环,此时循环体会重新执行,
                    * 而栈顶的信息已经改变,变成了子级,等到子级遍历完毕出栈后,才会继续当前的遍历,
                    * 此时因为我们之前已经保存了索引信息,所以我们可以直接从索引位置开始
                    * */
                    continue outside
                }else {
                    _target[key]=_obj[key]
                }
            }
        }
        //出栈,相当于从子级往外跳一层
        stack.pop()
    }
    return target
}

完整代码

(function () {
    function isObj(o) {
        return Object.prototype.toString.call(o)==="[object Object]"
    }
    function isArray(o) {
        return Object.prototype.toString.call(o)==="[object Array]"
    }
    class FnData{
        setArgs(...args){//存储参数集合
            this.args=args;
        }
        setKeys(keys){//存储当前对象的键集合
            this.keys=keys
        }
        setIndex(index){//存储遍历位置
            this.index=index
        }
    }
    function assign(target,obj) {
        //新建是一个栈集合
        const stack = new Array(0);
        //存储上述递归函数的相关信息
        let fnData = new FnData();
        fnData.setIndex(0);
        fnData.setArgs(target,obj);
        fnData.setKeys(Object.keys(obj));
        //将信息推入栈
        stack.push(fnData);
        //while加标签,模拟goto,只要栈内有信息,则一直执行
        outside:
        while (stack.length>0){
            //取出栈顶元素
            let curFnData = stack[stack.length-1];
            let [_target,_obj]=curFnData.args;
            let keys = curFnData.keys;
            //取出栈中遍历的索引,从上次没执行完的地方继续执行
            for(let i=curFnData.index;i<keys.length;i++){
                let key = keys[i];
                //设置当前键已经遍历过,索引后移
                curFnData.setIndex(i+1);
                if(Object.prototype.hasOwnProperty.call(_obj,key)){
                    if(isObj(_obj[key])||isArray(_obj[key])){
                        _target[key]=_target[key]||(isArray(_obj[key])?[]:{});
                        //如果是对象,表明我们要进入下一层了,保存所需要的信息
                        let _fnData = new FnData();
                        _fnData.setIndex(0);
                        _fnData.setArgs(_target[key],_obj[key]);
                        _fnData.setKeys(Object.keys(_obj[key]));
                        //入栈
                        stack.push(_fnData);
                        /*
                        * 核心是这个
                        * 我们将下一层的信息推入栈中后,会中断当前的遍历,强制跳转至外层while循环,此时循环体会重新执行,
                        * 而栈顶的信息已经改变,变成了子级,等到子级遍历完毕出栈后,才会继续当前的遍历,
                        * 此时因为我们之前已经保存了索引信息,所以我们可以直接从索引位置开始
                        * */
                        continue outside
                    }else {
                        _target[key]=_obj[key]
                    }
                }
            }
            //出栈,相当于从子级往外跳一层
            stack.pop()
        }
        return target
    }
    const obj1={a:1,b:{c:2}};
    const obj2={a:2,b:{d:3}};
    return assign(obj1,obj2);
})();

性能

今天太晚了,改天测试下

总结

一般情况下,我们还是使用递归为佳,因为改循环的方案,就像使用goto一样难受,很对语言不支持goto是有原因的,太难受,跳来跳去很绕,
但如果我们有层级很深或者追求性能的时候,可以尝试将递归改为循环,你就可以跳出系统的限制,自己决定栈的深度。

结束语:如果遇到了问题,欢迎在评论区交流,如果觉得不错,可以点赞和收藏,持续更新。

博客中标注原创的文章,版权归原作者 苦中作乐才是人生巅峰所有;转载或者引用本文内容请注明来源及原作者;

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值