最近再看各种排序的优劣与优化,碰到算法复杂度相关内容。为了记录和分享留下此笔记,如果有不对欠佳的地方还望各位大佬海涵评论指出,在下会多多学习。
相同的需求可以使用不同的算法实现,而一个算法的优劣会直接影响整个程序的运行效率与用户体验。
算法复杂度指算法在运行时所需的时间与内存资源。也就是时间复杂度和空间复杂度。
- 时间复杂度:是指执行当前算法所消耗的时间。
- 空间复杂度:是指执行当前算法需要占用多少内存空间。
一、时间复杂度
当我们要想知道一个算法的时间复杂度,最直观的方法就是将这个算法运行一遍自然就知道他的消耗时间了。但是这种方式容易受运行环境的影响,在性能不同的设备上跑出来的结果相差会很大。而且在算法开发过程中,还没有完整的逻辑是没有办法进行的。
因此,需要另一种通用的表述方法:【大O符表示法】,即T(n) = O(f(n))
例子:
0. int j = 0;
1. for(i=1; i<=n; ++i)
2. {
3. j = i;
4. j++;
5. }
根据【大O符号表示法】这段代码的时间复杂度为:O(n)
在大O符号表示法中,时间复杂度的公式是:T(n)=O(f(n)),其中的f(n)标识每行代码行次数之和,而O表示正比例关系,这个公式的全称是:算法的渐进时间复杂度
解:上面的例子中假设每行代码的执行时间都是一样的,我们用1颗粒时间表示,那么这个例子的第一行耗时是1个颗粒时间,第三行的执行时间是n个颗粒时间,第四行也是n个颗粒时间。那么总时间就是1颗粒+n颗粒时间+n颗粒时间,即(1+2n)个颗粒时间,即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(n²)
- 立方阶O(n³)
- K次方阶O(n^k)
- 指数阶(2^n)
1.常数阶O(1)
1. int i = 1;
2. int j = 2;
3. ++i;
4. j++;
5. int m = i + j;
无论代码执行多少行,只要没有循环(for,while)等复杂结构,那这个代码的时间复杂度统一为O(1)
这个例子在执行过程中的时间并不是随着某个变量的增长而增长,那么这类代码无论有几万几十万行,都可以用O(1)来表示他的时间复杂度。
2.线性阶O(n)
0. int j = 0;
1. for(i=1; i<=n; ++i)
2. {
3. j = i;
4. j++;
5. }
for循环里面的代码会执行n遍,因此他消耗的时间是随着n的变化而变化的,因此此类的代码都可以用O(n)来表示他的时间复杂度。
3.对数阶O(logN)
1. int i = 1;
2. while(i<n)
3. {
4. i = i * 2;
5. }
在While循环里,每次豆浆i * 2,i 距离 n 会越来越近。假设循环x次之后,i 就大于 2了,此时这个循环就退出了,也就是说2的x次方等于n,那么x=log2^n。
也就是说当循环log2^n次之后,这个代码段就结束了。因此这个代码的时间复杂度为:O(logn)
4.线性对数阶O(nlogN)
0. int i = 0;
1. for(m=1; m<n; m++)
2. {
3. i = 1;
4. while(i<n)
5. {
6. i = i * 2;
7. }
8. }
线性对数阶O(nlogN) 其实非常容易理解,将复杂度O(logn)的代码循环N遍,他的时间复杂度就是
n*O(logn),也就是O(nlogN)。我的理解是线性阶和对数阶的结合。
5.平方阶O(n^2)
1. for(i=1; i<=n; i++)
2. {
3. for(j=1; j<=n; j++)
4. {
5. j = i;
6. j++;
7. }
8. }
平方阶O(n^2)也比较容易理解,如果把线性阶O(n)的代码在嵌套循环一遍,他的时间复杂度就是O(n^2)。可以这么理解:
这段代码其实就是嵌套了2层n循环,他的时间复杂度就是O(n*n)。即O(n^2)
如果将其中一层的循环n改为m,那么时间复杂度就是:O(m*n)
二、空间复杂度
既然时间复杂度不是用来计算程序实际的耗时,那么相同的空间复杂度也不是程序运行中实际占用的空间。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,S(n)=O(f(n))。
比如插入排序算法的空间复杂度是O(1),递归排序算法的空间复杂度是O(n),因为每次递归都要存储返回数据。
空间复杂度常用的有:O(1),O(n),O(n^2)
1.空间复杂度O(1)
1. int i = 1;
2. int j = 2;
3. ++i;
4. j++;
5. int m = i + j;
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,那么这个算法空间复杂度为一个常量,可表示为O(1)。代码中的i,j,m所分配的空间都不随着处理的数据量而变化,那么他的空间复杂度S(n)=O(1)
2.空间复杂度O(n)
0. int j = 0;
1. int[] m = new int[n]
2. for(i=1; i<=n; ++i)
3. {
4. j = i;
5. j++;
6. }
这段代码中第一行new了一个新的数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环但没有在分配新的空间,因此这段代码的空间复杂度只看第一行就可以了,即S(n)=O(n)
3.空间复杂度O(n^2)
0. int j = 0;
1. for(i=1; i<=n; ++i)
2. {
3. int[] m = new int[n]
4. for(j=1; j<=n; ++i)
5. {
6. j = i;
7. j++;
8. }
9. }
这段代码中在第一个嵌套for循环中new了一个新的数组,这个数据占用大小为n,因为是在for循环中,他的空间复杂度为S(n)=O(n*n) ,即:S(n)=O(n^2)
对于一个算法,时间复杂度和空间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间;反之,当追求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。所以在设计时需要根据需求和使用频率处理的数据量大小综合考虑设计出合适的算法。