数据结构和算法本身解决的是“快”和“省”的问题,而执行效率是算法一个非常重要的考量指标。算法的复杂度就是它的一个衡量标准,尽管不同硬件的实际执行效率有所不同。
算法的复杂度主要分为空间复杂度和时间复杂度。
空间复杂度:算法在计算机执行时所需要存储空间的度量。通常使用S(n) = O(f(n))表示。
主要包括三个部分:
1.算法程序所占的空间
2.输入的初始数据所占的空间
3.算法执行过程中所需要的额外空间
时间频度:算法中语句的执行次数,通常用T(n)来表示。
时间复杂度:定性的描述算法的运行时间,考察输入值大小趋近无穷的情况,通常使用O(f(n))来表示。
1.时间复杂度分析
通常对于复杂度的分析,一般有以下三个常用的方法
1. 只关注循环执行次数最多的一段代码
private void method(int n){
int a = 0;
int b = 1;
int c = 2;
for (int i = 0; i < n ; i++) {
a += i;
}
}
这个方法里面,前三次的声明只会执行1次,对于常数级不予考虑。对于for循环,这两行代码,将会执行n次,所以时间复杂度为O(n)。
2. 总复杂度等于量级最大的那段代码的复杂度
private void methodOne(int n){
int a = 0;
for (int i = 0; i < 1000 ; i++) {
a += i;
}
int b = 0;//1
for (int i = 0; i < n ; i++) { // n+1
b += i; //n
}
int c = 0; //1
for (int i = 0; i < n ; i++) { //n+1
for (int j = 0; j < n ; j++) { //n * (n+1)
c = c + i*j; //n * n
}
}
}
对于第一个循环,执行了1000次,对于常数级我们仍不给予考虑。第二个循环执行了n次,时间复杂度为O(n)。对于第三个嵌套的for循环,代码执行了n²(并不是最准确的)次,时间复杂度为O(n²),所以这个方法的时间复杂度为O(n²)。
3. 嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
private void method(int n){
int a = 0;
for (int i = 0; i < n ; i++) {
a += methodOne(i);
}
}
private int methodOne(int n){
int a = 0;
for (int i = 0; i < n ; i++) {
a += i;
}
return a;
}
method方法的复杂度为O(n),但是在for循环中调用了methodOne,因为methodOne的复杂度也是O(n),所以整个方法的时间复杂度就是O(n²)。
2.常见的时间复杂度
大致分为两类,多项式量级和非多项式量级。
图中波浪线的两种为非多项式量级。我们把时间复杂度为非多项式量级的算法问题叫作 NP(Non-Deterministic Polynomial,非确定多项式)问题。
当数据规模 n 越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法其实是非常低效的算法,我们应尽量避免这种算法。
3.空间复杂度
空间复杂度像比于时间复杂度,重要性和分析都没有那么重要和困难,但是对效率的影响还是存在滴。
private int[] methodTwo(int n){
int a = 0;
int[] array = new int[n];
for (int i = 0; i < n ; i++) {
a += i;
array[i] = a;
}
return array;
}
a变量只声明了一个空间,常量级的可以考虑不计,array变量声明了n个空间,剩下的没有占用更多的空间,因此空间复杂度是 O(n)。
常见的空间复杂度就是 O(1)、O(n)、O(n²),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。
4.最好、最坏情况时间复杂度
最好情况时间复杂度就是,在最理想的情况下,执行这段代码的时间复杂度
最坏情况时间复杂度就是,在最糟糕的情况下,执行这段代码的时间复杂度
private int methodThree(int[] array, int x){
int a = 0;
for (int i = 0; i < array.length ; i++) {
if (array[i] == x){
a = i;
return a;
}
}
return -1;
}
这个方法是找出和x相同值的下标,最好的情况是数组的第一个就是我们想要的结果,此时时间复杂度是O(1)。最坏的情况是最后一个是我们想要的结果或者是没有和x相同的下标,此时时间复杂度是O(n)。
5.平均时间复杂度
在大多数情况下,最好或是最坏的情况一般很少发生,为了更好的表示复杂度,引入平均时间复杂度。对于上面的案例,有n+1种情况,每种情况发生的概率是一样的,把每种情况下查找需要遍历的元素个数累加起来,然后再除以 n+1,就可以得到需要遍历的元素个数的平均值
(1+2+3+……+n+n)/(n+1)
结果是一个n量级的数,所以它的平均时间复杂度是O(n)。
6.均摊时间复杂度
我们把耗时多的那次操作均摊到耗时少的操作上,这就是均摊分析的大致思路。
static int index;
private int[] insert(int[] array, int x){
if(array.length == index){
int sum = 0;
for (int i = 0; i < array.length ; i++) {
sum += array[i];
}
array[0] = sum;
index = 1;
}
array[index] = x;
index++;
return array;
}
这个方法实现了插入方法,当数组有空间时,直接插入,当没有空间时,将数组里全部数求和后放到数组第一个位置。
最好的情况是数组中有空闲空间,只需要插入就行了,所以最好情况时间复杂度为 O(1)。
最坏的情况是数组中没有空闲空间了,需要先做一次数组的遍历求和,然后再将数据插入,所以最坏情况时间复杂度为 O(n)。
平均时间复杂度是每个位置都有空闲,加上没有空间需要求和,一共有n+1种情况,假设每种情况概率相同,前n种的每种概率为1/(n+1),加起来也就是n/(n+1),最后一种需要执行一次for循环,也就是n/n+1(严格意义来说的话是n+1/n+1),所以平均时间复杂度就为O(1)。
与上一个查找下标方法相比,寻找下标方法在最好的情况下才为O(1),而插入方法在最坏的情况下才为O(n)。这是因为在大多数情况下,插入方法的时间复杂度很低,只有个别情况下复杂度才很高,我们将它的效率进行了均摊。
对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系,这个时候,我们就可以将这一组操作放在一块儿分析,看是否能将较高时间复杂度那次操作的耗时,平摊到其他那些时间复杂度比较低的操作上。而且,在能够应用均摊时间复杂度分析的场合,一般均摊时间复杂度就等于最好情况时间复杂度。
我们可以认为均摊时间复杂度是一种特殊的平均时间复杂度。
我们要做的控制复杂度,而不是制造复杂度。