这周主要总结了时间复杂度的学习,跟小伙伴们分享下,欢迎指正。
一、为何需要分析算法复杂度
挺多同学本科都学习过数据结构和算法这门课,但是有没有想过这门课到底是解决什么问题?科学家设计这些数据结构和算法是要干嘛?
其实,最终的目的只有一个:让我们写的代码在计算机上运行的速度更快,使用的内存更省!,可是如何才能知道我们写的代码使用多少运行时间和内存呢?这就需要分析算法时间复杂度和空间复杂度,只有学会分析这 2 者,才能知道我们设计的算法到底有没有性能的提升,不然你费了很大功夫想了一个算法,却发现运行速度慢如乌龟,得不偿失。
如果能够在运行算法之前就能知道算法大概的执行时间那就好了,而复杂度分析恰好可以解决这个问题!复杂度分析又分为 2 种:
1.1 运行后分析
这种就是写完算法直接放到机器上面跑,统计下到底用了多少时间和内存,但是这种方法有 2 个缺点:
- 测试结果依赖运行机器:性能强的机器当然需要的时间少
- 测试结果依赖于测试用的数据:比如对无序的数组和有序的数组排序的时间大不相同
1.2 运行前分析
那既然运行后分析有不可避免的缺点,有没有办法在纸上提前计算一下算法大概的执行时间和内存用量呢?当然有,就是今天的主角大 O 复杂度表示法!
二、大 O 复杂度表示法
我用一个例子来一步步解释大 O 复杂度表示法到底是什么意思:
int cal(int n) {
int sum = 0;
int i = 1;
int j = 1;
for (; i <= n; ++i) {
j = 1;
for (; j <= n; ++j) {
sum = sum + i * j;
}
}
}
对 CPU 而言执行程序分为以下 3 步:
- 读如代码指令和数据
- 计算
- 输出数据
因为我们只是在运行前粗略地估计算法的运行时间,因此可以假设每行代码在 CPU 上运行的时间都相同,为 cputime
,那么我们就可以直接计算出上面函数中所有代码执行的总时间(次数 x 单位时间),n 表示输入数据的规模:
- 第 2、3、4 行:每行需要 1 个
cpu_time
,共 3 ∗ c p u t i m e 3 * cputime 3∗cputime - 第 5、6 行:因为都是循环 n 次,所以需要 2 n ∗ c p u t i m e 2n * cputime 2n∗cputime
- 第 7、8 行:因为内外一共有 2 层 n 次的循环,所以需要 2 n 2 ∗ c p u t i m e 2n^2 * cputime 2n2∗cputime
那么总的运行时间 T(n)
为:
T ( n ) = ( 2 n 2 + 2 n + 3 ) c p u t i m e T(n) = (2n^2 + 2n + 3)cputime T(n)=(2n2+2n+3)cputime
对于每个确定的算法,所有代码的执行次数一定,那么上面的 T(n)
与所有代码的执行次数 2 n 2 + 2 n + 3 2n^2 + 2n + 3 2n2+2n+3 成正比关系,比例系数就是 CPU 执行每行代码的时间 cputime
,因为可以把上式写成大 O 复杂度表示法:
T ( n ) = O ( f ( n ) ) T(n) = O(f(n)) T(n)=O(f(n))
其中:
T(n)
:表示算法执行的总时间f(n)
:表示算法总的执行次数,就是 2 n 2 + 2 n + 3 2n^2 + 2n + 3 2n2+2n+3O()
:表示T(n)
与f(n)
的正比关系,也就是大 O 的由来
所以上面函数的总的执行时间又可以写成:
T ( n ) = O ( 2 n 2 + 2 n + 3 ) T(n) = O(2n^2 + 2n + 3) T(n)=O(2n2+2n+3)
但是要注意:大 O 复杂度表示法并不是计算代码准确的运行时间,而是表示一种代码执行时间随着数据规模 n
增长的变化趋势,记住不是准确的时间,只是一种趋势而已,因为实际工作的算法可能需要接受很大量级的数据,通过分析算法运行时间与输入数据规模的变化趋势就能大概知道一个算法能不能在实际环境中很好的工作。
但是呢,上面的大 O 表示法还是不够简洁,比如当算法代码很多的时候,那我们是不是要在后面(或者前面)加上很多项:
T ( n ) = O ( 2 n 2 + 2 n + 3 + 4 n + 5 n + . . . ) T(n) = O(2n^2 + 2n + 3 + 4n + 5n + ...) T(n)=O(2n2+2n+3+4n+5n+...)
这也不方便,因此大佬们又想了方法:**只需要保留最大量级的运行次数即可!**这是因为当输入数据规模很大,比如 100000, 1000000 等,常数项 3、一阶项 2n 等低阶的运行次数对最高次项 2 n 2 2n^2