直接上例子,回顾下迭代和递归:

问题目标:老板吃上冰淇凌

1. 递归的方式("懒惰"的老板):
1) 老板想吃冰淇淋,仅需传达给秘书;(对于老板而言,去哪里买不需要关心,秘书可以解决
2) 秘书接到任务,跑去肯德基,告诉店员要一个冰淇淋;(对秘书来说,仅需传达给店员,不需要知道怎么做;店员会把想要的给他)
3) 店员接到需求,生产出冰淇凌,交给秘书;(问题最小单元触发,终止问题分解;向上反馈
4) 秘书拿到了冰淇凌,用礼品盒小心翼翼的装好后,跑到公司交给老板;(秘书得到结果,经过包装后,向上反馈
5) 老板拿到了冰淇凌,开心的吃了起来。(老板坐在办公室一动没动,得到了想要的东西

每一层仅需关注从能帮自己解决问题的对接人那里得到反馈即可,直到真正解决问题的人终止问题分包,通过【传达链】向上反馈。

对应代码描述:

getIceCream = (roles, index) => {
    if(index === 2) {
        console.log(`${roles[index]}生产出冰淇凌!`);
        return '冰淇凌';
    }else {
        console.log(`${roles[index]}对${roles[index+1]}说:给我冰淇凌!`);
        let iceCream = getIceCream(roles, index+1);
        console.log(`${roles[index]}得到了冰淇凌!`);
        return iceCream;
    }
}
//这里的店员生产冰淇凌即是问题终结条件:index === 2
roles = ['老板','秘书','店员'];
getIceCream(roles, 0);

//output
老板对秘书说:给我冰淇凌!
秘书对店员说:给我冰淇凌!
店员生产出冰淇凌!
秘书得到了冰淇凌!
老板得到了冰淇凌!

2. 迭代的方式(亲力亲为的老板):
1) 老板想吃冰淇淋,问了秘书怎么可以吃到;(对于老板而言,秘书应该能回到自己的问题
2) 秘书把老板指向肯德基店;(对于老板而言,知道了去肯德基能买到冰淇凌的线索;对秘书而言,完成了"重定向",任务就结束了
3) 老板对店员说,给我冰淇凌;(老板把同样的问题问了一遍店员;相同逻辑的重复调用
4) 店员把冰淇凌递给老板;老板得到了想要的冰淇凌。(店员的任务就是把结果给顾客;而老板也得到了自己想要的.达到目标,终止循环

对应的代码描述:

//这里的店员生产冰淇凌即是问题终结条件:index === 2
roles = ['老板','秘书','店员'];
index = 0;
while(index !== 2) {
    console.log(`${roles[index]}想吃冰淇凌,从${roles[index+1]}得到解决办法`);
    roles[index+1] = roles[index];  //迭代解决问题时,一般会有旧值更新的过程
    index++;  //根据得到的线索,推进下一步,直到问题解决(index === 2)
}
console.log(`${roles[0]}吃上了冰淇凌!`);

//output
老板想吃冰淇凌,从秘书得到解决办法
老板想吃冰淇凌,从店员得到解决办法
老板吃上了冰淇凌!

从上面的例子可以很清楚的得知:
1.递归

  • 一般是使用if else判断方式决定处理逻辑是返回还是接着自调用;
  • 规模上:问题规模的分解
  • 将特定条件作为最小单元,提供出口条件;
  • 可读性好:分工明确,每次遍历关注点有限,无需关注太多细节;
  • 占用内存,需要切换上下文;
  • 冗余的计算(如斐波那契使用递归计算如果不做缓存优化的话,会有大量冗余计算)

2.迭代

  • 循环结构:while、for
  • 顺序上:同一类问题场景,即重复执行相同的逻辑;
  • 将特定条件作为初始条件,提取在循环体外,循环体内负责通用逻辑
  • 循环体内为细节逻辑,通过更新变量值,达到"重定向",不断完善以达到目标的过程;
对解决一类问题的两种思维方式。
共同点:重复计算多次
循环可以用递归代替,而一旦使用递归,就最好使用尾递归。
面试题

题目:将单向链表进行反转。假设A->B->C->D;
先根据上面的总结进行分析:
循环体:B、C中间节点只需将指针指向前一个节点,重复的逻辑;
特殊条件:A节点需要将指针指向null;D节点指向由null转为C;
确定了关键信息,先来看下迭代和递归的实现:
递归版:

function reverseList(node){
    //退出的特定条件
    if(!node.next) {
        return node;
    }
    //遍历链接
    let nextNode = reverseList(node.next);
    //循环逻辑
    nextNode.next = node;
    node.next = null;
    //返回新链表的尾部指针
    return node;
}

迭代版:

function reverseList(node){
    //初始条件初始化
    let nextNode = node.next;
    node.next = null;
    //循环体
    while(nextNode) {
        let tempNode = nextNode.next;
        nextNode.next = node;
        node = nextNode;
        nextNode = tempNode;
    }
    //返回新链表的头部指针
    return node;
}

虽然实现了相同的功能,但显然迭代版实现更好:
递归版:遍历链表,反向时改变节点的指向,返回新链表的尾部指针;
迭代版:只需一次遍历,就完成节点指向的改变,返回新链表的头部指针。
并不是说迭代的实现总是比递归更优解,根据实际情况而定。

compose

redux的applyMiddleware内部实现:compose([A,B,C,D])(dispatch)将中间件组合,增强dispatch。
其中假设A、B、C、D都是中间件。对此进行分析:
预期目标:A(B(C(D(dispatch))));由于dispatch是延迟传入的,因此可以想到结合thunk处理;
循环过程: 对于中间件,后一个作为前一个的参数,循环过程;
特定条件: 即中间件数组长度。
使用递归实现如下:

function compose(middlewares) {
    if(middlewares.length === 1) {
        return (next) => middlewares[0](next);
    }
    return (next) => middlewares[0](compose(middlewares.slice(1))(next));
}

另一种使用reduce的写法:

const compose = (fns) => fns.reduce((f, g) => (next) => g(f(next)));

实现原理类似,不再赘述,举这两个例子是想表达:学以致用,举一反三。

更多技术干货分享,欢迎【扫码关注】~
1584413667(1).jpg


夜暮sky
97 声望5 粉丝