一、复杂度分析
首先要明确一点,数据结构和算法本质是解决“快”和“省”的问题。要描述一个算法的好坏就需要用到复杂度分析了,复杂度分析可分为如下两种。
- 时间复杂度
- 空间复杂度
时间复杂度就是描述算法的快,空间复杂度则是描述算法的省。一般说的复杂度都是时间复杂度,毕竟现代计算机存储空间已经不那么拮据了,时间复杂度是我们重点研究的内容。
二、大 O
复杂度表示法
首先看一段代码,求从 1~n
的累加之和。
int demo(int n) {
int i;
int sum = 0;
for(i=1; i<n; i++) {
sum += i;
}
return sum;
}
现在就来估算一下这段代码的执行时间(下面都是以时间复杂度为例讲解,空间复杂度最后再讲)。
从 CPU
的角度来看,每一行代码都执行着类似的操作读数据-运算-写数据。这里为了方便计算,假设每行代码的执行时间都是一样的,用 t
表示执行一行代码所需要的时间,n
表示数据规模的大小,T(n)
表示代码执行的总时间。
那么这段代码总执行时间是多少呢?我们来数一下。
首先,函数体内有 5
条语句,第 1、2、5
条语句总共执行了 3
次,所需时间是 3*t
;第 3、4
条语句各自执行了 n
次,所需时间是 2*n*t
。把这两个代码段执行的时间相加,所得到的结果就是这段代码总共所需的时间。
通过上述公式可以得到一个规律,T(n)
随着 n
变大而变大,变小而变小。所以,T(n)
与 n
是成正比的,用数学符号表示就可以写成。
其中 f(n)
是代码段执行所需的时间之和,O
表示 T(n)
与 f(n)
之间的关系是成正比的。
由公式可得代码段执行所需的时间可表示为 。这就是大 O
时间复杂度表示法。大 O
时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随着数据规模增长的变化趋势,所以,也叫做渐进时间复杂度简称时间复杂度。
其实并不是最终时间复杂度的表示方式。在实际的复杂度分析中,一般会把公式中的常量、系数、低阶忽略。因为这三部分并不影响增长趋势(还记得时间复杂度其实是渐进时间复杂度吧!),所以只需要记录一个最大量级就可以了,时间复杂度的最终表示方式就是。
三、复杂度的分析方法
1. 最大量阶
int demo(int n) {
int i;
int sum = 0;
for(i=1; i<n; i++) {
sum += i;
}
return sum;
}
在分析一个算法、一段代码的时间复杂度的时候,只关注循环执行次数最多的那一段代码即可。
2. 加法法则
int demo(int n) {
int i;
int sum = 0;
for(i=1; i<n; i++) {
sum += i;
}
for(i=1; i<n; i++) {
int j;
for (j=1; j<n; j++)
sum += i;
}
return sum;
}
如果代码中存在着不同量级的时间复杂度,总的时间复杂度就等于量级最大的那段代码的时间复杂度。
3. 乘法法则
int demo(int n) {
int i;
int sum = 0;
for(i=1; i<n; i++) {
int j;
for (j=1; j<n; j++)
sum += i;
}
return sum;
}
如果是嵌套、函数调用、递归等操作,只需要将各部分相乘即可。
四、复杂度的量级
- 常量阶:
- 对数阶:
- 线性阶:
- 线性对数阶:
- 平方阶:
- 立方阶:
k
次方阶:- 指数阶:
- 阶乘阶:
对于上述不同的量级可以分为两类:多项式量级和非多项式量级。其中,非多项式量级只有两个:和,非多项式也叫做 NP
问题。
一般情况下,我们常见的复杂度只有、、、、
五、时间复杂度
我们已经分析了时间复杂度,但是还是有一点儿小问题,比如我们要查找某个元素在长度为 n
的数组中的下标。如果按照顺序遍历,最理想的情况是第一个就是我们要找的,所以时间复杂度是 O(1)
;如果最后一个才找到我们要的数据,那么它的时间复杂度是 O(n)
。
为了解决同一段代码在不同情况下时间复杂度出现量级差异,我们就需要对时间复杂度进一步细化分类,为了更准确、更全面的描述代码的时间复杂度,引入了一下 4
个概念。
1. 最好情况时间复杂度
代码在最理想情况下执行的时间复杂度。
2. 最坏情况时间复杂度
代码在最坏情况下执行的时间复杂度。
3. 平均情况时间复杂度
上面两个最好、最坏情况都是小概率事件,平均情况时间复杂度才是最能代表一个算法的时间复杂度。因为平均情况时间复杂度需要引入概率进行分析,所以也叫做加权平均时间复杂度。
4. 均摊时间复杂度
正常情况下,代码在执行过程中都处于低阶的复杂度,极个别情况会出现高阶的复杂度,这是我们就可以将高阶的复杂度均摊到每个低阶的复杂度上,这种分析使用的是摊还分析法的思想。
其实我们只需要知道时间复杂度就够了。这四种方法都是对时间复杂度的一些特殊情况的补充,也没必要花大力气去研究它,大概知道有这种时间复杂度分类就可以了,如果你自己想学或者有脑残面试官要问这些,那你就自己去查找资料研究研究,这里不会展开讲解。
六、空间复杂度
前面讲解过,时间复杂度是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。那么空间复杂度就是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。
看一段代码,定义一个新数组,赋值后遍历输出。
void demo(int n) {
int i;
int data[n];
for(i=0; i<n; i++) {
data[i] = i * i;
}
for(i=0; i<n; i++) {
printf("%d\n", data[i]);
}
}
跟时间复杂度分析一样,函数体内第 1
条语句是常量阶,直接忽略;第 2
条语句申请了一个大小为 n
的 int
类型数组,所以整段代码的空间复杂度就是。