1. 问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳 n 级的台阶总共有多少种跳法。
2. 问题分析
问题分析
- 递推关系分析
当它最后要跳到 n 级台阶时,有两种方式:
第一种:从 n-1 级台阶跳 1 步到达第 n 级,前面 n-1 级台阶的跳法有 f(n-1) 种
第二种:从 n-2 级台阶跳 2 步到达第 n 级, 前面 n-2 级台阶的跳法有 f(n-2) 种
因此,青蛙跳到第 n 级台阶的跳法为两种跳法数量之和:f(n) = f(n-1) + f(n-2)
—— 斐波那契数列问题 - 基准条件分析
当 n == 1,只有一种跳法
当 n == 2,有两种,跳 1 次 2 级,或跳 2 次 1 级
f(1) = 1 , f(2)=2
3. 递归
递归方式直观,但但存在大量的重复计算,如果n较大,计算时间会迅速增加
int jumpWays(int n)
{
//基准条件
if (n == 1) return 1;
if (n == 2) return 2;
//递归调用
return jumpWays(n - 1) + jumpWays(n - 2);
}
4. 非递归
记录已经计算过的结果来避免重复计算
用两个变量 pre1 和 pre2 分别保存跳到第 n-1 级和 n-2 级台阶的跳法数,每次计算时更新这两个变量
int jumpWaysOptimized(int n)
{
if (n == 1) return 1;
if (n == 2) return 2;
//记录前两个跳法
int pre1 = 1;
int pre2 = 2;
int current=0;
//从第3级开始计算跳法
for (int i = 3; i <= n; i++)
{
current = pre1 + pre2;
pre1 = pre2;
pre2 = current;
}
return current;
}
5. 对比分析
5.1 两种方案的运行时间
在调用时加入计时,需要包含头文件<chrono>,是 C++11 标准库的一个头文件,处理时间相关的功能,源于希腊语“时间”
完整代码如下
#include <iostream>
#include<chrono>
using namespace std;
int jumpWays(int n) //递归
{
//基准条件
if (n == 1) return 1;
if (n == 2) return 2;
//递归调用
return jumpWays(n - 1) + jumpWays(n - 2);
}
int jumpWaysOptimized(int n) //非递归
{
if (n == 1) return 1;
if (n == 2) return 2;
//记录前两个跳法
int pre1 = 1;
int pre2 = 2;
int current=0;
//从第3级开始计算跳法
for (int i = 3; i <= n; i++)
{
current = pre1 + pre2;
pre1 = pre2;
pre2 = current;
}
return current;
}
int main()
{
int n = 40;
//开始计时
auto start = chrono::high_resolution_clock::now();
//递归
cout << "跳法有:" << jumpWays(n) << " 种" << endl;
auto end1 = chrono::high_resolution_clock::now();
//非递归
cout<<"跳法有:"<<jumpWaysOptimized(n)<<" 种"<<endl;
auto end2 = chrono::high_resolution_clock::now();
//运行时间
chrono::duration<double> dura1 = end1 - start;
chrono::duration<double> dura2 = end2 - end1;
cout << "递归用时:" << dura1.count() << " 秒\n";
cout<<"非递归用时:" << dura2.count() << " 秒\n";
return 0;
}
某次运行结果为:
跳法有:165580141 种
跳法有:165580141 种
递归用时:0.807355 秒
非递归用时:0.0007137 秒
5.2 复杂度分析
5.2.1 递归方案
在递归写法中,函数 jumpWays(n) 会调用 jumpWays(n-1) 和 jumpWays(n-2),每次的递归调用会不断地分裂出新的递归调用。
假设用一棵递归树来表示递归调用的过程:
- 根节点是 jumpWays(n)
- 根节点有两个子节点,分别是 jumpWays(n-1) 和 jumpWays(n-2)
- 每个节点会再递归地调用两个子问题
假设我们要计算 f(5):
f(5)
/ \
f(4) f(3)
/ \ / \
f(3) f(2) f(2) f(1)
/ \ / \
f(2) f(1) f(1) f(0)
/ \
f(1) f(0)
f(n) 的递归结构是 f(n-1) + f(n-2),递归树中会有许多重复计算,比如 f(2) 和 f(1) 被多次调用
递归树的高度,也就是从根节点到叶子节点的最大深度,直接反映了递归调用的层次。在这个问题中,递归树的高度等于 n
递归树的节点总数表示了递归调用的总次数,这也是我们分析时间复杂度的关键点。每个节点对应一次函数调用
时间复杂度
递归调用的次数并不仅仅是 n 次。由于每个节点会递归调用两个子问题,并且子问题之间会产生重叠计算,当 n 很大时,递归树的节点数量几乎成倍增长。每个递归调用会生成两个新的子调用,在最坏情况下递归调用次数接近 2^n,指数级别增长,接近于斐波那契数的大小,因此,递归的时间复杂度是 O(2^n),这是一种指数级复杂度。对于较大的 n 值,递归方法的时间复杂度非常高。
空间复杂度
空间复杂度由递归调用的栈深度决定。每次递归调用都会在调用栈中创建一个新的栈帧,当递归深度为 n 时,最大栈深度就是 n,空间复杂度是 O(n)
5.2.2 非递归方案
也是动态规划思路的体现
时间复杂度 O(n)
只需从第3级台阶到第n级台阶遍历计算一次
空间复杂度 O(1)
只使用了常数级别的额外空间

1476

被折叠的 条评论
为什么被折叠?



