一、递归
有一对兔子,从出生后第3个月起每个月生一对兔子,小兔子长到第3个月后每个月又生一对兔子,假如兔子不死,问第二十个月的兔子对数是多少?
我们来看一下,归纳一下:
* 第一个月: 1
第二个月: 1
第三个月: 2
第四个月: 3
第五个月: 5
…
这个问题是我们初中时候学过的斐波那契数列的兔子问题。
好的,作为一名前端程序猿,试问如何用js代码来解决这个问题。
// 此处做一下解释,为什么i是从2开始,而不是3,看循环体中代码,因为接收的参数num从第num-1个月开始,如果i从3开始算,那么得到的数据回事n的前一个月的数据,而不是当月数据。//这里也可以将判断条件改为 <=,那么i就可以从di3个月算起了function fibs(num){let n1 = 1 // 第1个月let n2 = 1 // 第2个月的数量let n = 1for(let i = 2; i < num; i++){[n1, n2] = [n2, n]n = n1 + n2}return n}
上面的代码可能并不好理解,甚至还有同学对于数组的解构赋值不太了解,但是递归算法你一定可以一眼就看明白。
1、递归
递归,顾名思义是一种依次递增或者递减的归纳得来的算法。在学习递归前,我们先来说一下递归的两大要素:
1)终止条件
2)递归公式
尤其对于初学者而言,忘记终止条件,会导致循环持续下去,堆栈溢出,一般在chrome中会报错。
拿一个经典递归斐波那契数列来说,我们第一反应的fibs应该是这样的:
function fibs(n) {return n <3 ? 1 : fibs(n - 1) + fibs(n - 2)}
在这里 递归公式为 f(n) = f(n-1) + f(n-2)
终止条件为 n < 3(n===1 || n ===2 时候,返回都是1)
Good!这样计算固然没错,那么,这是不是最优解呢?
要讨论一个算法的优劣,毫无疑问,要看一下时间复杂度。这种粗暴递归的时间复杂度为O(2^n)
为什么是O(2^n)?我们以n=10举例。
来看下图!
通过上图,我们不仅可以看到,递归计算的复杂度,还可以看到在计算过程中,存在大量的重复计算!
What?
那想一下,我们能不能不去重复计算,怎么做。首先想到的方法——缓存
2、缓存递归
下面,我们来设置一个数组来缓存已经递归出的数据
var cache = []function fibs(n) {if(n < 3) {return 1} else {if(!cache[n - 1]) {cache[n-1] = fibs(n - 1)}if(!cache[n - 2]) {cache[n-2] = fibs(n - 2)}return cache[n-1] + cache[n-2]}}
通过以上方法将每一次递归的数据缓存在数组里,确实省去了一部分重复的计算。
重复计算去掉后,我们的时间复杂度变为多少了呢,应该是O(n),因为此时的时间复杂度和数据规模呈正相关。没有了多余分枝树的计算。
3、动态规划
缓存递归已经算得上是很好的递归算法了,代码执行效率比较高。日常工作中,我一般写到缓存递归这一步,也就洋洋自得了,欧耶。确实,日常工作中写到缓存递归基本够用。
然而,这还不够。想要进阶,你必须知道,递归的逆向思维方式——动态规划。
我们先来说一说什么是动态规划。
function fibs(n) {let val = new Array(n).fill(1)if (n <= 2) {return 1} else {for (var i = 3; i <= n; ++i) {val[i] = val[i-1] + val[i-2]}console.log(val)return val[n - 1]}}
二、递归的用途
1、对象、数组的深度复制
深度复制的原理是需要遍历对象,然后将得到的obj[key]赋值给新的对象,但是我们不能保证obj[key]为普通类型,必须判断当其为复杂类型时候,继续遍历obj[key](也就是递归),直到我们得到一个普通类型数据。
这是我之前写的一段代码:
// 判断数据类型const checkType = function(obj) {return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()}// 判断为普通类型const isNoraml = function(obj) {const type = checkType(obj)if (type === 'number' || type === 'string' || type === 'null' || type === 'undefined') {return true}return false}// 深度复制函数const deepClone = function(obj) {let newObj = checkType(obj) === 'array' ? [] : {}for(let key in obj){// 由于for...in遍历继承属性,此处需要判断是不是对象的自身属性。在官方是不推荐用for...in遍历 数组的。此处也可根据类型,拆分为用for...in遍历对象,用for...of遍历数组。或者都用for循环遍历。if(obj.hasOwnProperty(key)){newObj[key] = isNoraml(obj[key]) ? obj[key]: deepClone(obj[key])}}return newObj}
在网上也看到过有人这样写的,很简洁,贴出来。
function deepClone(obj) {let newObj = Array.isArray(obj) ? [] : {}if (obj && typeof obj === "object") {for (let key in obj) {if (obj.hasOwnProperty(key)) {newObj[key] = (obj && typeof obj[key] === 'object') ? deepClone(obj[key]) : obj[key]}}}return newObj}
感觉这个比我的写的好,哈哈哈。
当然,深度复制还有一些其他技巧,例如:
JSON.parse(JSON.stringify(oldObj))// 该方法有很多缺点,大家可以自行查资料。
其过程说白了 就是利用JSON.stringify 将js对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象。
缺点:
1、如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式。而不是时间对象;
2、如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象;
3、如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
4、如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null
5、JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;
6、如果对象中存在循环引用的情况也无法正确实现深拷贝;
以上缺点参考文章:https://blog.csdn.net/u013565133/article/details/102819929
const newObj = Object.assign([],oldObj);// 该方法无法实现多级的深拷贝
在实际工作开发中,往往使用lodash插件,进行deeoClone。索性来看一下lodash源码怎么写的。我去cnd复制了lodash的源码。

在代码中搜索cloneDeep,发现是引用的函数baseClone,同时,传递了一个深度克隆的标记CLONE_DEEP_FLAG。
下面去看baseClone这个函数是怎么工作的。
我们先看数组吧,如果判断为数组,走 initCloneArray 方法,同时,下面判断,如果不是深度复制,则直接走copyArray方法,并return
我们再去看initCloneArray 方法做了什么:
我们看到,首先获取了数组长度
然后,创建了一个和array一样长度的空(new array.constructor(length))
之后做了判断,
2、数组的扁平化
很多人用Array.prototype.flat去拍平一个多维数组,方便又好用。内部原理就是递归判断元素还是数组,接着遍历,直到为普通类型,将元素放入一个新的数组中。
看了上面的深度复制,稍微改造一下就可以得到一个flat方法。
const checkType = function(obj) {return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()}const isArray = function(obj) {if (checkType(obj) === 'array') {return true}return false}const arrayFlat = function(obj) {let newArr = []function flatData(obj) {for(const key in obj){if(obj.hasOwnProperty(key)){isArray(obj[key]) ? flatData(obj[key]) : newArr.push(obj[key])}}return newArr}return flatData(obj)}
上面为了方便使用的三元,其实都是不推荐的,if、else才是王道。当然,还可以继续优化,不在赘述。
