浅析时间复杂度和空间复杂度
一、时间复杂度
算法的时间复杂度是一个函数,这个函数描述的是该算法的运行时间,又或者说它是指执行算法所需要计算的工作量。常用大O符号表述,称之为大0记法。度量一个程序的执行时间通常有两种方法:(1)事后统计的方法(2)事前分析估算的方法。但事后统计方法比较依赖计算机的硬件、软件等环境因素,不能体现出算法的优劣。所以人们常常采用事前分析估算的方法。
推导大O阶的规则:
1、用常数1取代运行时间中的所有加法常数
2、只保留最高阶项
3、去除最高阶的常数
常见的时间复杂度:
1、常数阶O(1):
如:sum = (1 + n) * n / 2;
这一段代码函数执行的次数是一次,。算法的时间复杂度为常数阶。注意:如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。
2、线性阶O(n):
如:int i;for(i=0;i<n;i++) {cout<<i<<endl;}
这一段代码执行的次数是2n+1次,根据推导规则,该算法的时间复杂度是O(n)。
3、对数阶O(logn)
如:
int i;for(i=1;i<n;) {i*=2;}
这一段代码i每次都放大两倍,我们假设这个循环体执行了m次,那么2^m = n即m = logn,所以整段代码执行次数为1 + 2logn,则f(n) = logn,时间复杂度为O(logn)。
4、平方阶O(n^2)
for(int i=1;i<n;i++) {for(int j=0;j<n;j++) { cout<<j<<endl; }}
代码执行的次数总共为2n^2+n次,根据规则,该算法的时间复杂度为平方阶。
5、立方阶O(n^3)
如:for(int i=0;i<n;i++) {for(int j=0;j<m;j++) {for(int k=0;k<l;k++) { cout<<k<<endl;} }}
这一段代码其执行的次数是(2n^3)+nn+n次;根据规则,该算法的时间复杂度为立方阶;
常见的时间复杂度
常见的时间复杂度如表所示。
常用的时间复杂度所耗费的时间从小到大依次是:
O(1)常数阶、O(logn)对数阶、O(n)线性阶、 平方阶等,像O(n^3),过大的n都会使得结果变得不现实。
二、空间复杂度
一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分。
(1)固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
(2)可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。
一个算法所需的存储空间用f(n)表示。S(n)=O(f(n)) 其中n为问题的规模,S(n)表示空间复杂度。
1.空间复杂度O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
举例:
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
2.空间复杂度O(n)
我们先看一个代码:
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)
再来看一段代码
void fun(int a[],int n,int k)
//数组a共有n个元素
{
int i;
if (k==n-1)
for (i=0;i<n;i++)
printf(“%d\n”,a[i]); //执行n次
else
{
for (i=k;i<n;i++)
a[i]=a[i]+i*i; //执行n-k次
fun(a,n,k+1);
}
}
S(n) = O(g(1*n))
此方法属于递归算法,每次调用本身都要分配空间,fun(a,n,0)的空间复杂度为O(n)。
注意:
1.空间复杂度相比时间复杂度分析要少。
2.对于递归算法来说,代码一般都比较简短,算法本身所占用的存储空间较少,但运行时需要占用较多的临时工作单元。
若写成非递归算法,代码一般可能比较长,算法本身占用的存储空间较多,但运行时将可能需要较少的存储单元。
常用的排序算法的时间复杂度和空间复杂度
1.冒泡排序
时间复杂度:
其外循环执行N−1次。内层循环最多的时候执行NN次,最少的时候执行1次,平均执行(N+1)/2(N+1)/2 次。所以循环体内的比较交换约执行
(N−1)(N+1)/2=(N2−1)/2(N−1)(N+1)/2=(N2−1)/2(按照计算复杂度的原则,去掉常数,去掉最高项系数,其复杂度为O(N2) O(N2)。 对于一个已经有序的数组,算法完成第一次外层循环后就会返回。实际上只发生了 N−1
N−1次比较,所以最好的情况下,该算法复杂度是O(N) O(N)。
空间复杂度:
最优的空间复杂度就是开始元素顺序已经排好了,则空间复杂度为:0; 最差的空间复杂度就是开始元素逆序排序了,则空间复杂度为:O(n);
平均的空间复杂度为:O(1);
2.插入排序
时间复杂度:
插入排序的时间复杂度分析。在最坏的情况下,数组完全逆序,插入第2个元素时要考察前1个元素,插入第3个元素是,要考虑前2个元素,…,插入第N个元素,要考虑前N−1
个元素。因此,最坏情况下的比较次数是 1+2+3+…+(N−1)1+2+3+…+(N−1),等差数列求和,结果为
N*N/2,所以最坏情况下的复杂度为 O(N^2)
最好情况下,数组已经是有序的,每插入一个元素,只需要考查前一个元素,因此最好情况下,插入排序的时间复杂度为O(N)。
空间复杂度:
算法的空间复杂度很清楚:计算中只用了两个简单变量,用于辅助定位和完成序列元素的位置转移。因此算法的空间复杂度是O(1),与序列大小无关
3.选择排序
时间复杂度:
选择排序的复杂度分析。第一次内循环比较N - 1次,然后是N-2次,N-3次,……,最后一次内循环比较1次。共比较的次数是
(N - 1) + (N - 2) + … + 1,求等差数列和,得(N−1+1)∗N/2=N*N/2
舍去最高项系数,其时间复杂度为 O(N^2)。
虽然选择排序和冒泡排序的时间复杂度一样,但实际上,选择排序进行的交换操作很少,最多会发生 N - 1次交换。
而冒泡排序最坏的情况下要发生N^2 /2交换操作。从这个意义上讲,交换排序的性能略优于冒泡排序。而且,交换排序比冒泡排序的思想更加直观。空间复杂度:
最优的情况下(已经有顺序)复杂度为:O(0) ;最差的情况下(全部元素都要重新排序)复杂度为:O(n);平均的时间复杂度:O(1)
4.快速排序
最优情况下时间复杂度:
最差情况下时间复杂度
最差的情况就是每一次取到的元素就是数组中最小/最大的,这种情况其
实就是冒泡排序了(每一次都排好一个元素的顺序)
这种情况时间复杂度就好计算了,就是冒泡排序的时间复杂度:T[n] = n * (n-1) = n^2 + n;
综上所述:快速排序最差的情况下时间复杂度为:O( n^2 )平均时间复杂度
快速排序的平均时间复杂度也是:O(nlogn)空间复杂度:
就地快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间
的就是递归调用了,因为每次递归就要保持一些数据;最优的情况下空间复杂度为:O(logn) ;每一次都平分数组的情况; 最差的情况下空间复杂度为:O( n ) ;退化为冒泡排序的情况。
排序法 | 最差时间分析 | 平均时间复杂度 | 稳定度 | 空间复杂度 |
---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | 稳定 | O(1) |
插入排序 | O(n^2) | O(n^2) | 稳定 | O(1) |
选择排序 | O(n^2) | O(n^2) | 不稳定 | O(1) |
快速排序 | O(n^2) | O(n*log2n) | 不稳定 | O(log2n)~O(n) |
参考1:https://blog.csdn.net/weixin_41725746/article/details/93080926
参考2:https://blog.csdn.net/haha223545/article/details/93619874