数据结构与算法第一天

120 阅读1分钟

为什么需要时间、空间复杂度分析

1事后统计法

代码整体的跑一遍,得到算法执行的时间被称为事后统计法;

弊端1:测试查看性能非常依赖环境;

弊端2:小规模数据在存在排序的情况下不会有太大差异,但大数据的时候, 待排序数据有序度不一样,排序的执行时间就会有很大的差异。

2.如何粗略的估计算法的执行效率--时间空间复杂度分析方法

大O复杂度表示

样例1:

public int cal(int n){

int sum = 0;
int i = 1;
for (; i <= n; ++i){
    sum = sum + i;
}
return sum;

}

从cpu的角度看,每一行代码都是在读数据-运算-写数据,我们粗略的估计每一行代码执行的时间都为一个unti_time,那么第2行和第3行执行的时间都为1个unti_time,第4行和第5行都运行了n遍。所需时间就为2n* unti_time的执行时间。所以这段代码的执行时间就为(2n+2) * unit_time。可以看出,所有代码的执行时间T(n) 与每一行代码的执行次数是成正比的

样例2:

int cal(int n) {

int sum = 0;
int i = 1;
int j = 1;
for (; j <= n; ++j) {
    j = 1;
    for(; j <= n; ++j) {
        sum = sum + i*j;
    }
}

}

代码分析:假设每一行执行时间为unit_time.可以计算2,3,4行代码都会执行1个unit_time的执行时间,第5,6行代码循环执行了n遍,所需要的时间就是2n * unit_time的执行时间。第7,8行代码循环执行的n * n 遍。所以7,8行总共的运行时间为2 * n * n * unit_time。 所以整个代码的运行时间T(n) =(3+2n+2n * n)* unit_time。 通过这两段代码执行时间的推导可以得出一个规律,所有代码的执行时间T(n)与每行代码的执行次数f(n)成正比

T(n)=O(f(n))

T(n)代表代码执行的时间,n表示数据规模的大小;f(n)代表每行代码执行的次数总和,所以用f(n)来表示。公式中的O,来表示代码执行的时间T(n)与f(n)的比值。在第一个例子中T(n)=O(2n+2),其中2n+2代表f(n)。第二个例子可以理解为T(n)= O(2n ^ n + 2n +3)。这样的表示发就叫大O 时间复杂度表示法。

时间复杂度(渐进时间复杂度)

大O时间复杂度并不是真正的代码执行时间。而是代码执行时间随数据规模增长的变化趋势,所以也叫作渐进时间复杂度,简称时间复杂度。当n很大时,可以把它想象成10000、100000。而公式中的低阶、常量、系数三部分并不左右增长的变化趋势,所以都可以忽略。只需要记录一个最大量级就可以,如果用大O表示发表示样例1和样例2两段代码的时间复杂度,就可以用T(n)=O(n);T(n)=O(n^n)。

注解:第二段代码T(n)=O(2n^n+2n+3).在公式中低阶是2n,常量是3,系数是2,三部分都不会左右增长趋势,所以忽略。可以理解成T(n)=O(n^n)

1.只关注循环执行次数最多的一段代码

大O这种复杂度表示法只是表示一种变化的趋势。我们通常会忽略调公式中的常量、低阶、系数,只需要记录一个最大阶的量级就可以了。所以,我们在分析一个算法,一段代码的时间复杂度的时候,也只关注循环次数最多的一行代码就可以了。这段代码执行了n次的量级,就是整段要分析代码的时间复杂度。

int cal(int n){

`int sum = 0;
 int i =1;
 for (;i<= n; ++i){
     sum =sum + i;
 }
 return sum;`

}

这个代码里只有4,5行被执行了n次,所以时间复杂度就是O(n)。

2加法法则:总复杂度等于量级最大的那段代码复杂度

`int cal(int n){

int sum_1 = 0;
int p = 1;
for(;i<=100;++P){
     sum1 = sum_1 + p;
}
 int sum_2 = 0;
 int q = 1;
 for (; q < n; ++q){
     sum_2 = sum_2 + q;
 }
 int sum_3 = 0;
 int i = 1;
 int j = 1;
 for(;i <= n; ++i){
     j = 1;
     for(; j <= n;++j){
         sum_3 = sum_3 + i*j;
     }
 }
 retutn sum_1+sum_2+sum_3;

} `

分析

这个代码3部分,sum_1时间的复杂度是多少?这段代码执行了100次,一个常量的执行时间与n的规模无关。 及时他循环10000次,1000000次,只要是已知数就更n无关。时间复杂度的概念他表示一个算法执行效率与数据规模增长的变化趋势,不官常量的执行时间多达,我们都可以忽略因为他本身对增长趋势并无影响。

那么第二段和第三段时间复杂度可以算出来分别是O(n)和O(n^n)

综合三段代码的时间复杂度,我们取其中最大的量级,所以整段代码的时间复杂度就是O(n^n).也就是说时间复杂度等于量级最大的那段代码的时间复杂度。我们将这个规律抽象成公式就是: T1(n)=O(f(n)),T2(n)=O(g(n));那么T(n)=T1(n)+T2(n)=max(O(fn)),O(g(n))=O(max(f(n),g(n))).

