时间复杂度
一个算法中的语句执行次数称为语句频度或时间频度。记为T(n).一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。
一般总是考虑在最坏情况下的时间复杂度,以保证算法的运行时间不会比它更长。
f(n) n越大算法的执行时间越长
-
排序:n为记录数
-
矩阵:n为矩阵的阶数
-
多项式:n为多项式的项数
-
集合:n为元素个数
-
树:n为树的结点个数
-
图:n为图的顶点数或边数
常见的时间复杂度量级
-
常数阶:O(1)
-
对数阶:O(logN)
-
线性阶:O(n)
-
线性对数阶:O(nlogN)
-
平方阶:O(
)
-
立方阶:O(
)
-
k次方阶:O(
)
-
指数阶:O(
)
从上至下依次的时间复杂度越来越大,执行的效率越来越低
其他:
-
多项式复杂度:O(
)
-
阶乘复杂度:O(n!) O(
)<O(n!)<O(
)
-
在无序数列中查找某个数(顺序查找):O(n)
-
平面上有n个点,求出任意两点间的距离:O(
)
-
插入排序、选择排序、冒泡排序:O(
)
-
快速排序:O(n*log(n))
-
二分查找:O(log(n))
我们假设计算机运行一行基础代码需要执行一次运算,那么下边这个代码需要执行2次运算
时间复杂度为O(1)
int aFunc(void){
printf("Hello,World!\n");
return 0;
}
再看这个
int aFunc(int n){
for(int i=0;i<n;i++){
printf("Hello,World!\n");
}
return 0;
}
这个代码需要(n+1+n+1)=2n+2次运算
时间复杂度为O(n)
我们知道常数项对函数的增长速度影响不大,所以当T(n)=C,C为一个常数的时候,我们说这个算法的时间复杂度为O(1);如果T(n)不等于一个常数项时,直接将常数项省略。
我们知道高次项对于函数的增长速度的影响是最大的,同时因为要求的精度不高,所以我们直接忽略低次项。或者说:如果复杂度是多个n的函数之和,则只关心随n的增长而增长得最快的那个函数
-
O(
+
)->O(
)
-
O(
+
)->O(
)
-
O(n!+
)->O(n!)
因为函数的阶数对函数的增长速度的影响是最显著的,所以我们忽略与最高阶相乘的常数
O(3)->O(
)
求时间复杂度
long aFunc(int n){
if(n<=1){
return 1;
}else{
return aFunc(n-1)+aFunc(n-2);
}
}
显然运行次数T(0)=T(1)=1,同时T(n)=T(n-1)+T(n-2)+1,这里的1是其中的加法算一次执行。
显然T(n)=T(n-1)+T(n-2)是一个斐波那契数列,通过归纳证明法可以证明,当n>=1时T(n)<,同时当n>4时T(n)>=
所以时间复杂度可以表示为O(),简化后为O(
)
线性阶O(n)
for(i=1;i<=n;++i){
j=i;
j++;
}
for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的
常数阶O(1)
int i=1;
int j=2;
++i;
j++;
int m=i+j;
代码在执行的时候,它消耗的时间并不随着某个变量的增长而增长,那么无论这类代码有多长,都可以用O(1)来表示它的时间复杂度
对数阶O(logN)
int i=1;
while(i<n)
{
i=i*2;
}
在while循环里面,每次都将i乘以2,乘完之后,i距离n就越来越近了。我们试着求解一下,假设循环x次之后,i就大于n了,此时这个循环就退出了,也就是说2的x次方等于n,那么x=n
线性对数阶O(nlogN)
将时间复杂度为O(logN)的代码循环n遍的话,那么它的时间复杂度就是n*O(logN),也就是O(nlogN)
for(m=1;m<n;m++){
int i=1;
while(i<n)
{
i=i*2;
}
}
平方阶O()
for(x=1;i<=n;x++){
for(i=1;i<=n;i++){
j=i;
j++;
}
}
如果把O(n)的代码再嵌套循环一遍,它的时间复杂度就是O()
如果将其中一层循环的n改成m那它的时间复杂度就变成O(m*n)
空间复杂度
一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。用S(n)来定义。计算公式S(n)=O(f(n)).其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。
常量空间-空间复杂度O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为O(1)
int i=1;
int j=2;
++i;
j++;
int m=i+j;
线性空间-空间复杂度O(n)
int[] m=new int[n];
for(i=1;i<=n;++i)
{
j=i;
j++;
}
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间。因此,这段代码的空间复杂度主要看第一行即可,即S(n)=O(n)
二维空间-空间复杂度O()
当算法分配的空间是一个二维数组集合,并且集合的长度和宽度都与输入规模n成正比时,空间复杂度记作O()
int[][] matrix=new int[n][n];//O(n^2)
int[][] matrix=new int[m][n];//O(mn)
递归空间
void fun4(int n){
if(n<=1){
return;
}
fun4(n-1);
...
}
正如上述代码一样,递归代码中没有显示声明变量或者集合,但是计算机在执行程序时,会专门分配一块内存,用来存储“方法调用栈”。方法调用栈包括入栈和出栈两个操作:
当进入一个新方法时,执行入栈操作,把调用的方法和参数信息压入栈中
当方法返回时,执行出栈操作,把调用的方法和参数信息从栈中弹出
还是上述代码,假设现在传入参数5,那么方法fun4(5)的调用信息先入栈:
method fun4
n 5
接下来递归调用相同的方法,方法fun4(4)的调用信息入栈:
method fun4
n 4
method fun4
n 5
以此类推,递归越来越深,栈内的元素也越来越多,最终:
method fun4
n 1
method fun4
n 2
method fun4
n 3
method fun4
n 4
method fun4
n 5
当n=1的时候,触发递归的结束条件,执行return,方法出栈。最终所有入栈的元素都会出栈。
由上面“方法调用栈”的出入栈过程可以看出,执行递归操作所需要的内存空间和递归的深度成正比。纯粹的递归操作的空间复杂度也是线性的,如果递归的深度是n,那么空间复杂度就是O(n)
参考: