关于时间复杂度与空间复杂度
2017-04-19 16:16
204 查看
对于评价一个算法的好坏,我们通常以时间复杂度与空间复杂度来衡量。
时间复杂度:算法中执行基本操作语句的总次数。
空间复杂度:算法中创建对象的个数。
一.时间复杂度
这里有一点需要注意:对于时间复杂度而言,我们并没有算法的运行总时间来衡量算法的好坏,这里最重要的原因是对于一个算法的运行时间,它受外在的其他因素的影响较大,而且能够影响时间的因素又有很多。
而对于一个算法的时间复杂度,我们可以大致分为三种情况:
1.最坏情况:任意输入规模的最大运行次数(上界);
2.平均情况:任意输入规模的期望运行次数;
3.最好情况:任意输入规模的最小运行次数,通常最好情况不会出现。(下界)
而我们在实际中我们通常情况考量的是算法的最坏运行情况。也就是说对于任意输入规模N,算法的最长运行时间,理由如下:
1. 一个算法的最坏情况的运行时间是在任意输入下的运行时间上界。
2. 对于某些算法,最坏的情况出现的较为频繁。
3. 大体上看,平均情况与最坏情况一样差。
算法分析要保持大局观:
1. 忽略掉那些的常数。
2. 关注运行时间的增长趋势,关注函数式中增长最快的表达式。
由此,我们通常通过O的渐进表达式来表示我们的时间复杂度,而我们原本用f(n)也就是算法中所有基本语句的总次数来衡量,而对于O渐进表达式我们是通过f(n)来求的,也就是O(f(n))的求法如下:
令n趋近无穷大,忽略对这个式子的影响最小项,以常数1来代替所有的常数项,只保留最高项,并且如果最高项系数为常数,则令最高项系数为1
二.空间复杂度
对于我们的空间复杂度,其实与时间复杂度类似,它也通常用O渐进表达式来表示,只不过时间复杂度的f(n)是我们算法中的所有基本语句总次数,而这里f(n)则是算法中创建对象的个数,而通过f(n)来求O(f(n))的方法都是一样的。
下面给出两个算法,来求它们的时间复杂度与空间复杂度:
1.二分查找(非递归版本)
int BinarySearch(int* arr,int size,int data)
{
int left=0;
int right=size;
int mid=0;
while(left<right)
{
mid=(left&right)+((left^right)>>1);
if(arr[mid]==data)
return mid;
else if(arr[mid]>data)
{
right=mid;
}
else
{
left=mid+1;
}
}
return -1;
}这里空间复杂度很好求,为O(1),而对于时间复杂度,我们可以来分析一下:
我们的二分查找也叫折半查找,如上图所示,每次查找都会对当前的查找范围缩小一半,最坏的情况就是直到将我们的查找范围缩小至只有一个数据的时候才找到,或者根本找不到,无论何种情况,到了这一步,我们的算法都将会退出,由此,我们就可以找到查找范围size和查找次数M的关系为size/(2^M)=1。而这里我们就可以求出M=log2(size),这里2为底数。所以我们的时间复杂度就为O(log2(n))。
2.二分查找(递归算法)
int BinarySearch(int* arr,int left,int right,int data)
{
int mid=(left&right)+((left^right)>>1);
if(left<=right)
{
if(arr[mid]==data)
return mid;
else if(arr[mid]>data)
return BinarySearch(arr,left,mid-1,data);
else
return BinarySearch(arr,mid+1,right,data);
}
return -1;
}对于递归算法,我们要明确它的时间复杂度为递归调用的总次数*每次进行递归调用中执行基本语句的总次数;而对于它的空间复杂度,它等于递归深度*每层递归创建的对象个数。
而这里对于二分查找的递归算法,它的时间复杂度与非递归的一样,还是为O(log2(n)),原本非递归的版本是将所有的折半放在一个函数调用当中,而这里只不过是将每一次的折半都换成一次递归调用自己,递归的次数和深度均为log2(n),总体计算下来时间复杂度还是没变的。
但是对于空间复杂度,这里由于递归深度为log2(n),所以空间复杂度变为O(log2(n))了。
3.斐波那契数列(递归算法)
int Fib(int n)
{
if(n<3)
return 1;
return Fib(n-2)+Fib(n-1);
}对于斐波那契数列的递归算法,我们一眼是很难看出它的时间复杂度和空间复杂度的,这种情况最好画图来分析:
(以N=5为例)
当我们需要求Fib(5)的时候,则必须要先知道Fib(3)和Fib(4),要知道Fib(4)则要知道Fib(3)和Fib(2),总之我们这个算法的核心就是通过Fib(n-1)与Fib(n-2)来求出Fib(n),由此往下直至已知的Fib(1)和Fib(2),由此我们就得到了上图的树结构,其中的每一个节点都是一次递归调用,而我们知道当n趋近无穷大的时候,最后一层的那两个节点是可以忽略的,所以我们可以根据层数与节点数之间的关系m=2^(n)以及层数与所给的参数N之间的关系N=n+3(忽略最后一层),求出总节点数(即递归调用总次数)M=2^(N-3)
-1,而递归深度也就是我们的层数n=N-2。
所以我们可以得到我们的时间复杂度为O(2^N),空间复杂度为O(N)。
4.斐波那契数列(非递归算法)
int Fib(int n)
{
if(n<3)
return 1;
int ret=0;
int first=1;
int second=1;
for(int i=2;i<n;i++)
{
ret=first+second;
first=second;
second=ret;
}
return ret;
}对于斐波那契数列的非递归算法,它的时间复杂度与空间复杂度就很简单了,分别为O(N)和O(1)
所以,你明白为什么对于斐波那契数列我们这么抵触递归了吗?
常见的时间复杂度与空间复杂度的O渐进表达式之间的复杂度大小关系:
O(1)<O(logN)<O(N)<O(NlogN)<O(N^2)<O(N^3)<O(2^N)<O(N!)<O(N^N)
时间复杂度:算法中执行基本操作语句的总次数。
空间复杂度:算法中创建对象的个数。
一.时间复杂度
这里有一点需要注意:对于时间复杂度而言,我们并没有算法的运行总时间来衡量算法的好坏,这里最重要的原因是对于一个算法的运行时间,它受外在的其他因素的影响较大,而且能够影响时间的因素又有很多。
而对于一个算法的时间复杂度,我们可以大致分为三种情况:
1.最坏情况:任意输入规模的最大运行次数(上界);
2.平均情况:任意输入规模的期望运行次数;
3.最好情况:任意输入规模的最小运行次数,通常最好情况不会出现。(下界)
而我们在实际中我们通常情况考量的是算法的最坏运行情况。也就是说对于任意输入规模N,算法的最长运行时间,理由如下:
1. 一个算法的最坏情况的运行时间是在任意输入下的运行时间上界。
2. 对于某些算法,最坏的情况出现的较为频繁。
3. 大体上看,平均情况与最坏情况一样差。
算法分析要保持大局观:
1. 忽略掉那些的常数。
2. 关注运行时间的增长趋势,关注函数式中增长最快的表达式。
由此,我们通常通过O的渐进表达式来表示我们的时间复杂度,而我们原本用f(n)也就是算法中所有基本语句的总次数来衡量,而对于O渐进表达式我们是通过f(n)来求的,也就是O(f(n))的求法如下:
令n趋近无穷大,忽略对这个式子的影响最小项,以常数1来代替所有的常数项,只保留最高项,并且如果最高项系数为常数,则令最高项系数为1
二.空间复杂度
对于我们的空间复杂度,其实与时间复杂度类似,它也通常用O渐进表达式来表示,只不过时间复杂度的f(n)是我们算法中的所有基本语句总次数,而这里f(n)则是算法中创建对象的个数,而通过f(n)来求O(f(n))的方法都是一样的。
下面给出两个算法,来求它们的时间复杂度与空间复杂度:
1.二分查找(非递归版本)
int BinarySearch(int* arr,int size,int data)
{
int left=0;
int right=size;
int mid=0;
while(left<right)
{
mid=(left&right)+((left^right)>>1);
if(arr[mid]==data)
return mid;
else if(arr[mid]>data)
{
right=mid;
}
else
{
left=mid+1;
}
}
return -1;
}这里空间复杂度很好求,为O(1),而对于时间复杂度,我们可以来分析一下:
我们的二分查找也叫折半查找,如上图所示,每次查找都会对当前的查找范围缩小一半,最坏的情况就是直到将我们的查找范围缩小至只有一个数据的时候才找到,或者根本找不到,无论何种情况,到了这一步,我们的算法都将会退出,由此,我们就可以找到查找范围size和查找次数M的关系为size/(2^M)=1。而这里我们就可以求出M=log2(size),这里2为底数。所以我们的时间复杂度就为O(log2(n))。
2.二分查找(递归算法)
int BinarySearch(int* arr,int left,int right,int data)
{
int mid=(left&right)+((left^right)>>1);
if(left<=right)
{
if(arr[mid]==data)
return mid;
else if(arr[mid]>data)
return BinarySearch(arr,left,mid-1,data);
else
return BinarySearch(arr,mid+1,right,data);
}
return -1;
}对于递归算法,我们要明确它的时间复杂度为递归调用的总次数*每次进行递归调用中执行基本语句的总次数;而对于它的空间复杂度,它等于递归深度*每层递归创建的对象个数。
而这里对于二分查找的递归算法,它的时间复杂度与非递归的一样,还是为O(log2(n)),原本非递归的版本是将所有的折半放在一个函数调用当中,而这里只不过是将每一次的折半都换成一次递归调用自己,递归的次数和深度均为log2(n),总体计算下来时间复杂度还是没变的。
但是对于空间复杂度,这里由于递归深度为log2(n),所以空间复杂度变为O(log2(n))了。
3.斐波那契数列(递归算法)
int Fib(int n)
{
if(n<3)
return 1;
return Fib(n-2)+Fib(n-1);
}对于斐波那契数列的递归算法,我们一眼是很难看出它的时间复杂度和空间复杂度的,这种情况最好画图来分析:
(以N=5为例)
当我们需要求Fib(5)的时候,则必须要先知道Fib(3)和Fib(4),要知道Fib(4)则要知道Fib(3)和Fib(2),总之我们这个算法的核心就是通过Fib(n-1)与Fib(n-2)来求出Fib(n),由此往下直至已知的Fib(1)和Fib(2),由此我们就得到了上图的树结构,其中的每一个节点都是一次递归调用,而我们知道当n趋近无穷大的时候,最后一层的那两个节点是可以忽略的,所以我们可以根据层数与节点数之间的关系m=2^(n)以及层数与所给的参数N之间的关系N=n+3(忽略最后一层),求出总节点数(即递归调用总次数)M=2^(N-3)
-1,而递归深度也就是我们的层数n=N-2。
所以我们可以得到我们的时间复杂度为O(2^N),空间复杂度为O(N)。
4.斐波那契数列(非递归算法)
int Fib(int n)
{
if(n<3)
return 1;
int ret=0;
int first=1;
int second=1;
for(int i=2;i<n;i++)
{
ret=first+second;
first=second;
second=ret;
}
return ret;
}对于斐波那契数列的非递归算法,它的时间复杂度与空间复杂度就很简单了,分别为O(N)和O(1)
所以,你明白为什么对于斐波那契数列我们这么抵触递归了吗?
常见的时间复杂度与空间复杂度的O渐进表达式之间的复杂度大小关系:
O(1)<O(logN)<O(N)<O(NlogN)<O(N^2)<O(N^3)<O(2^N)<O(N!)<O(N^N)
相关文章推荐
- GeeksforGeeks 上关于时间空间复杂度的大O和大zeta分析
- 关于几种排序的时间复杂度和空间复杂度
- 关于计算时间复杂度和空间复杂度
- 关于时间复杂度与空间复杂度
- 关于算法的空间复杂度和时间复杂度定义
- 关于时间复杂度和空间复杂度
- 关于时间和空间复杂度的一点小结
- 关于计算时间复杂度和空间复杂度
- 【c++】关于时间复杂度和空间复杂度的相关问题
- 关于时间复杂度和空间复杂度的一些想法
- 关于时间复杂度和空间复杂度的计算
- 时间复杂度和空间复杂度
- 常用的排序算法的时间复杂度和空间复杂度
- 时间复杂度和空间复杂度
- 算法的空间复杂度于时间复杂度的关系
- 算法复杂度——时间复杂度和空间复杂度
- 算法的时间复杂度和空间复杂度
- 时间复杂度和空间复杂度
- 常用的算法的时间复杂度和空间复杂度
- 算法复杂度——时间复杂度和空间复杂度