今天我们来讲讲算法的一种思想:递归。掌握好这种思想能对我们写代码很有帮助。
1. 求阶乘
我们来用代码求解5的阶乘应该怎么写呢?
我们先定义一个变量等于5,然后写一个for循环,让它在每一次循环时都乘以比自己小1位的数,再更新自己的值。再进行下一次循环。
最后当乘到一的时候就结束循环。
function jc(n) {
let num = n
for (let i = n - 1; i > 0; i--) {
num = num * i
}
return num
}
console.log(jc(5));
成功求到了120。这样想还是挺容易的。那求阶乘能不能用递归的思想解决呢?
我们这样想一想:我们要求5的阶乘,那5的阶乘是不是等于5乘以4的阶乘。问题就来到了求4的阶乘,那4的阶乘又可以等于4乘以3的阶乘;3的阶乘又可以等于3乘以2的阶乘,2的阶乘又可以等于2乘以1的阶乘。这时我们发现了,我们可以知道1的阶乘是多少,就是1啊。
// jc(5) => 5*jc(4)
// jc(4) => 4*jc(3)
// jc(3) => 3*jc(2)
// jc(2) => 2*jc(1)
// jc(1) => 1
所以我们这样写:
function jc(n) {
return n * jc(n - 1)
}
console.log(jc(5));
当你传一个n进来,我们直接返回 n * jc(n - 1) ,至于 jc(n - 1) 为多少,我不管,你自己帮我弄懂来。而当传进来的n为1时,这时我们就能管了。因为我们知道1的阶乘为1.所以我们还得加一条判断语句,当n===1时,我们直接返回1就行了。
function jc(n) {
if (n === 1) {
return 1
}
return n * jc(n - 1)
}
console.log(jc(5));
这样我们就是用递归的思想解决了这道题,结果同样是120。
所以我们发现,递归的思想,就是先找规律再找出口。如果我们不加上那条判断语句,它就会无穷无尽的执行下去,因为它并不知道1的阶乘为1,它下次就会求1的阶乘等于1乘以0的阶乘。所以我们要人为的给它设置一个出口。当它执行到求1的阶乘时,直接返回1,不用再求下去了。
2.递归求解斐波那契数列
我们知道斐波那契数列是这样一串数字:1 1 2 3 5 8 13 21 34 ...
从第三位开始,当前的值等于前两位的和。我们能不能用递归的思想求解任意位置的斐波那契值呢?
function fb(n) {
}
console.log(fb(20));
我们定义一个函数fb,当我们传进去20时,能给我们返回第20位的斐波那契值。应该怎么写呢?
首先,递归的思想之一:找规律。
当我们想求解fb(20)时,fb(20)是不是等于fb(19)+fb(18)。所以这就是规律:fb(n)=fb(n-1)+fb(n-1)。
function fb(n) {
return fb(n - 1) + fb(n - 2)
}
console.log(fb(20));
然后。递归的思想之二:找出口。我们需要人为的去设置一个出口,不能让它无穷无尽的执行下去。而我们知道,斐波那契数列的前两位没有规律,就是固定的两个1。所以当执行到fb(2)和fb(1)时,我们直接返回1就行了,就不会继续递归下去。
function fb(n) {
if (n == 1 || n == 2) {
return 1
}
return fb(n - 1) + fb(n - 2)
}
console.log(fb(20));
我们同样成功的用递归思想解决了这道题。
3. 用递归实现一个深拷贝
在上一篇文章中,我们解析了拷贝这个概念:新对象受原对象的影响叫浅拷贝,新对象不受原对象的影响叫深拷贝。
上一篇文章因为我们还没有讲到递归思想,所以无法写一个深拷贝的代码。现在,我们就能运用递归的思想写一个深拷贝代码出来了。
let obj = {
name: '晏总',
age: 18,
sex: null,
like: {
n: '骑车'
}
}
function deepClone(obj) {
}
let newObj = deepClone(obj)
obj.like.n = '打球'
console.log(newObj);
我们有这样一个场景:我们定义了一个对象obj,我们自己来写一个方法deepClone,当我们将obj作为参数传给deepClone时,它能给我们返回一个新对象newObj和原对象obj一模一样。并且当我们更改原对象obj中like中的值时,新对象newObj能不受影响。这样我们就成功实现了深拷贝。
应该怎么写呢?
拷贝需要得到一个新对象,首先我们定义一个空对象,最后再返回它。
function deepClone(obj) {
let clone = {}
return clone
}
然后我们要往新对象clone中添加值了。对象obj中有什么key,对象clone中也要有什么key。所以我们得去遍历对象obj。用for in 去遍历对象。
function deepClone(obj) {
let clone = {}
for (let key in obj) {
clone[key] = obj[key]
}
return clone
}
这样我们就能将obj上的key复制到clone上去。但我们说过,for in遍历太强大了,它会把对象obj隐式原型上的key也遍历到,而我们不想要obj隐式原型上的key。所以在这里,我们就得判断一下obj上的key是否为显示拥有。用hasOwnProperty方法。
function deepClone(obj) {
let clone = {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = obj[key]
}
}
return clone
}
这样我们完成的其实是浅拷贝的代码,因为当读到为对象的key时,我们也直接复制到clone上去了,而引用类型的赋值是引用地址的复制。我们在上一篇文章详细分析过。所以我们得在这里加一个判断语句,当我们读到为对象的key时,我们就得执行另外的操作。
而这里是不是得做类型判断了。我们知道typeof能判断原始类型的值,instanceof能判断引用类型的值。而且instanceof是通过原型链判断的。我们在这里要对引用类型进行判断,所以得用instanceof。
function deepClone(obj) {
let clone = {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] instanceof Object) {
} else {
clone[key] = obj[key]
}
}
}
return clone
}
当判断它为Object类型时,就不能直接进行复制了,得做另外操作。那应该怎么写呢?
我们这样想一想:当我们读到为对象的key时,这个对象里面是不是又会有key,这些key可能为原始类型,可能为引用类型。我们需要将这些key复制到新对象中所对应的对象中去。当碰到原始类型时,直接复制;当碰到引用类型时,需要干其它操作。那我们写的这个deepClone方法不就是干这个的吗?
当我们读到为对象的key时,我们就把这个对象传到我们写的这个deepClone方法中去,这个方法就会帮我们把原始类型复制过去,当再次碰到引用类型时,再将这个引用类型传到deepClone方法中去。总有一天会碰到一个对象里只有原始类型。这样就找到出口,不会无穷无尽执行下去。
所以这样写:
function deepClone(obj) {
let clone = {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] instanceof Object) {
clone[key] = deepClone(obj[key])
} else {
clone[key] = obj[key]
}
}
}
return clone
}
当碰到为对象的key时,我们就将它传到deepClone(obj[key])中去,让这个方法帮我们去进行这个对象的拷贝。如果这个对象里面还有对象的话,这个对象的对象也会被传到deepClone(obj[key])中去进行拷贝。
这样我们就完成了深拷贝的代码。当然这里还有个小细节,因为我们要深拷贝的可能是数组。所以我们再加一条判断语句判断obj是对象还是数组。
function deepClone(obj) {
let clone = obj instanceof Array ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] instanceof Object) {
clone[key] = deepClone(obj[key])
} else {
clone[key] = obj[key]
}
}
}
return clone
}
我们来用一下这个方法。
let obj = {
name: '晏总',
age: 18,
sex: null,
like: {
n: '骑车'
}
}
function deepClone(obj) {
let clone = obj instanceof Array ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] instanceof Object) {
clone[key] = deepClone(obj[key])
} else {
clone[key] = obj[key]
}
}
}
return clone
}
let newObj = deepClone(obj)
console.log(newObj);
我们看看输出的newObj是否和obj一样。
确实和原对象obj一样。再验证一下它是否为深拷贝。
let obj = {
name: '晏总',
age: 18,
sex: null,
like: {
n: '骑车'
}
}
function deepClone(obj) {
let clone = obj instanceof Array ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] instanceof Object) {
clone[key] = deepClone(obj[key])
} else {
clone[key] = obj[key]
}
}
}
return clone
}
let newObj = deepClone(obj)
obj.like.n = '打球'
console.log(newObj);
我们发现并没有更改。新对象newObj并没有随着obj的改变而改变。说明它确实是深拷贝。
4.数组降维
我们再来用递归解决一道题:数组降维。
我们有这样一个数组:arr = [1, 2, [3, [4]]]。我想让它降维变成 [1, 2, 3, 4] 输出。应该怎么写呢?
let arr = [1, 2, [3, [4]]]
function flattenArray(arr) {
let newArr = []
return newArr
}
console.log(flattenArray(arr));
我们写一个方法flattenArray,当我们将arr传进去时,它能给我们返回一个降维后的数组。
首先我们得搞清楚一个问题。arr的长度是多少?应该是3吧。arr下标为2的值是一个数组 [3, [4]] 。所以arr中的值为1、2再加上一个数组。
首先我们创建一个新数组newArr,用它来当作降维后的数组,最后再返回它。
我们想实现降维输出,首先我们要去遍历arr中的值,如果是原始类型我们就直接push到newArr中去;如果是数组我们就不push,去干点别的操作。
let arr = [1, 2, [3, [4]]]
function flattenArray(arr) {
let newArr = []
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
} else {
newArr.push(arr[i])
}
}
return newArr
}
console.log(flattenArray(arr));
我们拿arr[i]去判断是否为数组,如果不是数组返回false,去执行else中的push操作。那如果是数组呢?
如果得到的是一个数组,我们再调用自己本身,调用自己本身是不是能得到一个新数组啊,我们再将这个新数组和newArr拼接到一起。
let arr = [1, 2, [3, [4]]]
function flattenArray(arr) {
let newArr = []
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
newArr = newArr.concat(flattenArray(arr[i]))
} else {
newArr.push(arr[i])
}
}
return newArr
}
console.log(flattenArray(arr));
意思就是这样:读到1、2直接放到newArr中去,读到 [3, [4]] 时,调用自己本身,会得到一个新数组,然后读到3,将3存到新数组中去,然后读到 [4] 又调用自己本身,又得到一个新数组,读到4然后将4存放的新数组中去。然后,[3] 和 [4] 拼接得到 [3, 4], [3, 4]返回给上一个上下文去和 [1 ,2] 拼接得到[1, 2, 3 ,4],然后返回newArr。这样就得到了一个降维后的数组。
5.总结
我们一起看了几道用递归思想解决的经典题目。当我们碰到一个操作总是相同的进行时,我们可以考虑看看是否能用递归解决。