走台阶算法实现(理解算法)
本文章会分析算法的特点,以及实现过程。帮助理解不同算法之间时间复杂度和空间复杂度的区别
一、程序要求
假设有n个台阶,一次只能上1个台阶或2个台阶,请问走到n个台阶有几种走法
二、程序思路
例:假如有三个台阶,有以下三种走法:111 12 21
一个(1种) 1
二个(2种) 11 2
三个(3种) 111 12 21
四个(5种) 1111 112 121 211 22
爬n个台阶
要么从n-1个爬1阶直接到n阶
要么从n-2个爬2阶直接到n阶
因此爬n阶的走法=爬n-1个的走法+爬n-2个的走法
即f(n)=f(n-1)+f(n-2)
f(3)=f(2)+f(1)
f(2)=2
f(1)=1
为了检测算法正确性 函数一律输入20,只要结果为10946即正确
一、普通递归
1.普通递归
特点:
但是由于f(n)=f(n-1)+f(n-2)有两个递归(即f(n-1)和f(n-2)),每个执行n次(取极限),导致时间复杂度变为 O(2^n)
而递归的深度为n,即空间复杂度是为O(n)
当n的值过大时,耗费时间过长
function NclimbStairs(n){
//阶梯数为1或者2时,走法数等于阶梯数
if(n==1 || n==2) return n
return NclimbStairs(n-1)+NclimbStairs(n-2)
}
//这里我们用console.time去获取函数执行时间,去对比算法的时间复杂度
console.time('N');
console.log('普通递归:'+NclimbStairs(20));
console.timeEnd('N')
二、记忆递归
2.记忆递归
特点:一个递归,一个n+1长度的数组,时间复杂度O(n),空间复杂度O(n)
原理:
在递归的时候,我们做了很多重复的运算
例如:f(5)=f(4)+f(3) f4=f(3)+f(2) f3=f(2)+f(1) 可以发现f(3),f(2)执行了两次,即递归两次。
实际我们只需要执行f(1),f(2)...f(n) 即 n 次
因此记忆递归就是把执行过的结果用数组保存起来,以减少空间复杂度
为什么是n+1个长度数组解释:
因为我们的存储形式是 例如:arr[1]=fn(1)
而数组的序号是从0起始的
所以0-n有n+1个数
function climbStairsMemo(n){
//阶梯数为1或者2时,走法数等于阶梯数
let arr=[] //定义一个数组来保存阶梯的走法
arr[1]=1,arr[2]=2
function climbStairs(n){
//n为1或2,arr里面已经保存过了,直接返回即可
if(n==1 || n==2) return arr[n]
let result=arr[n]
//当数组的第n位保存过数据,那就不进行递归,直接返回
if(result!==undefined){
return result
}else{
//没保存过就进行递归
result=climbStairs(n-1)+climbStairs(n-2)
arr[n]=result
}
return result
}
climbStairs(n) //调用递归
return arr[n]
}
//这里我们用console.time去获取函数执行时间,去对比算法的时间复杂度
console.time('Memo');
console.log('记忆递归:'+climbStairsMemo(20));
console.timeEnd('Memo')
三、动态规划
3.动态规划法
特点:一个循环,一个n+1长度的数组,空间复杂度O(n) 时间复杂度O(n)
原理:沿用了记忆递归保存结果的特性,但是运算的方式发生了改变
根据表达式f(n)=f(n-1)+f(n-2),在外面知道fn(1)=1和f(2)=2的条件下
我们可以得出fn(3)=f(2)+f(1),当我们得出f(3)可进行保存,然后就可以利用f(2)和f(3)求出f(4)
类比操作,我们求到f(n)即可求出结果
function climbStairsAc(n){
//阶梯数为1或者2时,走法数等于阶梯数
let arr=[] //定义一个数组来保存阶梯的走法
arr[1]=1,arr[2]=2
//由于arr[1]和arr[2]的值已经有了,i需要从3开始
for(var i=3;i<=n;i++){
arr[i]=arr[i-1]+arr[i-2]
}
return arr[n]
}
//这里我们用console.time去获取函数执行时间,去对比算法的时间复杂度
console.time('AC');
console.log('动态规划:'+climbStairsAc(20));
console.timeEnd('AC')
四、斐波那契数列
4.斐波那契数列
特点:一个循环,只有三个变量 时间复杂度O(n) 空间复杂度O(1)
原理:
在动态规划,我们在运算过程实际只用到了三个变量,即arr[i],arr[i-1],arr[i-2]
即两个变量进行运算,一个变量存放结果
在f(3)=f(2)+f(1)中,我们通过f(1)和f(2)求出了f(3)的结果
而进行下一步运算时,我们是用f(2)和f(3)求出f(4)
这也就意味着,在下一步运算中f(1)是不需要的
假设我们有三个变量a,b,c
先用a存f(1) 再用b存f(2)
c存f(3),f(3)=f(2)+f(1)
那么下一步运算中a就不用存f(1)了
然后开始把f(2)给a,再把f(3)给b,即可求出f(4) (为什么先把f(2)给a:因为f(2)的值不先给a,就会被f(3)的值覆盖)
运行到最后返回b的值即可
解释:
因为变量需要先计算,再赋值
所以最后的操作一定是赋值操作
即b=c
function climbStairsFb(n){
let a=1,b=2,c
//由于arr[1]和arr[2]的值已经有了,arr[i]需要从3开始
for(var i=3;i<=n;i++){
c=a+b
a=b
b=c
}
return b
}
//这里我们用console.time去获取函数执行时间,去对比算法的时间复杂度
console.time('Fb');
console.log('斐波那契数列:'+climbStairsFb(20));
console.timeEnd('Fb')
五、总结
以下是上面四种算法运行的时间,可对时间复杂度做参考
对于时间复杂度
可以看出,普通递归的运行时间最长的。因为它时间复杂度是为O(n^2)。
而记忆递归,动态规划,斐波那契数列的运行时间基本相差不大。因为它们都是O(n).
对于空间复杂度
递归算法,记忆递归,动态规划的空间复杂度相同,都是O(n)
其中记忆递归的因为也是递归调用,递归次数过多会不停的开辟函数执行栈而导致溢栈现象。
斐波那契数列对动态规划进行了优化,使得其空间复杂度为O(1)