前文阅读:迭代和递归:一道面试题引发的思考

本篇主要举两个案例,来强化使用递归和迭代去解决复杂问题的思考过程。掌握了精髓,一般的问题都能迎刃而解。

不说废话,直接上正文。

案例1

已知信息如下,loadUrls用于控制多个异步请求,通过max限制同一时刻的并发请求数,其返回Promise,结果为所有urls的异步结果。有点类似于Promise.all,不过内部对最大并发数做了控制。

const urls = ['url1', 'url2', 'url3', 'url4','url5'];
function load(url){
  return new Promise((resolve, reject) => {
      setTimeout(() => resolve(`${url}result`), Math.random(0,6)*1000);
  })
}
function loadUrls(urls, max) { }

分析:每次最多同时有max个异步请求,有一个请求返回,补上下一个请求,以此类推。

关键信息

1)首先需要发max个请求;
2)只有一个请求结束,才会补上一个请求发出;
3)当urls中的url全部处理后,结束并返回所有异步结果。
很显然,这是个循环处理的问题。前面我们讨论过递归与迭代,不再赘述。

迭代和递归的选择

由于while循环是同步控制,顺序执行的;而上例是异步的,执行循环的时机由.then决定,因此这里我们选择递归实现。

明确循环体和出口条件

按上期制定的原则,接下来最重要的是明确出口条件以及循环体:
1)出口条件:urls的中url是否已全部处理完成;
2)循环体:一个请求返回后,load().then()中触发下一个url进行load
需要注意下,上例多了个条件:限制max个并发请求,这个放到循环函数中好像没法处理啊,怎么办呢?
加一条原则:和循环过程无关的条件提到函数外

基于上面的分析,对loadUrls进行改造:
1) 由于loadUrls需要返回Promise对象,因此第一步可先构造返回:

function loadUrls(urls, max) { 
    return Promise((resolve, reject) => {
        //...
    });
}

2) 再者由于loadUrls返回的Promise对象得到的结果是所有异步请求的结果,因此,需要其resolve:

function loadUrls(urls, max) { 
    return Promise((resolve, reject) => {
        //启动max个请求,传入resolve,供执行结束返回结果
        for(let i = 0; i < max; i++ ) {
            loadNext(urls[i], i, resolve);
        }
    });
}

3) 递归函数loadNext:

function loadUrls(urls, max) {
    //记录所有异步请求的结果
    let result = [];
    //记录已经发出请求的序号
    let loadIndex = max - 1;
    //已完成加载的数量
    let loadedNum = 0;
    //递归函数,初始条件已提出
    function loadNext(index, resolve) {
        load(urls[index]).then((val) => {
            result[index] = val;
            loadedNum++;
            //判断是否所有的url已发出请求
            if(loadedNum < urls.length && loadIndex <  urls.length-1) {    
                //下一个待发起url
                loadIndex++;
                loadNext(loadIndex, resolve);
            }else if(loadedNum === urls.length){
                resolve(result);
            }
        })
    }
    return new Promise((resolve, reject) => {
        for(let i = 0; i < max; i++ ) {
            loadNext(i, resolve);
        }
    })
}

执行结果:

loadUrls(urls, 2).then(i => console.log(i))
// ["url1result", "url2result", "url3result", "url4result", "url5result"]
案例2

二维数组:[[1,2,3], [9,10,11,4], [8,7,6,5]]

1  2  3
9 10 11 4
8  7  6 5

回形输出:1 2 3 4 5 6 7 8 9 10 11

复杂问题一定是由简单问题组成一般都是由简单问题组成,这里我们尝试按照上面的分析过程找关键信息:
1)循环体:一圈输出是一个循环
2)出口条件:遍历层级 == Math.ceil(二维数组的长度)

完美,找到了简化的处理逻辑:
1) 输出一圈数值
2) 改变层级

dealStr = (arr) => {
    let line = 0;
    while(line <= Math.ceil(arr.length/2)) {
        let startIndex = 0;
        //输出一圈的上层
        startIndex = line;
        for(let i = startIndex; i < arr[line].length - line; i++) {
            console.log(arr[line][i]);
        }
        //输出一圈的右层
        startIndex = line + 1;
        for(let i = startIndex; i < arr.length - 1 - line; i++) {
            console.log(arr[i][arr[i].length - 1 - line]);
        }
        //输出一圈的底层
        bottomLine = arr.length - 1 - line;
        if( bottomLine > line){
            startIndex = arr[bottomLine].length - 1 - line;
            for(let i = startIndex; i >= line; i--) {
                console.log(arr[bottomLine][i]);
            }
        }
        //输出一圈的左层
        for(let i = bottomLine - 1; i > line; i--) {
            console.log(arr[i][line]);
        }
        line++;
    }
}
总结

一般遇到复杂问题,按如下思路想一下:
1.适不适合用递归或迭代解决(出口条件 | 循环体)
2.两者二选1。案例1显然适合使用递归,案例2适合使用迭代;具体问题具体分析。
3.决定了大方向,如何细化分析:
递归函数如果遇到和循环逻辑无关的,不要慌,试着提出函数外考虑,那么关于递归总结的三大原则:
1)出口条件
2)循环逻辑
3)初始逻辑(考虑提出函数外执行)
迭代的不同在于:while可以简单理解为一个独立的'循环函数',因此其初始条件(如果有的话),并不需要提出函数外,只要写在while外即可。

() => {
  //1.初始信息
  while() {  //2.终止条件
    //3.循环体
  }
}

好了,递归和迭代的内容到此为止。

获取更多干货分享,请【扫码关注】~
1584413667(1).jpg


夜暮sky
97 声望5 粉丝