[最全算法总结]我是如何将递归算法的复杂度优化到O(1)的

简介: [最全算法总结]我是如何将递归算法的复杂度优化到O(1)的

相信提到斐波那契数列,大家都不陌生,这个是在我们学习 C/C++ 的过程中必然会接触到的一个问题,而作为一个经典的求解模型,我们怎么能少的了去研究这个模型呢?笔者在不断地学习和思考过程中,发现了这类经典模型竟然有如此多的有意思的求解算法,能让这个经典问题的时间复杂度降低到 O(1) ,下面我想对这个经典问题的求解做一个较为深入的剖析,请听我娓娓道来。

我们可以用如下递推公式来表示斐波那契数列 F 的第 n项:


image.png


回顾一下我们刚开始学 C 语言的时候,讲到函数递归那节,老师总是喜欢那这个例子来说。

斐波那契数列就是像蜗牛的壳一样,越深入其中,越能发觉其中的奥秘,形成一条条优美的数学曲线,就像这样:


1100338-20190716084548622-1052984394.png


递归在数学与计算机科学中,是指在函数的定义中使用函数自身的方法,可能有些人会把递归和循环弄混淆,我觉得务必要把这一点区分清楚才行。

递归查找

举个例子,给你一把钥匙,你站在门前面,问你用这把钥匙能打开几扇门。

递归:你打开面前这扇门,看到屋里面还有一扇门(这门可能跟前面打开的门一样大小,也可能门小了些),你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开。若干次之后,你打开面前一扇门,发现只有一间屋子,没有门了。 你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这钥匙开了几扇门。

循环:你打开面前这扇门,看到屋里面还有一扇门,(这门可能跟前面打开的门一样大小,也可能门小了些),你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,(前面门如果一样,这门也是一样,第二扇门如果相比第一扇门变小了,这扇门也比第二扇门变小了),你继续打开这扇门,一直这样走下去。 入口处的人始终等不到你回去告诉他答案。

简单来说,递归就是有去有回,循环就是有去无回

我们可以用如下图来表示程序中循环调用的过程:


1100338-20190716084542422-1142462169.jpg


于是我们可以用递归查找的方式去实现上述这一过程。

时间复杂度:O(2n)O(2n)

空间复杂度:O(1)O(1)

/**
递归实现
*/
int Fibonacci_Re(int num){
  if(num == 0){
    return 0;
  }
  else if(num == 1){
    return 1;
  }
  else{
    return Fibonacci_Re(num - 1) + Fibonacci_Re(num - 2);
  }
}

线性递归查找


It's amazing!!!如此高的时间复杂度,我们定然是不会满意的,该算法有巨大的改进空间。我们是否可以在某种意义下对这个递归过程进行改进,来优化这个时间复杂度。还是从上面这个开门的例子来讲,我们经历了顺路打开门和原路返回数门这两个过程,我们是不是可以考虑在边开门的过程中边数我们一路开门的数量呢?这对时间代价上会带来极大的改进,那我们想想看该怎么办呢?


为消除递归算法中重复的递归实例,在各子问题求解之后,及时记录下其对应的解答。比如可以从原问题出发自顶向下,每当遇到一个子问题,都首先查验它是否已经计算过,以此通过直接调阅纪录获得解答,从而避免重新计算。也可以从递归基出发,自底而上递推的得出各子问题的解,直至最终原问题的解。前者即为所谓的制表或记忆策略,后者即为所谓的动态规划策略。


为应用上述的制表策略,我们可以从改造 FibonacciFibonacci 数的递归定义入手。我们考虑转换成如下的递归函数,即可计算一对相邻的Fibonacci数:


(Fibonacci_Re(k1),Fibonacci_Re(k1))(Fibonacci_Re(k−1),Fibonacci_Re(k−1)),得到如下更高效率的线性递归算法。


时间复杂度:O(n)

空间复杂度:O(n)

/**
线性递归实现
*/
int Fibonacci_Re(int num, int& prev){
  if(num == 0){
    prev = 1;
    return 0;
  }
  else{
    int prevPrev;
    prev = Fibonacci_Re(num - 1, prevPrev);
    return prevPrev + prev;
  }
}

该算法呈线性递归模式,递归的深度线性正比于输入 num ,前后共计仅出现 O(n) 个实例,累计耗时不超过 O(n)。遗憾的是,该算法共需要使用 O(n)规模的附加空间。如何进一步改进呢?

减而治之


若将以上逐层返回的过程,等效地视作从递归基出发,按规模自小而大求解各子问题的过程,即可采用动态规划的过程。我们完全可以考虑通过增加变量的方式代替递归操作,牺牲少量的空间代价换取时间效率的大幅度提升,于是我们就有了如下的改进方式,通过中间变量保存 F(n1))F(n2),利用元素的交换我们可以实现上述等价的一个过程。此时在空间上,我们由 O(1)变成了 O(4),由于申请的空间数量仍为常数个,我们可以近似的认为空间效率仍为 O(1)


时间复杂度:O(n)

空间复杂度:O(1)


/**
非递归实现(减而治之1)
*/
int Fibonacci_No_Re(int num){
  if(num == 0){
    return 0;
  }
  else if(num == 1){
    return 1;
  }
  else{
    int a = 0;
    int b = 1;
    int c = 1;
    while(num > 2){
      a = b;
      b = c;
      c = a + b;
      num--;
    }
    return c;
  }
}

我们甚至还可以对变量的数量进行优化,将 O(4)O(4) 变成了 O(3)O(3),减少一个单位空间的浪费,我们可以实现如下这一过程:

/**
非递归实现(减而治之2)
*/
int Fibonacci_No_Re(int num){
  int a = 1;
  int b = 0;
  while(0 < num--){
    b += a;
    a = b - a;
  }
  return b;
}

分而治之(二分查找)


而当我们面对输入相对较为庞大的数据时,每每感慨于头绪纷杂而无从下手的你,不妨先从孙子的名言中获取灵感——“凡治众如治寡,分数是也”。是的,解决此类问题的最有效方法之一,就是将其分解为若干规模更小的子问题,再通过递归机制分别求解。这种分解持续进行,直到子问题规模缩减至平凡情况,这也就是所谓的分而治之策略。

与减而治之策略一样,这里也要求对原问题重新表述,以保证子问题与原问题在接口形式上的一致。既然每一递归实例都可能做多次递归,故称作为多路递归。我们通常都是将原问题一分为二,故称作为二分递归。

按照二分递归的模式,我们可以再次求和斐波那契求和问题。


