算法的复杂度是用来衡量一个算法好坏的重要指标,其中包含时间复杂度(Time Complexity), 以及空间复杂度(Space Complexity)。我们一般说算法的复杂度指的都是算法的时间复杂度。当然有时候,时间复杂度也是可以靠消耗更多的空间来减少的。
在介绍算法的复杂度之前,先介绍一个概念:函数的渐进增长。
函数的渐进增长
假设有两个函数 A 和 B,它们的输入规模都是 n。 A算法要做 2 n + 3 2n + 3 2n+3次操作,B算法要做 3 n + 1 3n + 1 3n+1 次操作。那么这两个函数哪个更快呢。在假设硬件条件都一致的情况下,也就是在每次操作用时都一样的情况下,这两个函数谁的操作更少,谁就更快。 2 n + 3 < 3 n + 1 → n > 2 2n + 3 < 3n + 1 \rightarrow n > 2 2n+3<3n+1→n>2。 即当$ n > 2$ 时,函数 A 比函数 B 快。
此时我们给出这样的定义,输入规模 n n n 在没有限制的情况下,只要超过一个数值 N N N ,这个函数就总是大于另一个函数,则称函数是渐近增长的。
同时如果去掉后面的常数,如 + 1 , + 3 +1, +3 +1,+3, 我们会发现得出的结论依旧是不变的。所以我们可以忽略这些加法常数。
同时再看一个例子, 算法 A 是 2 n + 3 2n + 3 2n+3, 算法 B 是 2 n 2 + n + 1 2n^2 + n + 1 2n2+n+1, 算法 C 是 n 3 + n + 1 n^3 + n +1 n3+n+1。我们可以直观的看出随着 n n n 的增大,C 增长的最快,其次是 B, 最后是 A。 而去掉了与次高项相乘的常数并不影响最终的结果。因此我们可以忽略。
由此可以得出结论,判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。
即函数的操作次数越少,则代表该函数直到完成用时更少,则可以得出该算法的时间复杂度更小。因此,我们可以通过估算一个函数的操作次数,并以此代表该算法的时间复杂度来衡量算法的优劣性。
时间复杂度
首先,给出时间复杂度的定义:在进行算法分析时 , 语旬总的执行次数 T ( n ) T ( n ) T(n) 是关于问题规模 n n n 的函数, 进而分析 T ( n ) T ( n ) T(n) 随 n n n 的变化情况来确定 T ( n ) T ( n ) T(n) 的级数。记做: T ( n ) = O ( f ( n ) ) T ( n ) = O(f(n)) T(n)=O(f(n))。 其中 f ( n ) f(n) f(n) 是关于 n n n 的函数。
推导时间复杂度:
1.用常数 1 取代运行时间中的所有加法常数 。
2 .在修改后的运行次数函数中,只保留最高阶项 。
3.如果最高阶项存在且不是 1 ,则去除与这个项相乘的常敢 。
得到的结果就是大 O 阶。
常阶数: 执行次数为常数,且与问题规模 n n n 无关。例如: n = 1 , s u m = n ( n + 1 ) n = 1, sum = n (n + 1) n=1,sum=n(n+1) 。上述代码一共执行两次,每句执行一次,可记为 O ( 2 ) O(2) O(2)。根据上述原则,忽略系数,则常阶数统一记为 O ( 1 ) O(1) O(1)。
线性阶: 线性阶的循环结构会复杂很多。要确定某个算法的阶次,我们常常需要确定某个特定语句或某个语句集运行的次数。因此,我们要分析算法的复杂度,关键就是要分析循环结构的运行情况。例如:
for i in range(n):
print(n)
其中, p r i n t ( n ) print(n) print(n) 这个语句会运行 n n n 遍。 所以该算法的时间复杂度为 O ( n ) O(n) O(n)。
对数阶:
count = 1
while(count < n):
count = count * 2
可以看出,假设该算法的运行次数为 x x x,则 2 x < n 2^x<n 2x<n。得出 x < l o g 2 n x <log_2n x<log2n。因此可以推导出概算法的时间复杂度为 O ( l o g n ) O(logn) O(logn)。
平方阶:
for i in range(n):
for j in range(n):
sum = i + j
上述例子中,我们很容易看出 s u m = i + j sum = i + j sum=i+j 运行了 n 2 n^2 n2 次。所以该算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
for i in range(n):
for j in range(m):
sum = i + j
再看这个例子,很容易看出该算法运行了 m + n m + n m+n 次。 所以该算法的时间复杂度为 O ( m + n ) O(m+n) O(m+n)。
for i in range(n):
for j in range(i, n):
sum = i + j
对于这个例子而言,当
i
=
0
i=0
i=0 时,内循环了
n
n
n 次;
i
=
1
i=1
i=1 时, 内循环了
n
−
1
n-1
n−1 次;… ;当
i
=
n
−
1
i=n-1
i=n−1 时, 内循环了
0
0
0 次。
所以,该算法的执行次数为
(
n
+
(
n
−
1
)
+
(
n
−
2
)
+
.
.
.
+
1
)
=
n
(
n
+
1
)
2
=
n
2
2
+
n
2
(n + (n-1) + (n-2) + ... + 1) = \frac{n(n+1)}{2}=\frac{n^2}{2} + \frac{n}{2}
(n+(n−1)+(n−2)+...+1)=2n(n+1)=2n2+2n。由上述我们说的推导原则可知,忽略到次高项以及最高项的系数,得出该算法的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。
由此,我们可以得出结论,算法的时间复杂度等于循环体的复杂度乘以循环运行的次数。
最坏时间复杂度与平均时间复杂度:这两个概念很好理解,即考虑输入参数, 在最坏的情况下时间复杂度是多少,相对应就还有最好的时间复杂度;平均就更好理解了。比如说我们在一个list中查找一个数,如果第一个数就是,那么算法执行次数是 O ( 1 ) O(1) O(1), 如果直到遍历到最后一个才找到,则运行次数是 O ( n ) O(n) O(n)。 那么平均就是 O ( n 2 ) O(\frac{n}{2}) O(2n)。
空间复杂度
之前在前面提到过算法是可以用空间的消耗来换取时间复杂度的减小的。举个例子,比如要计算100以内的素数,如果你写一个算法的话,也就是每个数都需要经过一定的计算才能得到结果。这时你也可以直接建立一个100的数组,里面存着每个数以及它是不是素数,这样你只要去数组里调出结果就行。
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公
式记作:
S
(
n
)
=
O
(
f
(
n
)
)
S(n)= O(f(n))
S(n)=O(f(n)) ,其中,
n
n
n 为问题的规模,
f
(
n
)
f(n)
f(n) 为语句关于
n
n
n 所占存储空间的函数。
若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为 O ( 1 ) O(1) O(1) 。