老虎会吃羊吗?🐅🐑
草原上有 1000 只老虎和 1 只羊,老虎吃羊,羊吃草,但是老虎吃完羊会变成羊(一只羊只能被一只老虎吃)
提示:每只老虎都很聪明
不吃:一只老虎吃完羊后变为羊会被剩下的老虎吃
吃:一只老虎吃完羊后情景变为 case1
不吃:一只老虎吃完羊后情景变为 case2 会被吃
吃:一只老虎吃完羊后情景变为 case3
.
.
.
.
.
.
由最基本的归纳法可以判断只有一只羊的时候,偶数只老虎情况下不吃,而奇数只老虎情况下可以吃
引出课题 Dynamic Programming
简单的理解就是从最基本的的情况入手逐渐将整个问题解决,动态规划主要包含以下三种概念
下面用常见的斐波那契数列(黄金分割数列、兔子数列)来具象化问题
Fibonacci:斐波那契数列指的是这样一个数列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368…这个数列从第 3 项开始,每一项都等于前两项之和
在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)因此可以直接写出以下代码:
function fn(n) {
if (n === 1 || n === 2) {
return 1
}
return fn(n - 1) + fn(n - 2)
}
运行结果如下:
console.log(
`第1项${fn(1)}`,
`第2项${fn(2)}`,
`第3项${fn(3)}`,
`第4项${fn(4)}`,
`第5项${fn(5)}`,
`第6项${fn(6)}`,
`第7项${fn(7)}`
)
// VM455:1 第1项1 第2项1 第3项2 第4项3 第5项5 第6项8 第7项13
现在根据斐波那契数列来解释上面提出的三个动态规划相关的概念
m
项的值,那么只需要去求得第 m-1
和 m-2
的值即可,那么 fn(m-1) + fn(m-2) === f(m)
等式成立f(m) = fn(m-1) + fn(m-2)
上面的方法其实看起来就是一个最简单的递归,但是性能会有很大的问题,为什么呢? 具象化来看: 求得 f(5) = ?
显然上面的计算有着大量的重复操作,既然所有的结果都是基于 f(1)和 f(2), 那么从 f(1)和 f(2)的计算开始就避免了重复的计算,也是就动态规划的思想, 代码如下:
function fn(n) {
if (n === 1 || n === 2) {
return 1
}
let m = 1,
m_1 = 1,
result
for (let i = 3; i <= n; i++) {
result = m + m_1
m_1 = m
m = result
}
return result
}
结果和上面是一致的但是性能就会有非常大的提升了
递归: 程序调用自身的编程技巧称为递归(recursion)
那么上面 return fn(n-1) + fn(n-2)
就是一个最普通的递归
这里再引入一个尾递归的概念:就是函数在尾部调用自身
那下面来简单说说为什么上面递归的写法性能会那么差
调用栈随着 n 的增加而线性增加很容易造成内存堆栈溢出。这是因为这种递归操作中,同时保存了大量的栈帧,调用栈非常长,消耗了巨大的内存
将上面方法改用尾递归的写法:
function fn(n, m_1 = 1, m = 1) {
if (n === 1 || n === 2) return m
return fn(n - 1, m, m + m_1)
}
此时 fn 每次递归仅仅更新自己的调用栈,性能得到大幅提升 👍
当然所有的递归都可以用循环来实现,不就避免了堆栈溢出的可能了吗 啊哈哈哈哈哈哈
draven:好好看 好好学