浅析算法的时间复杂度和空间复杂度 (C++/python双语实例)

简介: 浅析算法的时间复杂度和空间复杂度 (C++/python双语实例)

如何衡量一个算法的好坏呢?

一个算法如果写的十分的短,是不是就非常的好呢?

例如斐波那契数列:

49a55fcc6bd260c8f0321d20a7fe94d3.jpg


C++:

#include <iostream>
#include <iomanip>
#include <cmath>
using namespace std;
#define M_SQRT5      2.2360679774997896964091736687313
#define M_1pSQRT5_2  1.6180339887498948482045868343656
long double Fibo(size_t n)
{
  return round((pow(M_1pSQRT5_2,n)-pow(1-M_1pSQRT5_2,n))/M_SQRT5);
}
int main(void)
{ 
    for (long n=1;n<=100;n++)
      cout<<n<<"\t= "<<setprecision(21)<<Fibo(n)<<endl;
  cout << "... ..." << endl;
    for (long n=1470;n<=1480;n++)
      cout<<n<<"\t= "<<setprecision(21)<<Fibo(n)<<endl;
  return 0;
}


python:

1. def Fib(n:int)->int:
2.     return 1 if n<3 else Fib(n-1)+Fib(n-2)



以上代码,c++和python的主体函数都只有一行return语句,一个是使用数列通项公式(又称比内公式),一个使用递归法;前者局限于long double的精度,后者局限于递归层数的限制。


通常情况下,计算斐波那契数列多数会使用数列定义的递推公式来递归,但当n大于30时计算就非常吃力了;想要计算第100万项的值基本上是不可能的了。


>>> from time import time
>>> for i in range(30,40):
  t=time();n=Fib(i);print(i,time()-t)
30 0.42186927795410156
31 0.6874938011169434
32 1.1093668937683105
33 1.7812433242797852
34 2.8906190395355225
35 4.640621185302734
36 7.48436975479126
37 12.109369277954102
38 19.6406192779541
39 31.74999499320984


用Fib(n-1)+Fib(n-2)递归只能一项项往前推,太慢了;我样可以这样改进,用斐波数列的性质(如下所示)来递归,效率明显提升:计算第10万项秒杀,第100万项20秒以内。

   由 F(1)=F(2)=1; F(n)=F(n-1)+F(n-2) 推导出:

   F(2n)=F(n+1)*F(n)+F(n)*F(n-1) ; F(2n+1)=F²(n+1)+F²(n)