3.乘法规则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

如果T1(n) = O(n),T2(n)=O(n^n),则T(n)=T1(n)* T2(n)=O(f(n)* g(n)) 也就是说,假设T1(n)=O(n),T2(n)=O(n^n),则T1(n)* T2(n) =O(n^n^n).落实到具体的代码可以把乘法法则看成是嵌套循环。

int cal (int n){

int ret = 0;
int i = 1;
for(;i < n;++i){
    ret =ret + f(i);
}

}

int f(int n){

int sum = 0;
int i = 1;
for(;i < n;++i){
    sum = sum +i;
}
return sum;

}

分析:

单独看cal()函数。f()只是一个普通操作,那么4~6行的时间复杂度就是T1(n) = O(n).单f()函数本身不是一个简单的操作,它的时间复杂度T2(n)=O(n),所以,整个cal()函数的时间复杂度就是T(n) = T1(n)* T2(n)=O(n * n)= O(n^n)

几种常见时间复杂度实例分析

image.png

图中的复杂度量级可以粗略的分为两类,多项式量级和非多项式量级。其中非多项式量级只有两个:O(2^n) 和O(n!) 时间复杂度为非多项式量级的算法叫做NP问题,当数据规模n越来越大时,非多项式量级算法的执行时间会急剧增加,求解的问题的执行时间会无限增大。所以非多项式时间复杂度的算法其实是非常低效的算法

常见的多项式时间复杂度 1.O(1) O(1)只是常量级时间复杂度的一种表示方法,并不是指代码指执行了一行,代码有3,4行,他的时间复杂度也是O(1),而不是O(3)

int i =9; int j =10; int sum = i+j

**总结:**只要代码的执行时间不随n的增大二增长,这样的代码的时间复杂度我们都记作O(1).

2.O(logn)、O(nlogn) 对数介时间复杂度非常常见,同时也是最难肥西的一种时间复杂度。

i=1;

while (i<=n){

 i = j*2;
 

}

根据我们前面讲得复杂度分析方法,第三行代码是循环执行次数最多的。只要计算出这行代码被执行了多少次,就能知道这段代码的复杂度。从代码中看出变量i的值从1开始取,每循环一次就乘2.当大于n时循环结束。实际上,变量i的取值就是一个等比数列。

image.png 所以想要知道这个代码执行的次数,通过2^x=n,x=log2n,所以这段代码的时间复杂度就是O(log2n)

i = 1;

while(i <= n){

i = i * 3;

}

同上的原理这个的时间复杂度就是O(log3n) log3n就等于log32 * log2n,所以O(log3n)=O(C * log2n),其中C=log32是一个常量。基于我们前面的一个理论:在采用大O标记复杂度的时候,可以忽略系数,即O(Cf(n)) = O(f(n)).所以,O(log2n)就等于O(log3n)。因此在对数阶时间复杂度的表示方法里,我们忽略对数的“底”,统一表示为O(logn).

如果一段代码的时间复杂度是O(logn),那我们执行n遍,时间复杂度就是O(nlogn),而且,O(nlogn)也是一种非常常见的算法时间复杂度。比如归并排序,快速排序的时间复杂度都是O(nlogn) 3.O(m+n)、O(m * n)

int cal(int m,int n) {

int sum_1 = 0;
int i = 1;
for (; i <m;++i) {
    sum_1 = sum_1 + i;
}
int sum_2 = 0;
int j = 1;
for (; j< n;++j){
    sum_2 = sum_2 +j;
}
return sum_1+ sum_2;

}

从代码上看,m和n是表示两个数据规模。我们无法看出m和n谁的量级大,所以在表示复杂度的时候。就不能简单的利用加法法则。省略调其中一个。所以以上代码的时间复杂度就是O(m+n).

所以加法法则就不正确了,所以加法规则改为:T1(m)+T2(n)=O(f(m)+g(n))。但是乘法法则则继续有效: T1(m) * T2(n) = O(f(m) * f(n))

空间复杂度分析

时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系,类比一下,空间复杂度全称就是渐进空间复杂度。表示算法的存储空间与数据规模之间的增长关系

样例:

void print(int n){

int i = 0;
int [] a = new int[n];
for(i;i<n;++i){
    a[i] = i*j;
}
for (i = n-1; i>=0;--i){
    print out a[i]
}

}

代码分析: 第二行,第三行都是常量阶,其他最大复杂度是O(n)

内容小结

时间复杂度:表示算法的执行时间与数据规模之间的增长关系。

空间复杂度:表示算法的存储空间与数据规模之间的增长关系

复杂度也叫渐进复杂度,他暴扣时间复杂度,用来分析算法执行效率与数据规模之间的增长关系,可以粗略的表示,越高阶复杂度的算法,执行效率越低。常见的复杂度并不多。从低阶到高阶:O(1)、O(logn)、O(n^n)

image.png