聊一聊前端算法面试——递归

19,643 阅读2分钟

文章首发于:github.com/USTB-musion…

写在前面

现在竞争越来越激烈,今天来聊一聊前端面试中出现频率非常高的一种算法思想——「递归」。

先看下几个常见的面试题:

  1. 假如楼梯有n个台阶,每次可以走1个或2个台阶,请问走完这n个台阶有几种走法(动态规划实现)❓
  2. 如下图所示:一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径❓

  1. 在M件物品里❓

你可以先思考一下如何回答上边的问题🤔,然后带着答案来阅览接下来的内容。

如何编写递归代码❓

递归思想在前端面试中非常常见,除了上面的一些题目之外,二叉树的前中后序遍历,斐波那契数列等都用到了递归的思想。简单地理解递归就是:自己调用自己。那如何编写递归的代码呢❓ 在笔者看来:

主要有两个关键步骤:

  • 写出递归公式
  • 找到终止条件

先来看个简单的例子:如何求1+2+3+4+...+n的和?相信用for循环的方法大家都知道如何编写:

function sum(n) {
    var total = 0
    for (int i = 1; i <= n; i++) {
    	total = total + i
    }
    return total
}

那如何改为递归的写法呢?

第一步: 写出递归公式

细心观察就会发现,其实就是n与n-1和n-2的关系

sum(n) = sum(n-1) + n
···
···
···
sum(100) = sum(99) + 100
sum(99) = sum(98) + 99
···
···
···
sum(5) = sum(4) + 5
sum(4) = sum(3) + 4
sum(3) = sum(2) + 3
sum(2) = sum(1) + 2
sum(1) = 1

将如上转化成递归公式就是

function sum(n) {
    return sum(n-1) + n
}

第二步:找出终止条件

递归公式写出来了,那么递归代码就完成了一大半。现在来看一下上述问题的终止条件是什么呢,即sum(1)= 1;

结合递归公式和终止条件,1+2+3+4+...+n求和的递归代码如下:

function sum(n) {
    if( n ===1 ) return 1
    return sum(n-1) + n
}

面试题1: 楼梯问题

假如楼梯有n个台阶,每次可以走1个或2个台阶,请问走完这n个台阶有几种走法❓

按照我们上面的思路,先写出递归公式。那这道题我们如何去找出递归公式呢。假设有3个台阶,我们可以有3种走法:

1 1 1
1 2
2 1

第一种是每次都是走1个台阶。第二种是第一步走1个台阶,第二步走2个台阶。第三种是第一步走2个台阶,第二步走1个台阶。

写出递归公式:

那如果有n个台阶呢?我们认真思考下就会发现,第1步的走法有两类:第一种是第一步走1个台阶,第二种是第二步走2个台阶。所以n个台阶的走法就可以分为:走完1个台阶后的n-1种走法,加上走完2个台阶后的n-2种走法,用递归公式表示就是:

function climbStairs(n) {
    return climbStairs(n - 1) + climbStairs(n - 2)
}

找到终止条件:

climbStairs(n) = climbStairs(n-1) + climbStairs(n-2)
climbStairs(n-1) = climbStairs(n-2) + climbStairs(n-3)
···
···
···
climbStairs(5) = climbStairs(4) + climbStairs(3)
climbStairs(4) = climbStairs(3) + climbStairs(2)
climbStairs(3) = climbStairs(2) + climbStairs(1)
climbStairs(2) = 2
climbStairs(1) = 1

从上面的推导可以看出:终止条件为:

climbStairs(2) = 2
climbStairs(1) = 1

综上所述,解决爬楼梯的代码如下:

function climbStairs(n) {
  if (n == 1) return 1
  if (n == 2) return 2
  return climbStairs(n-1) + climbStairs(n-2)
}

当然,可以对上述题目做一个memorize操作,性能会好很多:

var calculated = []

function climbStairs(n) {

    if(n == 1) {
        return 1
    }else if (n == 2) {
        return 2
    }else {
        if(!calculated[n-1]){
            calculated[n-1] = climbStairs(n-1)
        }

        if(!calculated[n-2]){
            calculated[n-2] = climbStairs(n-2)
        }
        return calculated[n-1] + calculated[n-2]
    }

}

解决完爬楼梯问题之后,思考下斐波那契数列问题的求解,有木有发现是一样的问题和思路:)

面试题2:实现深拷贝

如何用递归思想实现深拷贝❓如果要实现深拷贝那么就需要考虑将对象的属性, 与属性的属性,都拷贝过来, 这种情况下就非常适合使用递归思想来解决,在拷贝的适合判断属性值的类型,如果是对象则递归调用deeplClone函数,否则直接返回该属性值:

var deepCopy = function(obj) {
    if (typeof obj !== 'object') return;
    // // 根据obj的类型判断是新建一个数组还是对象
    var newObj = obj instanceof Array ? [] : {};
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
        }
    }
    return newObj;
}

面试题3:如何把数组拍平

如何用递归思想实现数组的扁平化❓即如何把[1, [2], [3, [4, [5]]]]拍平得到[1,2,3,4,5]❓

const flatten = (arr) => {
  let result = [];
  arr.forEach((item, i, arr) => {
    // 若为数组,递归调用 faltten,并将结果与result合并
    if (Array.isArray(item)) {
      result = result.concat(flatten(item));
    } else {
      result.push(arr[i])
    }
  })
  return result;
};
const arr = [1, [2, [3, 4, 5]]];
console.log(flatten(arr)); // [1, 2, 3, 4, 5]

总结

编写递归代码的关键在于找出递归公式和终止条件,最后将它们翻译成代码。递归代码虽然比较简洁。但是也有很多弊端,如性能不是很高效,这时候要做memorize等一些操作来优化性能。容易产生堆栈溢出等问题,所以我们在写代码的时候要注意这些。

你可以关注我的公众号「慕晨同学」,鹅厂码农,平常记录一些鸡毛蒜皮的点滴,技术,生活,感悟,一起成长。