算法是指用来操作数据、解决程序问题的一种方法。对于同一问题,使用不同的算法,也许最终结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。
那我们该如何去衡量不同算法之间的优劣呢?主要还是从算法所占用的【时间】和【空间】两个维度去考量。
- 时间维度:是指执行当前算法所消耗的时间,我们通常用【时间复杂度】来描述;
- 空间维度:是指执行当前算法所需要占用多少内存空间,我们通常用【空间复杂度】来描述。
因此,评价一个算法的效率主要是看它的时间复杂度和空间复杂度情况。然而,有的时候时间和空间却又是鱼和熊掌不可兼得,那我们就需要从从中去取一个平衡点。
下面我来分别介绍一下【时间复杂度】和【空间复杂度】的计算方式。
一、时间复杂度
我们想要知道一个算法的【时间复杂度】,很多人首先想到的方法就是把这个算法程序运行一遍,那么它所消耗的时间就自然而然知道了。
这种方式可以吗?当然可以,不过它有很多弊端。
这种方式很容易受运行环境影响,在性能高的机器上跑出来的结果与性能低的机器上跑出来的结果相差会很大。而且对测试时使用的数据规模也有很大关系。再者,我们在写算法的时候,还没有办法完整的去运行呢。
因此,另一种更为通用的方法就出来了:【大O符号表示法】,即T(n)=O(f(n))。
我们先来看个例子:
for (i=1; i <=n; i++)
{j = i;j++;
}
通过【大O表示法】,这段代码的时间复杂度为O(n),为什么呢?
在大O表示法中,时间复杂度的公式是:T(n)=O(f(n)),其中f(n)表示每行代码执行次数之和,而O表示正比例关系,这个公式的全程是:算法的渐进时间复杂度。
我们继续看上面的例子,假设每行执行时间都是一样的,我们用1颗粒时间来表示,那么这个例子的第一行好事是1个颗粒时间,第三行的执行时间是n个颗粒时间,第四行的执行时间也是n个颗粒时间(第二行和第五行是符号,暂时忽略),那么总时间就是T(n)=(1+2n)*颗粒时间,从这个结果可以看出,这个算法的耗时是随着n的变化而变化,因此,我们可以简化这个算法的时间复杂度表示为:T(n)=O(n)。
为什么可以去简化呢,因为大O表示法并不是 用于来真实代表算法的执行时间的,它是哟弄个来表示代码执行时间的增长变化趋势的。
所以上面的例子中,如果n无限大的时候,T(n)=time(1+2n)中的常量1就没有意义了,倍数2也意义不大。因此直接简化为T(n)=O(n)就可以了。
常见的时间复杂度量级有:
- 常熟阶O(1)
- 对数阶O(logN)
- 线性阶O(n)
- 线性对数阶O(nlogN)
- 平方阶O()
- 立方阶O()
- k次方阶O()
- 指数阶O()
从上至下依次的时间复杂度越来越大,执行效率越来越低。
下面选取一些较为常用的来讲解游戏啊(没有严格按照顺序):
1. 常数阶O(1)
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),如:
int i = 1;
int j = 2;
i++;
j++;
int m = i + j;
上述代码在执行的时候,它消耗的时间并不随着某个变量的增长而增长,那么无论这个代码有多长,即使几万几十万行,都可以用O(1)来表示它的时间复杂度。
2. 线性阶O(n)
这个在最开始的代码示例中就讲解过了,如:
for (i = 1; i <= n; i++)
{j = i;j++;
}
这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化为变化的,因此这类代码都可以用O(n)来表示它的时间复杂度。
3. 对数阶O(logN)
还是先来看代码:
int i = 1;
while(i < n)
{i = i * 2;
}
从上面代码可以看到,在while循环里面,每次都将i乘以2,乘完以后i距离n就越来越近了。我们试着求解一下,假设x次之后,i就大于n了,此时这个循环就退出了,也就是说,那么,也就是说循环次以后,这个代码就结束了。因此这个代码的时间复杂度为:。
4. 线性对数阶O(nlogN)
线性对数阶O(nlogN)其实非常容易理解,将时间复杂度O(log(N))的代码循环n遍的话,那么它的时间复杂度就是nO(logN),也就是O(nlogN)。
就拿上面的代码加一点修改来举例:
for(m = 1; m <= n; m++)
{i = 1;while(i < n){i = i * 2; }
}
5. 平方阶
平方阶就更容易理解了,如果把O(n)的代码再嵌套循环一遍,它的时间复杂度就是了。
举例:
for (x=1; x<=n; x++)
{for (i=1; i<=n; i++) {j = i;j++; }
}
这段代码其实就是嵌套了两层n循环,它的时间复杂度就是,即。
如果将其中一层循环的n改成m,即:
for (x=1; x<=m; x++)
{for (i=1; i<=n; i++) {j = i;j++; }
}
那么它的时间复杂度就变成了。
6. 立方阶、k次方阶
参考上面的去理解就好了,相当云三层n循环,其它的类似。
除此之外,其实还有平均时间复杂度、均摊时间复杂度、最坏时间复杂度、最好时间复杂度的分析方法,有点复杂,这里就不展开了。
二、空间复杂度
既然时间复杂度不是用来计算程序具体耗时的,那么我也应该明白,空间复杂度也不是用来计算程序实际所占用的空间的。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用S(n)来定义。
空间复杂度比较常用的有:、、,下面我们来看看:
1. 空间复杂度
如果算法执行所需要的临时空间不随着某个变量n的大小变化,即此算法空间复杂度为一个常量,可表示为。
举例:
int i = 1;
int j = 2;
i++;
j++;
int m = i + j;
代码中i、j、m所分配的空间都不随着处理数据量变化,因此它的空间复杂度S(n)=O(1)。
2. 空间复杂度
我们先看一个代码:
int[] m = new int[n]
for (i=1; i<=n; i++)
{j = i;j++;
}
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这个代码的2~6行虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即S(n)=O(n)。
以上,就是对算法的时间复杂度和空间复杂度基础的分析,欢迎大家一起交流。
算法的时间与空间复杂度(一看就懂)