[最全算法总结]我是如何将递归算法的复杂度优化到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 增大时浮点误差不断增大会导致返回结果不正确甚至数据溢出。

目录
相关文章
|
2月前
|
机器学习/深度学习 存储 算法
如何评判算法好坏?复杂度深度解析
如何评判算法好坏?复杂度深度解析
24 0
|
2月前
|
存储 算法
数据结构与算法:复杂度
数据结构: 数据结构是用于存储和组织数据的方式,以便可以有效地访问和修改数据。不同的数据结构适用于不同类型的应用,并且具体的数据结构可以大幅影响程序的性能。数据结构分为两大类:线性数据结构和非线性数据结构。 算法: 算法是完成特定任务的一系列操作步骤,是解决问题的明确规范。算法的效率通常通过时间复杂度和空间复杂度来评估,即算法执行所需的时间和空间资源。
|
2月前
|
算法
经典控制算法——PID算法原理分析及优化
这篇文章介绍了PID控制算法,这是一种广泛应用的控制策略,具有简单、鲁棒性强的特点。PID通过比例、积分和微分三个部分调整控制量,以减少系统误差。文章提到了在大学智能汽车竞赛中的应用,并详细解释了PID的基本原理和数学表达式。接着,讨论了数字PID的实现,包括位置式、增量式和步进式,以及它们各自的优缺点。最后,文章介绍了PID的优化方法,如积分饱和处理和微分项优化,以及串级PID在电机控制中的应用。整个内容旨在帮助读者理解PID控制的原理和实际运用。
94 1
|
2月前
|
机器学习/深度学习 算法 Oracle
ICLR 2024:近似最优的最大损失函数量子优化算法
【2月更文挑战第27天】ICLR 2024:近似最优的最大损失函数量子优化算法
33 3
ICLR 2024:近似最优的最大损失函数量子优化算法
|
28天前
|
存储 算法 编译器
掌握Go语言:探索Go语言递归函数的高级奥秘,优化性能、实现并发、解决算法难题(28)
掌握Go语言:探索Go语言递归函数的高级奥秘,优化性能、实现并发、解决算法难题(28)
|
4天前
|
机器学习/深度学习 自然语言处理 算法
深度解析深度学习中的优化算法:从梯度下降到自适应方法
【4月更文挑战第28天】 在深度学习模型训练的复杂数学迷宫中,优化算法是寻找最优权重配置的关键导航者。本文将深入探讨几种主流的优化策略,揭示它们如何引导模型收敛至损失函数的最小值。我们将比较经典的批量梯度下降(BGD)、随机梯度下降(SGD)以及动量概念的引入,进一步探索AdaGrad、RMSProp和Adam等自适应学习率方法的原理与实际应用。通过剖析这些算法的理论基础和性能表现,我们旨在为读者提供一个关于选择合适优化器的参考视角。
|
5天前
|
算法 索引
数据结构与算法-并查集多种实现以及优化步骤
数据结构与算法-并查集多种实现以及优化步骤
7 0
|
7天前
|
机器学习/深度学习 人工智能 算法
揭秘深度学习中的优化算法
【4月更文挑战第24天】 在深度学习的广阔天地中,优化算法扮演着至关重要的角色。本文将深入探讨几种主流的优化算法,包括梯度下降法、随机梯度下降法、Adam等,并分析它们的特点和适用场景。我们将通过理论分析和实例演示,揭示这些优化算法如何帮助模型更高效地学习参数,从而提高模型的性能。
|
7天前
|
人工智能 达摩院 算法
什么是优化技术?给算法小白同学的快速讲解和上手文
本文作者用一个曾经小白学习的视角,来讲解什么是优化问题,以及要如何用这个优化技术。
|
14天前
|
算法
PID算法原理分析及优化
这篇文章介绍了PID控制方法,这是一种广泛应用的控制算法,具有结构简单、鲁棒性强等特点。PID通过比例、积分和微分三个部分调整控制量,以减少系统输出与目标值的偏差。文章详细阐述了PID的基本原理,包括比例、积分和微分调节的作用,并提到积分饱和和微分项振荡的问题以及对应的优化策略,如积分分离、变速积分和微分先行等。此外,还提到了数字PID的实现形式,如位置式、增量式和步进式,以及串级PID在电机控制等领域的应用。
24 10