课程:《复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?》 总结
算法的一个非常重要的的考量指标是执行效率,复杂度分析就是用来衡量代码执行效率的一种方法。前面又提到复杂度分析是数据结构和算法学习的精髓。
为什么需要复杂度分析?
-
实际的测试结果非常依赖测试环境
比如测试环境硬件配置不一样,得到的结果截然不同 -
测试结果受数据规模的影响很大
比如排序算法,就会受实际测试数据的有序度和数据的规模影响。导致测试的结果无法反应算法的性能。 -
可以在开发阶段更好的指导代码的编写
复杂度分析可以估算代码的执行效率,从而在根据实际选择算法时起到一定的指导作用。
复杂度分析不需要具体测试数据测试,就可以大概估计算法执行效率。既快速方便,又不受测试环境的影响,所以我们需要复杂度分析。
复杂度分析包括时间复杂度分析和空间复杂度分析,那么如何进行复杂度分析呢?
大O复杂度表示法
如果我们将每行代码的执行时间看成固定的,那所有代码的执行时间 T(n) 与每行代码的执行次数成正比。我们再将每行代码的执行次数之和用 f(n) 表示,那就可以总结出一个公式: T(n) = O(f(n)),其中 n 表示数据规模的大小,O 代表 T(n) 与 f(n) 之间是正比关系。这就是 大O时间复杂度表示法。
大O时间复杂度表示的是代码执行时间随数据规模增长的变化趋势。并不是正真的执行时间。当n很大时,公式中的低阶、常量、系数三部分对变化趋势影响不大,所以可以忽略,只记录最大量级的那个就可以了。
时间复杂度分析
四个实用的时间复杂度分析方法:
-
单一循环看次数执行最多的
-
如果代码只受单一数据规模 n 的影响,非嵌套循环相加取量级最大的
-
如果代码受多个数据规模的影响,非嵌套循环相加取和
-
嵌套循环内外相乘取乘积
常见的时间复杂度有:O(1)、O(logn)、O(n)、O(nlogn)、O(n2)。
此外还有:O(m+n)、O(m*n),前者为复杂度有两个数据规模决定,后者则是起决定性的两个数据规模是嵌套的。
空间复杂度分析
由时间复杂度的表示意义可知,空间复杂度表示的是算法的存储空间随数据规模增长的变化趋势。
常见的空间复杂度有:O(1)、O(n)、O(n2)。空间复杂度和时间复杂度类似,所以和数据规模 n 无关的相,我们都可以忽略。
练习
1. 时间复杂度练习:
练习一:
def test(n:int):
num = n * 5
return num
如上代码所示:函数 test,有一个形参 n,这个函数需要执行的代码为第2行、第3行,在调用函数 test 的过程中,第2行代码是将 n 和 5 做了乘法,运行次数为1次,第3行代码为返回 num 的值,执行次数也为1次,根据大O复杂度表示法,可知 f(n) 为每行代码执行次数之和,所以这段代码的时间复杂度 T(n)=O(1 + 1) = O(2),又因为时间复杂度研究的是代码执行时间随数据规模增长的趋势。可见,上述代码中每行代码的执行次数之和是一个常量,当n很大时,常量对变化趋势影响不大,所以可以忽略。这种代码执行时间为常量的时间复杂度都可以表示为O(1)。
def test(n:int):
num = n * 5
num = num * 5
return num
def test(n:int):
num = n * 5
num = num * 5
j = 6
num += j
return num
再看上述两端代码,虽然代码的行数变多了,但是通过分析可知,这几段代码中每行代码的执行次数之和都是常量,执行时间并不随数据规模n的增长而变化。所以他们的时间复杂度都是 O(1)。
一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是O(1)。
练习二:
def test(n:int):
for i in range(n):
print(i)
再看上述代码,分析可知第2行代码的执行次数是 n 次,第3行代码的执行次数也是 n 次,可得这段代码的时间复杂度 T(n)=O(n + n)=O(2n)。我们将系数忽略,取最大量级的一个,得到 T(n)=O(n)。所以这段代码的时间复杂度就是 O(n)。这也验证了“单一循环看次数执行最多的行”的分析方法。
练习三:
def test(n:int):
for i in range(n):
for j in range(n):
print(i * j)
再来看一个嵌套循环,如上图,分析可知第2行代码执行次数为n,第3行代码执行次数为n * n,第4行代码的执行次数为n * n。所以根据大O复杂度表示法可得这段代码的时间复杂度为:T(n)=O(n + n * n + n * n)=O(n+2n2)。又知低阶、系数可以忽略,这段代码的时间复杂度为:T(n)=O(n2)。刚好验证了“嵌套循环内外相乘取乘积”的分析方法。
练习四:
def test(n:int):
i = 1
while(i < n):
i = i * 2
print(i)
如上图,根据单一循环看次数执行最多的行的分析方法,可知我们只要分析第4行代码或者第5行代码就可以了。代码中循环结束的条件是 i>=n,但是 i 并不是每次递增1,i 的变化是每次乘以2。假设第4行代码执行x次后循环结束,那么 i 的取值列出来就是20, 21, 22, 23, 24, 25, …,可以看出 i 的值为 2x,x 即为第4行代码执行的次数。也就是当 2x >= n 时循环结束。那我们就可以近似的得出第4行代码的执行次数 x=log2n ,所以这段代码的时间复杂度 T(n)=O(log2n )。那么当第4行代码变成 i = i * 3 或者 i = i * 10 等的时候,那这段代码的时间复杂度就是 O(log3n )、O(lgn)。不管是以哪个数字为底,我们都可以把这种对数阶的时间复杂度表示为 O(logn) 。所以上述代码的时间复杂度为 T(n)=O(logn)。
练习五:
def test_sub(n:int):
i = 1
while(i < n):
i = i * 2
print(i)
def test(n:int):
for i in range(n):
test_sub(n)
我们已经知道上述代码中 test_sub 函数这段代码的时间复杂度为O(logn),那 test 这段代码是将 test_sub 代码段嵌套在了一个 for 循环中,根据“嵌套循环内外相乘取乘积”的分析方法,可知 for 循环中 test 函数的执行次数为 n,所以外部循环代码段的时间复杂度为 O(n),又因为内部循环代码段的时间复杂度,也就是 test_sub 代码段的时间复杂度为:O(logn),可得到 test代码段的时间复杂度T(n)=O(n) * O(logn)=O(nlogn)。
练习六:
def test(m:int, n:int):
for i in range(m):
print(i)
for j in range(n):
print(j)
前面的代码段都是只有一个参数 n,上图中的 test 函数有两个形参 m 和 n ,再看函数体,第2、3行代码的执行次数受 m 的影响,第4、5行代码的执行次数受 n 的影响。即代码段的每行代码的执行次数之和受到了 m 和 n 两个数据规模的影响。这种情况下,m 和 n 都不能确定是多少,所以都不能忽略。我们分析可知,第2、3行代码的时间复杂度 T1(m)=O(m),第4、5行代码的时间复杂度 T2(n)=O(n),所以根据“如果代码受多个数据规模的影响,非嵌套循环相加取和”的分析方法可得,这段代码的时间复杂度为:T1(m)+T2(n)=O(m)+O(n)=O(m+n)。
练习七:
def test(m:int, n:int):
for i in range(m):
for j in range(n):
print(i * j)
上述代码段是一个嵌套循环,我们可以先将第3、4行代码段看成是一个单一循环,很容易可以得到它的时间复杂度T(n)=O(n),再看第2行代码,这是外部循环部分,这行代码的执行次数受 m 的影响,从而得到外部循环代码段的时间复杂度为T(m)=O(m),根据“嵌套循环内外相乘取乘积”的分析方法,可得 test 代码段的时间复杂度为:T(m) * T(n)=O(m) * O(n)=O(m * n)。
2. 空间复杂度练习:
练习一:
def test(n:int):
a = 4
sum = a + n
return sum
上述代码中,第2行代码中,我们申请了一块空间 a 来存放数字4,第3行代码中我们又申请了一块空间 sum 来存放 a+n 的和。第4行代码并没有申请空间,只是返回了sum 的值。由于空间复杂度表示的是算法的存储空间随数据规模增长的变化趋势,而经过我们分析,不管n的数据规模如何变化,这段代码永远只是申请了a和sum两块空间,并没有对存储空的的变化趋势造成影响,可见是常量阶的,所以这段代码的空间复杂度为:O(1)。
def test(n:int):
num_list = []
for i in range(3):
num_list.append(i * n)
再来看上述代码,第2行代码中创建了一个list对象 num_list,第3、4行代码中是循环三次,每次将 i * n 的值存放到 num_list 对象中。由于 num_list 是一个可变对象,每存入一个数据就需要申请一块存储空间。但是由于第3行代码中,循环的次数是固定的3次,所以不管数据规模 n 如何变化,代码中只是申请了三次存储空间。也没有对存储空间的变化趋势造成影响,所以它的空间复杂度也是常量阶的,所以也是 O(1)。
练习二:
def test(n:int):
num_list = []
for i in range(n):
num_list.append(i)
看上述代码,和前面代码的不同之处在于第3、4行代码的执行次数受 n 的数据规模的影响,我们知道 num_list 对象是一个列表,每放入一个数据都会申请一块空间存储,如果放入 n 个数据,那就要申请 n 块空间存储,所以这段代码的申请存储空间的次数或总量,会受到 n 的影响,所以可以得出这段代码的空间复杂度为:O(n)。
def test(n:int):
num_list = []
for i in range(n):
for j in range(n):
num_list.append(j)
上述代码是在前一代码段的基础上又嵌套的一层循环,根据“嵌套循环内外相乘取乘积”的分析方法,可知,这段代码的空间复杂度为:O(n*n)=O(n2)。
以上就是我对本节课程的理解,当然例子都是写的简单的例子。可能有些理解的不到位,或是表达的不准,欢迎大家指正,帮助我更好的理解。“复杂度分析并不难,关键在于多练。”