时间复杂度:O(log(n)

空间复杂度:O(1)O(1)

/**
二分查找(递归实现)
*/
int binary_find(int arr[], int num, int arr_size, int left, int right){
  assert(arr);
  int mid = (left + right) / 2;
  if(left <= right){
    if(num < arr[mid]){
      binary_find(arr, num, arr_size, left, mid - 1);
    }
    else if(num > arr[mid]){
      binary_find(arr, num, arr_size, mid + 1, right);
    }
    else{
      return mid;
    }
  }
}

当然我们也可以不采用递归模式,按照上面的思路,仍采用分而治之的模式进行求解。

时间复杂度:O(log(n))

空间复杂度:O(1)

/**
二分查找(非递归实现)
*/
int binary_find(int arr[], int num, int arr_size){
  if(num == 0){
    return 0;
  }
  else if(num == 1){
    return 1;
  }
    int left = 0;
    int right = arr_size - 1;
    while(left <= right){
      int mid = (left + right) >> 1;
      if(num > arr[mid]){
          left = mid + 1;
      }
      else if(num < arr[mid]){
          right = mid - 1;
      }
      else{
          return mid;
      }
  }
    return -1;
}

矩阵快速幂

为了正确高效的计算斐波那契数列,我们首先需要了解以下这个矩阵等式:

image.png

为了推导出这个等式,我们首先有:


image.png

随即得到:


image.png


又由于F(1)=1F(0)=0F(1)=1,则我们得到了开始给出的矩阵等式。当然,我们也可以通过数学归纳法来证明这个矩阵等式。等式中的矩阵


image.png

被称为斐波那契数列的 Q- 矩阵。

通过 Q- 矩阵,我们可以利用如下公式进行计算 Fn:

image.png


如此一来,计算斐波那契数列的问题就转化为了求 Q 的 n−1 次幂的问题。我们使用矩阵快速幂的方法来达到 O(log(n)) 的复杂度。借助分治的思想,快速幂采用以下公式进行计算:


image.png


实现过程如下:

时间复杂度:O(log(n))

空间复杂度:O(1)

//矩阵数据结构定义
#define MOD 100000
struct matrix{
    int a[2][2];
}
//矩阵相乘函数的实现
matrix mul_matrix{
    matrix res;
    memset(res.a, 0, sizeof(res.a));
    for(int i = 0; i < 2; i++){
      for(int j = 0; i < 2; j++){
        for(int k = 0; k < 2; k++){
          res.a[i][j] += x.a[i][k] * y.a[k][j];
          res.a[i][j] %= MOD;
        }
      }
    }
    return res;
}
int pow(int n)
{
    matrix base, res;
    //将res初始化为单位矩阵
    for(int i = 0; i < 2; i++){
      res.a[i][i] = 1;
    }
    //给base矩阵赋予初值
    base.a[0][0] = 1;
    base.a[0][1] = 1;
    base.a[1][0] = 1;
    base.a[1][1] = 0;
    while(n > 0)
    {
        if(n % 2 == 1){
          res *= base;
        }
        base *= base;
        n >>= 1;//n = n / 2;
    }
    return res.a[0][1];//或者a[1][0]
}

对于斐波那契数列,我们还有以下这样的递推公式:

image.png

为了得到以上递归式,我们依然需要利用 Q- 矩阵。由于image.png 展开得到:

image.png


将该式中 n替换为 n+1 可得:

image.png


在如上两个等式中令 m=nm=n,则可得到开头所述递推公式。利用这个新的递归公式,我们计算斐波那契数列的复杂度也为 O(log(n)),并且实现起来比矩阵的方法简单一些:

时间复杂度:O(log(n))

空间复杂度:O(1)

int Fibonacci_recursion_fast(int num){
  if(num == 0){
    return 0;
  }
  else if(num == 1){
    return 1;
  }
  else{
    int k = num % 2 ? (num + 1) / 2 : num / 2;
    int fib_k = Fibonacci_recursion_fast(k);
    int fib_k_1 = Fibonacci_recursion_fast(k - 1);
    return num % 2 ? power(fib_k, 2) + power(fib_k_1, 2) : (2 * fib_k_1 + fib_k) * fib_k;
  }
}

公式法

我们还有没有更快的方法呢?对于斐波那契数列这个常见的递推数列,其第 nn 项的值的通项公式如下:

image.png

既然作为工科生,那肯定要用一些工科生的做法来证明这个公式呀,嘿嘿,下面开始我的表演~


我们回想一下,斐波那契数列的所有的值可以看成在数轴上的一个个离散分布的点的集合,学过数字信号处理或者自动控制原理的同学,这个时候,我们很容易想到用Z变换来求解该类问题。

1100338-20190716084536018-556307712.jpg

Z变换常用的规则表如下:


n>1 时,由 f(n)=f(n1)+f(n2) (这里我们用小写的 ff 来区分):

由于 n>=0,所以我们可以把其表示为f(n+2)=f(n+1)+f(n),其中 n>=0

所以我们利用上式前向差分方程,两边取 ZZ 变换可得:

image.png


所以有:

image.png


f(0)=0,f(1)=1整理可得:

image.png

我们取 Z的逆变换可得:

image.png

我们最终可以得到如下通项公式:

image.png



更多的证明方法可以参考知乎上的一些数学大佬:


https://www.zhihu.com/question/25217301

实现过程如下:


时间复杂度:O(1)

空间复杂度:O(1)

/**
纯公式求解
*/
int Fibonacci_formula(int num){
  double root_five = sqrt(5 * 1.0);
  int result = ((((1 + root_five) / 2, num)) - (((1 - root_five) / 2, num))) / root_five
  return result;
}

该方法虽然看起来高效,但是由于涉及大量浮点运算,在 nn 增大时浮点误差不断增大会导致返回结果不正确甚至数据溢出。

目录
相关文章
|
1月前
|
机器学习/深度学习 人工智能 自然语言处理
深度学习中的优化算法及其应用
【10月更文挑战第8天】 本文将探讨深度学习中常用的优化算法,包括梯度下降法、Adam和RMSProp等,介绍这些算法的基本原理与应用场景。通过实例分析,帮助读者更好地理解和应用这些优化算法,提高深度学习模型的训练效率与性能。
139 63
|
11天前
|
算法 调度
基于遗传模拟退火混合优化算法的车间作业最优调度matlab仿真,输出甘特图
车间作业调度问题(JSSP)通过遗传算法(GA)和模拟退火算法(SA)优化多个作业在并行工作中心上的加工顺序和时间,以最小化总完成时间和机器闲置时间。MATLAB2022a版本运行测试,展示了有效性和可行性。核心程序采用作业列表表示法,结合遗传操作和模拟退火过程,提高算法性能。
|
11天前
|
人工智能 算法 大数据
Linux内核中的调度算法演变:从O(1)到CFS的优化之旅###
本文深入探讨了Linux操作系统内核中进程调度算法的发展历程,聚焦于O(1)调度器向完全公平调度器(CFS)的转变。不同于传统摘要对研究背景、方法、结果和结论的概述,本文创新性地采用“技术演进时间线”的形式,简明扼要地勾勒出这一转变背后的关键技术里程碑,旨在为读者提供一个清晰的历史脉络,引领其深入了解Linux调度机制的革新之路。 ###
|
21天前
|
人工智能 算法 数据安全/隐私保护
基于遗传优化的SVD水印嵌入提取算法matlab仿真
该算法基于遗传优化的SVD水印嵌入与提取技术,通过遗传算法优化水印嵌入参数,提高水印的鲁棒性和隐蔽性。在MATLAB2022a环境下测试,展示了优化前后的性能对比及不同干扰下的水印提取效果。核心程序实现了SVD分解、遗传算法流程及其参数优化,有效提升了水印技术的应用价值。
|
21天前
|
存储 缓存 算法
优化轮询算法以提高资源分配的效率
【10月更文挑战第13天】通过以上这些优化措施,可以在一定程度上提高轮询算法的资源分配效率,使其更好地适应不同的应用场景和需求。但需要注意的是,优化策略的选择和实施需要根据具体情况进行详细的分析和评估,以确保优化效果的最大化。
|
22天前
|
并行计算 算法 IDE
【灵码助力Cuda算法分析】分析共享内存的矩阵乘法优化
本文介绍了如何利用通义灵码在Visual Studio 2022中对基于CUDA的共享内存矩阵乘法优化代码进行深入分析。文章从整体程序结构入手,逐步深入到线程调度、矩阵分块、循环展开等关键细节,最后通过带入具体值的方式进一步解析复杂循环逻辑,展示了通义灵码在辅助理解和优化CUDA编程中的强大功能。
|
22天前
|
存储 缓存 算法
前端算法:优化与实战技巧的深度探索
【10月更文挑战第21天】前端算法:优化与实战技巧的深度探索
19 1
|
23天前
|
机器学习/深度学习 算法 数据安全/隐私保护
基于贝叶斯优化CNN-LSTM网络的数据分类识别算法matlab仿真
本项目展示了基于贝叶斯优化(BO)的CNN-LSTM网络在数据分类中的应用。通过MATLAB 2022a实现,优化前后效果对比明显。核心代码附带中文注释和操作视频,涵盖BO、CNN、LSTM理论,特别是BO优化CNN-LSTM网络的batchsize和学习率,显著提升模型性能。
|
30天前
|
存储 缓存 算法
如何通过优化算法和代码结构来提升易语言程序的执行效率?
如何通过优化算法和代码结构来提升易语言程序的执行效率?
|
30天前
|
机器学习/深度学习 人工智能 算法
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解
70 0
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解