文章目录
- 我的第一次
- 今日份练习
- | 斐波那契数列
- 憧憬
我的第一次
第一次浏览CSDN的时候刚开始学C++的时候,当时的课设是《C++ & SQL 2008的学生管理系统》,C++作前段界面、逻辑处理,SQL作为后端服务器支持的题目,当时不太认真学习,上课只顾着玩手机,快结课了才想着赶作业,当时就百度了这个课设题目,恰好点进了一个CSDN博主的文章(具体是哪篇已经不太记得了,只记得是Visual Studio 2010 + (C++) + SQL的开发环境)。在此之前感觉写代码好像也就是把人算数学题的过程变成了代码算数学题的逻辑,当然大一大二两年也是没怎么学编程,导致基础贼烂😭
从大三开始进实验室慢慢补基础,学东西才意识到编程这玩意只看书,应付考试拿分没意义,还得是多敲键盘。
第一次在这写文章,是大三进实验室的时候,网上看到Vscode的界面比VS好看多了,就跟着搭环境,这不会那不会,当时折腾了一天,想着那么难搞就记录下来,生怕之后换电脑了又不会整了。也是这一次开始接触Markdown
语言😁,确实好用
今日份练习
恰好,偷懒多天后又练习了一哈哈代码,记录一哈😍
| 斐波那契数列
题目
写一个函数,输入 n,求斐波那契(Fibonacci)数列的第 n 项(即
F
(
N
)
F(N)
F(N) )。斐波那契数列的定义如下:
F
0
=
0
,
F
1
=
1
F_0 = 0, F_1 = 1
F0=0,F1=1
F
N
=
F
N
−
1
+
F
N
−
2
,
N
>
1
F_N = F_{N - 1} + F_{N - 2}, N > 1
FN=FN−1+FN−2,N>1
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
- 输入:n = 2
- 输出:1
示例 2:
- 输入:n = 5
- 输出:5
思路
-
递归法
很常见的递归解题法,因为从第三项开始,每一项都是前两项的和,所以可以将问题分解成每个项的求值再相加,也就得到了递归的总思想,下面按照递归三部曲确定算法流程-
确定递归函数的参数列表:
每一次递归要计算的每一项的数值,所以参数列表应该代表着当前求值项,那么参数就是下标N
int Fibonacci1(int N)
-
确定递归函数的终止条件:
递归也是一个遍历的过程,而遍历肯定有边界,对于本题来说,边界就是[0, n],那么终止条件就是下标N达到边界时即停止
if (0 == N) { return 0; } if (1 == N) { return 1; }
-
确定递归函数的单层递归逻辑
单层逻辑要求的是项值,而每一项又等于前两项的和,这就是本题中的单层递归逻辑return Fibonacci1(N - 1) + Fibonacci1(N - 2);
-
以求 f 5 f_5 f5为例子,框图如下
图中可以看出,绿色节点代表着首次计算,蓝色节点代表着已知节点,粉色节点代表着重复计算,每个节点的时间消耗为 O ( 1 ) O(1) O(1),而二叉树的节点数为 O ( 2 n ) O(2^n) O(2n),所以时间复杂度
为 O ( 2 n ) O(2^n) O(2n)
-
-
递归进阶法
上面提到了常规的递归法会导致多余的资源浪费,当N足够大的时候,计算的时间和资源开销会很大,如果把这些重复计算的资源省略掉,就可以进一步减少算法的时间开销,因为重复计算的项的值都是一样的,那么只要判断当前项是否已经计算过,如果计算过了则直接从之前的结果中取出即可,这样就可以节省重复项的计算时间
同理下面按照递归三部曲确定算法流程-
确定递归函数的参数列表
与常规递归法不同的是,进阶法需要保存之前计算过的项值,所以需要增加一个数组用来保存这些值
int Fibonacci2(int N, vector<int> Arr)
-
确定递归函数的终止条件:
这一块跟常规递归法没有什么差异,保持一致 -
确定递归函数的单层递归逻辑
与常规递归法相比,增加了重复项的对比流程if (0 != Arr[N]) { return Arr[N]; } return Fibonacci2(N - 1, Arr) + Fibonacci2(N - 2, Arr);
-
以求 f 5 f_5 f5为例子,框图如下
与常规递归法对比,灰色节点代表已经计算过的值,不需要进行计算,那么实际的调用次数就只剩下左边绿色节点和蓝色节点,这样时间复杂度就从 O ( 2 n ) O(2^n) O(2n) 降到 O ( n ) O(n) O(n),但是因为使用了额外的数组进行保存值,所以空间复杂度由原来的 O ( 1 ) O(1) O(1) 升到 O ( n ) O(n) O(n),这就是算法中常见的空间换时间
策略
-
-
动态规划
既然有递归,那么迭代法也跑不掉。
从第二项开始,每一项都等于前两项和,那么可以迭代的把遍历过的所有项进行计算,就可以得到最后一项的值了
f 2 = f 0 + f 1 f_2 = f_0 + f_1 f2=f0+f1
f 3 = f 1 + f 2 = f 1 + ( f 0 + f 1 ) f_3 = f_1 + f_2 = f_1 + (f_0 + f_1) f3=f1+f2=f1+(f0+f1)
f 4 = f 2 + f 3 = ( f 0 + f 1 ) + ( f 1 + ( f 0 + f 1 ) ) f_4 = f_2 + f_3 = (f_0 + f_1) + (f_1 + (f_0 + f_1)) f4=f2+f3=(f0+f1)+(f1+(f0+f1))
. . . . . . ...... ......
那么只需要保存记录好遍历到的每一项的前两项值,并做结果累加就能模拟出上面式子的过程,具体算法流程如下
1. 先记录保存首项和次项值
f
0
,
f
1
f_0, f_1
f0,f1
2. 从第二项开始遍历,遍历到的项值等于其前两项的和
3. 遍历到最后一项,结束遍历返回结果值
以上算法唯一需要思考的是第二步,要怎么计算当前遍历项的值?
可以申请一个长度为N的数组,保存之前的每一项值,那么该算法的空间复杂度就跟N有关,为
O
(
n
)
O(n)
O(n),仔细思考其实要保存的只是当前遍历项的前两项,而已经遍历后的项的前两项值其实并不关心,所以只需要申请两个变量保存当前遍历项的前两项值即可,空间复杂度也降为
O
(
n
)
O(n)
O(n)
- 为什么不申请三个变量,两个保存前两项,一个保存结果值?
因为当前项计算好之后,遍历到下一项时,会用到当前项的值已经当前项的前一项的值,也就说明总有一个变量时一直跟着遍历往下“共享”的,那么只需要将其中一个变量的功能扩展为先记录前一项的值,后保存为当前项的值就可以实现这个“共享”的目的,最后遍历到 f n f_n fn的时候,这个变量刚好完成了 f n − 1 + f n − 2 f_{n - 1} + f_{n - 2} fn−1+fn−2的计算,所以这个变量的值就是结果值
代码
int Fibonacci1(int i_uNum)
{
if (0 > i_uNum)
{
return -1;
}
else if (0 == i_uNum)
{
return 0;
}
else if (1 == i_uNum)
{
return 1;
}
return Fibonacci1(i_uNum - 1) + Fibonacci1(i_uNum - 2);
}
int Fibonacci2(int i_uNum, vector<int>& i_uArr)
{
if (0 > i_uNum)
{
return -1;
}
else if (0 == i_uNum)
{
return 0;
}
else if (1 == i_uNum)
{
return 1;
}
if (0 != i_uArr[i_uNum])
{
return i_uArr[i_uNum];
}
return Fibonacci2(i_uNum - 1, i_uArr) + Fibonacci2(i_uNum - 2, i_uArr);
}
int Fibonacci3(int i_uNum)
{
if (0 > i_uNum)
{
return -1;
}
else if (0 == i_uNum)
{
return 0;
}
else if (1 == i_uNum)
{
return 1;
}
int Fir = 0, Sec = 1;
for (int i = 2; i <= i_uNum; i++)
{
int tmp = Fir;
Fir = Sec;
Sec = tmp + Sec;
}
return b;
}
小结
进阶递归和动态规划的本质思想是一致的,区别在于:
- 进阶递归 — 从顶至低: 求 f n f_n fn 需要 f n − 1 f_{n - 1} fn−1 和 f n − 2 f_{n - 2} fn−2, ⋯ \cdots ⋯ ;求 f 2 f_2 f2 需要 f 1 f_1 f1 和 f 0 f_0 f0 ;而 f 1 f_1 f1 和 f 0 f_0 f0 已知;
- 动态规划 — 从底至顶: 将已知 f 0 f_0 f0 和 f 1 f_1 f1 组合得到 f 2 f_2 f2, ⋯ \cdots ⋯;将 f n − 2 f_{n - 2} fn−2 和 f n − 1 f_{n - 1} fn−1 组合得到 f n f_n fn;
憧憬
希望以后能好好学习,接触更多的新东西,赚大钱😶