递归 ——关于算法的学习

466 阅读9分钟

今天我们来讲讲算法的一种思想:递归。掌握好这种思想能对我们写代码很有帮助。

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));

image.png

成功求到了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));

image.png

我们同样成功的用递归思想解决了这道题。

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一样。

image.png

确实和原对象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);

image.png

我们发现并没有更改。新对象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。这样就得到了一个降维后的数组。

image.png

5.总结

我们一起看了几道用递归思想解决的经典题目。当我们碰到一个操作总是相同的进行时,我们可以考虑看看是否能用递归解决。