前言
众所周知,之所以引入数据结构和算法,是为了让我们的程序更加地健壮,更“快”、更“省”。那么,怎么去考量一个算法的执行效率呢,那就是今天的主角:复杂度分析。
什么是复杂度分析
复杂度也叫渐进复杂度,是指算法在编写成可执行程序后,运行时所需要的资源,包括时间资源和空间资源(内存资源),所谓的分析就是用来分析算法执行效率和数据规模之间的增长关系,可以这么说越高阶复杂度的算法,执行效率越低。
为什么需要复杂度分析
你可能会这么想:我把自己写的代码跑一边,然后看看执行时间和内存使用情况不就知道自己写的如何了吗。
这么操作是没有问题的,但是这种方法有非常大的局限性。
依赖测试环境
如果通过执行结果来分析的话,在不同设备上,不同内存,不同cpu的情况下,测试的结果就会出现很大的差异。
依赖数据规模
如果通过上述方法来检测代码的健壮性,需要大量的测试,对于数据的规模有很大要求。
和性能测试相比,复杂度分析有不依赖执行环境、成本低、效率高等特点。 掌握好复杂度分析,将编写出更加优质的代码。
如何进行复杂度分析
大O表示法
举一个累加的例子:
void add (int n) {
int sum = 0;
for(int i = 1 i <=n; i++) {
sum+=i
}
return sum;
}
复制代码
我们来分析下这段代码的执行时间,用T(n) 表示执行的总时间,x表示每行代码执行的时间,可以得出:
T(n) = (n+ 1)x
复制代码
当n越大,所执行的时间就越久,可以看出:执行时间和代码的执行次数n成正比
总结一下:
T(n) = O(f(n))
复制代码
f(n)表示每行代码的执行次数总和,用一个公式表示。O表示执行时间和公式成正比
当n无限大时,公式中的常量和低阶的部分就可以忽略掉,所以上述代码的执行时间T(n) = O(n)
复杂度分析法则
- 单段代码看高频 比如上面例子的 循环
- 多段代码取最大 举个例子:
void add (int n) {
int sum1 = 0;
for(int i =1;i <= 50; i ++) {
sum1+=i;
}
sum2 = 0;
for(int j = 1;j <= n; j++) {
sum2+=j;
}
sum3 = 0;
for(int m = 1; m <=n; m ++) {
for(int s = 1; s <=n; s++) {
sum3+=s;
}
}
return sum1+sum2+sum3;
}
复制代码
这是一个多段代码,有三个片段,时间复杂度分别是:50,n,n*n 取最大的复杂度就是 T(n) = O(n**n) 3. 嵌套代码求乘积:比如上述例子中的最后一个片段。 4. 多个规模求加法 再举个栗子:
void add (int n, int m) {
sum1 = 0;
for(int i = 1; i <=n; i ++) {
sum1+=i;
}
sum2 = 0;
for(int j = 1; j <=m; j++) {
sum2+=j;
}
return sum1 + sum2;
}
复制代码
该代码的执行时间 T(n) = O(n + m)
时间复杂度和空间复杂度
时间复杂度
上述代码的执行时间都是一中时间复杂度,也叫渐进式时间复杂度,表示代码执行时间随着数据规模增长的变化趋势。
空间复杂度
空间复杂度,也叫渐进式空间复杂度,表示算法的存储空间与数据规模之间的增长关系。
int i = 0;
int [] a = new int [n]
复制代码
可以看出第2行代码开辟了大小为n的空间,所以空间复杂度为O(n)
常用复杂度级别
虽然代码千差万别,但是大致分为一下几种: 根据数量级递增可以分为两类:多项式量级和非多项式量级。
多项式量级:随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增加。包括:
O(1) 常数阶 O(logn) 对数阶
O(n) 线性阶 O(nlogn) 线性对数阶
O(n^2) 平方阶 O(n ^3) 立方阶
复制代码
O(1)
int i = 5;
int j = i +6;
复制代码
像上面这段代码的复杂度就是O(1),只要f(n)是一个常量,就是O(1)
非多项式量级:随着数据规模的增加,算法的执行时间和空间占用暴增,这类算法性能差。包括:
O(2^n) 指数阶 O(n!)阶乘阶
复制代码