>>> def Fib(n:int)->int:
    if n<3: return 1
    if n%2: return Fib(n//2)**2+Fib(n//2+1)**2
    return Fib(n//2)*(Fib(n//2+1)+Fib(n//2-1))
>>> from time import time
>>> t=time();n=Fib(100);print(time()-t)
0.0
>>> t=time();n=Fib(1000);print(time()-t)
0.015600204467773438
>>> t=time();n=Fib(10000);print(time()-t)
0.062400102615356445
>>> t=time();n=Fib(100000);print(time()-t)
0.9536018371582031
>>> t=time();n=Fib(1000000);print(time()-t)
18.566032886505127



此函数已知项只有F(1)、F(2)两项,若增加已知项的项数也能提升一点效率,计算第100万项的值只要3秒不到:

>>> def Fib(n:int)->int:
    if n<11: return [0,1,1,2,3,5,8,13,21,34,55][n]
    t=n//2
    if n%2: return Fib(t)**2+Fib(t+1)**2
    return Fib(t)*(Fib(t+1)+Fib(t-1))
>>> from time import time
>>> t=time();n=Fib(1000);print(time()-t)
0.0
>>> t=time();n=Fib(10000);print(time()-t)
0.017600059509277344
>>> t=time();n=Fib(100000);print(time()-t)
0.22040081024169922
>>> t=time();n=Fib(1000000);print(time()-t)
2.76320481300354
>>> 

那么我们要用什么方式去衡量一个算法的好坏,所以我们引进了时间复杂度和空间复杂度。 时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所占用空间。

 



时间复杂度


时间复杂度的概念


衡量一个算法的运行时间快慢也就是时间复杂度,那么计量的单位是什么?如果用我们常用的时间单位例如:秒、毫秒。这就存在一个问题,不同机器由于配置不同时间会不相同,而且要想知道时间就必须上机跑代码很麻烦。所以我们把算法花费的时间与其中语句执行次数成正比,算法中的基本操作执行个数,为算法的时间复杂度。



>>> def fun(n:int)->None:
  count = 0;
  for i in range(n):
    for j in range(n):
      count += 1
  for i in range(n*2):
    count += 1
  m = 10
  while m:
    count += 1
    m -= 1
  print(count)
>>> fun(10)
130
>>> fun(100)
10210
>>> 


由这个函数中的全局变量count,我们很容易知道执行了F(N)=N^2+2*N+10

其实我们在计算精确的执行次数,而只需要大概执行次数,这里我们使用大O的渐进表示法。




大O的渐进表示法

大O符号(Big O notation):用于描述函数渐进行为的数学符号。常见复杂度如下:

image.png

大O复杂度操作数曲线:

31a2fc33887ca765ade96fe70503a362.png



常见的时间复杂度举例


例一:常量级

C++:

#include<iostream>
using namespace std;
int func(int n){
  int i = 0;
  for(int j=0;j<1000;i++,j++);
  return i;
}
int main(void){
  cout << func(100) << endl;
  cout << func(1000) << endl;
  return 0;
}


python:

>>> def fun(n:int)->int:
  i = 0
  for j in range(1000):
    i += 1
  return i
>>> fun(100)
1000
>>> fun(1000)
1000

时间复杂度为O(1000)但是如果为常数的话,时间复杂度可以直接写为O(1)


例二:阶乘之和 1!+2!+3!+...+n!

C++:

#include<iostream>
using namespace std;
long long func(int n){
  long long sum = 1;
  for(int i=n;i>1;i--){
    sum *= i;
    sum += 1;
  }
  return sum;
}
int main(void){
  cout << func(10) << endl;
  cout << func(20) << endl;
  return 0;
}


python:

def factSum(n):
    Sum = 1
    for i in range(n,1,-1):
        Sum *= i
        Sum += 1
    return Sum
def factSum2(n):
  Sum = 0
  from math import factorial as fact
  for i in range(1,n+1):
    Sum+=fact(i)
  return Sum

时间复杂度为O(N)

例三:冒泡排序

C++:

#include<iostream>
#include<cmath>
using namespace std; 
int main(void) { 
  double a[100];
  int N; 
  int i = 0, j = 0;
  cin >> N;
  for (i = 0; i<N; i++)
    cin >> a[i];
  for (i = 0; i<N - 1; i++) { 
    for (j = 0; j<N - 1 - i; j++)
    {
      if (a[j]>a[j + 1]) {
        int tmp;
        tmp = a[j];
        a[j] = a[j + 1];
        a[j + 1] = tmp;
      }
    }
  }
  for (i = 0; i<N; i++){
    cout << a[i] << " "; 
  }
  cout << endl;
  return 0;
}



python:

>>> def Bubble(a:list)->None:
  for i in range(len(a)-1):
    for j in range(len(a)-1-i):
      if a[j]<a[j+1]:
        a[j],a[j+1]=a[j+1],a[j]
>>> b = [*range(1,6)]
>>> Bubble(b)
>>> b
[5, 4, 3, 2, 1]
>>> 


由这个程序我们可以知道F(N)=N-1+N-2+.....+1=(N^2-N)/2

例四:二分查找

C++:

template<class T>  
int Binary_Search(T *x, int N, T keyword)  
{  
    int low = 0, high = N-1,mid;  
    while(low <= high)  
    {  
        mid = (low + high)/2;  
        if(x[mid] == keyword)  
            return mid;  
        if(x[mid] < keyword)  
            low = mid + 1;  
         else  
            high = mid -1;  
    }  
    return -1;  
}  
int Binary_Search2(int *a, int low, int high, int key)  
{  
     if(low > hign) 
          return -1;
     int mid = (low + high) / 2;  
     if(a[mid] == key)  
          return mid;  
     if(a[mid] > key)  
          return Binary_Search2(a, low, mid-1, key); 
     else  
          return Binary_Search2(a, mid+1, high, key);
}  


python:

def binSearch(a:list,x:int):
  left,right = 0,len(a)
  while left<=right:
    mid = (left+right)//2
    if x>a[mid]:
      left = mid+1
    elif x<a[mid]:
      right = mid-1
    elif x==a[mid]:
      return mid
  return -1

这个算法对于每次查找,找不到就会将范围缩小二倍,时间复杂度也就是O(log2 N)

例五:递归阶乘

C++:

#include<iostream>
using namespace std;
long long func(int n){
  long long sum = 1;
  for(int i=n;i>0;i--)
    sum *= i;
  return sum;
}
int main(void){
  cout << func(10) << endl;
  cout << func(20) << endl;
  return 0;
}

python:

>>> def F(n:int)->int:
  if n: return F(n-1)*n
  return 1
>>> F(0)
1
>>> F(1)
1
>>> F(2)
2
>>> F(3)
6
>>> F(4)
24
>>> F(5)
120
>>> 

这个也非常简单return的 值为F(N-1)*F(N-2)........F(0)一共是N+1个,所以中间的代码也就执行了N+1次,所以时间复杂度是O(N)

例六:斐波那契数列

C++:

#include<iostream>
using namespace std;
long long func(int n){
  if (n<3) return 1;
  return func(n-1)+func(n-2);
}
int main(void){
  cout << func(10) << endl;
  cout << func(20) << endl;
  return 0;
}

python:

1. def F(n:int)->int:
2. if n<3: return 1
3. return F(n-1)+F(n-2)

这个函数的时间复杂度很多文章都说它是指数级O(2ⁿ)的,但我还是用python全局变量法来测试:

>>> def F1(n):
  global count
  count += 1
  if n<3: return 1
  return F1(n-1)+F1(n-2)
>>> count=0; F1(20),count
(6765, 13529)
>>> count=0; F1(25),count
(75025, 150049)
>>> count=0; F1(30),count
(832040, 1664079)
>>> 6765*2,75025*2,832040*2
(13530, 150050, 1664080)
>>> 6765*2-1,75025*2-1,832040*2-1
(13529, 150049, 1664079)
>>> 


看全局变量n终值的测试结果,可知: F(n)的运算次数 = 2*F(n) - 1,其中F(n)的通项公式(也就是比内公式)为:

再来使用cProfile库函数Profile()来验证一下:

>>> from cProfile import Profile
>>> def F(n:int)->int:
    if n<3: return 1
    return F(n-1)+F(n-2)
>>> cp = Profile()
>>> cp.enable(); n = F(30); cp.disable()
>>> cp.print_stats()
         1664081 function calls (3 primitive calls) in 0.708 seconds
   Ordered by: standard name
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
1664079/1    0.708    0.000    0.708    0.708 <pyshell#7>:1(F)
        1    0.000    0.000    0.000    0.000 rpc.py:614(displayhook)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
>>> 2*F(30)-1
1664079
>>> 2**30
1073741824
>>> 30**3
27000
>>> 


结果cp.print_stats()输出的function  calls的值正好也等于2*F(30)-1,所以我认为这个函数的时间复杂度应在立方级O(n³)和指数级O(2ⁿ)之间,比O(n³)大好几十倍但要比O(2ⁿ)小两到三个数量级,实际上应该是这样一个“数量级”:


20210911204255127.png




空间复杂度


空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用储存空间大小的度量。


空间复杂度不是程序占用了多少bytes空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。也用O()来表示。


注意:函数运行所需要的栈空间在编译的时候就已经确认好了,因此空间复杂度主要通过函数在运行时显示申请的空间来确定。


例一:冒泡排序

源代码略(参见上一节内容空间复杂度,以下同)实际上这里开辟的空间只有i,j  所以空间复杂度为O(1)。


例二:阶乘函数

答案是O(N)

这里如果对函数栈帧不是很了解的话,这个原理也就不会知道。函数栈帧可以简单的理解为:函数每被调用一次,都会在栈区上开辟一块空间,所以F(N)会调用F(N-1),F(N-1)会调用F(N-2)…同时会调用N个F,每个F都会开辟一块空间,所以空间复杂度为:O(N)。


示例三:斐波那契数列

通过上面的实例我们就可以知道斐波那契数列实际上开辟了N个Fib函数空间,只是这些函数空间会被重复利用,总共被运行2^n-1次,所以空间复杂度为:O(N)。


(本篇完)



目录
相关文章
|
9天前
|
机器学习/深度学习 安全 算法
【图论】【割点】【C++算法】928. 尽量减少恶意软件的传播 II
【图论】【割点】【C++算法】928. 尽量减少恶意软件的传播 II
|
2天前
|
算法 数据可视化 Python
Python贝叶斯推断Metropolis-Hastings(M-H)MCMC采样算法的实现
Python贝叶斯推断Metropolis-Hastings(M-H)MCMC采样算法的实现
|
2天前
|
数据可视化 算法 数据挖掘
PYTHON实现谱聚类算法和改变聚类簇数结果可视化比较
PYTHON实现谱聚类算法和改变聚类簇数结果可视化比较
|
2天前
|
算法 数据可视化 Python
Python中LARS和Lasso回归之最小角算法Lars分析波士顿住房数据实例
Python中LARS和Lasso回归之最小角算法Lars分析波士顿住房数据实例
11 0
|
3天前
|
机器学习/深度学习 算法 Python
使用Python实现集成学习算法:Bagging与Boosting
使用Python实现集成学习算法:Bagging与Boosting
15 0
|
4天前
|
缓存 算法 Python
python算法对音频信号处理Sonification :Gauss-Seidel迭代算法
python算法对音频信号处理Sonification :Gauss-Seidel迭代算法
|
7天前
|
算法 数据可视化 数据挖掘
使用Python实现DBSCAN聚类算法
使用Python实现DBSCAN聚类算法
139 2
|
8天前
|
存储 算法 安全
Python加密算法有哪些?有什么作用?
这些加密算法的作用在于保护敏感数据的隐私和完整性。它们可以用于数据传输、存储、身份验证和数字签名等领域。通过加密,可以确保数据在传输和存储过程中不被未经授权的人访问或篡改。同时,数字签名可以用于验证数据的来源和完整性,防止数据被篡改或冒充。不同的加密算法在不同的应用场景中起到不同的作用,选择合适的算法取决于安全需求和性能要求。 买CN2云服务器,免备案服务器,高防服务器,就选蓝易云。百度搜索:蓝易云
6 0
|
9天前
|
算法 数据可视化 数据挖掘
使用Python实现K均值聚类算法
使用Python实现K均值聚类算法
15 1
|
9天前
|
算法 测试技术 C#
【广度优先搜索】【堆】【C++算法】407. 接雨水 II
【广度优先搜索】【堆】【C++算法】407. 接雨水 II

热门文章

最新文章