赞
踩
最开始学习递归,经典算法就是斐波那契
你是否留意过它的算法复杂度如何
在这里我们忘掉意识里已有的复杂度观念,从最基础代码分析
我们把 代码执行的行数 作为时间复杂度
fibonacci(3)
因此 fibonacci(3) 总共执行了4行代码,复杂度就是具体的4次
可以看出,重要不是 fibonacci(1) 或 fibonacci(2),暂时也不考虑具体中间递归调度过程
只看最直接的调度,执行两行代码,这有助于后面的分析
以fibonacci(5)计算为例,我们把整个过程平摊开来
fibonacci(5) 总共执行的代码行数 = 5个叶子结点 + 4个非叶子结点 * 2
整理之后 fibonacci(5) 总共执行的代码行数 = fibonacci(5) + (fibonacci(5) - 1) * 2
最终的到 fibonacci函数复杂度:
F(n) = 3 * fibonacci(n) - 2
如果 n == 5, 最终执行了13行代码,也就是复杂度 13次
这样不会觉得有什么问题
如果 n = 50, 数据太大,整型溢出,换n = 30, 复杂度 832040 * 3 - 2,2 * 10^6, 这是一个很高的复杂度了
fibonacci 递归,期间经历很多重复的递归计算,如果我们把一些中间过程缓存起来,就能省去很多不必要的计算
统计代码执行行数: 3 + (n - 2 + 1)[for] + n - 2 = 2n
按之前的n = 30, 与简化前的版本比较 就是 60 与 2 * 10^6 差别,复杂度差出5个数量级
已经有点动态规划的思想在这个简化版本里了
但存在一个问题,复杂度降低了,却开辟了很多内存,
数据特别大超出平时脑子里意识的时候,虽然时间复杂度比较可观,但是需要庞大的内存开销作为代价,甚至可能算法都执行不起来
简化的版本,用到了 n+1 个整型内存空间,主要的执行部分逻辑,其实只涉及两个需要的变量,可以省去额外的内存开销
稍微对比下 修正后的简化版本,与初始的递归版本
就是 n 与 10^6的差别
算法也许是个很枯燥的过程,但如果善于运用工具,比如fibonacci利用矩阵,就很好的体现了代码的执行过程 矩阵相乘包含着前两项的相加过程
追踪矩阵的 第(0,0)元素,就得到 fibonacci(n)的结果
struct matrix_struct { int matrix[2][2]; }; // 两个 2x2 矩阵相乘 void matrixMultiply(struct matrix_struct matrix_structA, struct matrix_struct *matrix_structB) { struct matrix_struct matrix_structX; matrix_structX.matrix; for (int i = 0; i < 2; i++) { for (int j = 0; j < 2; j++) { matrix_structX.matrix[i][j] = matrix_structA.matrix[i][0] * matrix_structB->matrix[0][j] + matrix_structA.matrix[i][1] * matrix_structB->matrix[1][j]; } } *matrix_structB = matrix_structX; } // 矩阵的平方运算 // n: 矩阵与初始矩阵执行 n次相乘运算,每次相乘之后,结果替换掉原矩阵 执行下一次相乘 // matrix: 2行2列的矩阵 void matrixPower(int n, struct matrix_struct *matrix_structY) { struct matrix_struct matrix_structX; matrix_structX.matrix[0][0] = 1; matrix_structX.matrix[0][1] = 1; matrix_structX.matrix[1][0] = 1; matrix_structX.matrix[1][1] = 0; for (int i = 0; i < n; i++) { matrixMultiply(matrix_structX, matrix_structY); } } int fibonacci3(int n) { if (n < 2) { return n; } struct matrix_struct matrix_structInit; matrix_structInit.matrix[0][0] = matrix_structInit.matrix[1][1] = 1; matrix_structInit.matrix[0][1] = matrix_structInit.matrix[1][0] = 0; struct matrix_struct *matrix_structI = &matrix_structInit; // 其中第一次相乘 的到 初始 矩阵 matrixPower(n - 1, matrix_structI); return matrix_structI->matrix[0][0]; }
这个版本并没有不合适的内存问题
接下来继续分析是执行次数,也就是时间复杂度
matrixPower for循环还是n,所以复杂度继续保持在n这个数量级,与之前的版本没多大差别
根据 矩阵的平方运算 matrixPower,执行了n次
其实也就是矩阵 (1,1,1,0)【标识 两行两列, 第一行: 1,1 ,第二行: 1,0】 执行了n次相乘
我们可以考虑减少 相乘计算的次数,比如16次相乘,其实就是 4次相乘结果的平方
void matrixPowerEnhance(int n, struct matrix_struct *matrix_structY) {
if (n > 1) {
matrixPower(n / 2, matrix_structY);
matrixMultiply(*matrix_structY, matrix_structY);
}
if (n & 0x1) {
// n奇数
struct matrix_struct matrix_structX;
matrix_structX.matrix[0][0] = 1;
matrix_structX.matrix[0][1] = 1;
matrix_structX.matrix[1][0] = 1;
matrix_structX.matrix[1][1] = 0;
matrixMultiply(matrix_structX, matrix_structY);
}
}
折半操作还可以持续重复,因为折半收敛非常快
struct matrix_struct matrixMultiply1(struct matrix_struct matrix_structA, struct matrix_struct matrix_structB) { struct matrix_struct matrix_structX; for (int i = 0; i < 2; i++) { for (int j = 0; j < 2; j++) { matrix_structX.matrix[i][j] = matrix_structA.matrix[i][0] * matrix_structB.matrix[0][j] + matrix_structA.matrix[i][1] * matrix_structB.matrix[1][j]; } } return matrix_structX; } struct matrix_struct matrixPowerEnhance1(int n, struct matrix_struct matrix_structY) { struct matrix_struct result = matrix_structY; if (n > 1) { result = matrixPowerEnhance1(n / 2, matrix_structY); result = matrixMultiply1(result, result); if (n & 0x1) { // n奇数 struct matrix_struct matrix_structX; matrix_structX.matrix[0][0] = 1; matrix_structX.matrix[0][1] = 1; matrix_structX.matrix[1][0] = 1; matrix_structX.matrix[1][1] = 0; result = matrixMultiply1(matrix_structX, result); } return result; } struct matrix_struct matrix_structX; matrix_structX.matrix[0][0] = 1; matrix_structX.matrix[0][1] = 1; matrix_structX.matrix[1][0] = 1; matrix_structX.matrix[1][1] = 0; return matrixMultiply1(matrix_structX, matrix_structY); }
虽然递归了,但是每次递归的作用是为了 折半
如此 原来的执行次数n 做了个折扣处理,2^t = n
即 t = log(n)
时间复杂度变为了 log(n)
通过最基础的斐波那契,我们可以先通过自己的理解,在自己能力范围内先实现算法
在实现的基础上分析复杂度问题
在自己实现的版本基础上发现可优化的点,比如常规的递归实现,包含了大量的重复执行,去掉不必要的执行,其实就是在优化算法了
算法的复杂度优化是一步步循序渐进完善的,并非一蹴而就,也不是刷多少算法能解决的,其实是一种本质的思维方式
没有什么高深的点,守得住耐心,总会找到完善的空间
不抛弃底层的本质思维,不眼高手低,慢慢就会触及到一些思想的门槛,比如文章中的优化思路就摸到了 动态规划的思想
有了这个引子,再去看一些算法思想,看别人的实现,才会深有体会,不致于过眼烟云
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。