本篇用到的几处符号:
∈ \in ∈: 属于
∉ \notin ∈/: 不属于
∀ \forall ∀: 任意
渐近符号
在上方的函数中,假设 i i i 每次自增的实际运算次数为 a a a,判断 i ∈ S i \in S i∈S 的运算次数为 b b b,打印的运算次数为 c c c。 a , b , c a, b, c a,b,c 均为常数,那么时间复杂度为:
- O ( n ) O(n) O(n):只考虑上限,此时 ∀ i ∈ S \forall i \in S ∀i∈S,运算最多有 ( a + b + c ) n (a+b+c)n (a+b+c)n 次,忽略掉常数。
- Ω ( n ) \Omega(n) Ω(n):只考虑下限,此时 ∀ i ∉ S \forall i \notin S ∀i∈/S,运算最少有 ( a + b ) n (a + b)n (a+b)n 次,忽略掉常数。
- Θ ( n ) \Theta(n) Θ(n):当且仅当上限和下限为同一数量级时,可以使用 Θ \Theta Θ, 本例刚好满足。
虽然常数有影响,但大多数情况下,低复杂度的表现都优于高复杂度。
此外,影响因数较小的项在 相加 时可忽略。比如:
- O ( n 2 + n ) = O ( n 2 ) O(n^2 + n) = O(n^2) O(n2+n)=O(n2)
- 如果 n < m n < m n<m,则 O ( n + m ) = O ( m ) O(n+m) = O(m) O(n+m)=O(m)
渐近符号也可以表示空间复杂度。
O O O 发音 \oʊˈ\, Ω \Omega Ω 发音 \oʊˈmeɡə\, Θ \Theta Θ 发音 \ˈθeɪtə\。它们也读作 大 O O O, 大 Ω \Omega Ω,大 Θ \Theta Θ。此外还存在 𝑜, 𝜔,读作小 o 和小 omega,但在计算机科学中用得极少,此处不详述。
实际中常见这样的表示 O ( n 2 ) , O ( m + n ) , Θ ( m n ) O(n^2), O(m + n), \Theta(mn) O(n2),O(m+n),Θ(mn) ,单纯的常数上限则表示为 O ( 1 ) O(1) O(1)。 最常用的是上限 O O O ,然后是 Θ \Theta Θ,很少用下限 Ω \Omega Ω 。
严格分析以上伪代码的时间复杂度:其中最后一行不一定执行,所以是 O ( c ) O(c) O(c) ,即 O ( 1 ) O(1) O(1)。三行合计是 Θ ( n ) + n × ( Θ ( 1 ) + O ( 1 ) ) = Θ ( n ) + n × Θ ( 1 ) = Θ ( n ) \Theta(n) + n \times (\Theta(1) + O(1)) = \Theta(n) + n \times \Theta(1)= \Theta(n) Θ(n)+n×(Θ(1)+O(1))=Θ(n)+n×Θ(1)=Θ(n)。
复杂度累计的技巧
归纳一点技巧。先把示例改得复杂一点,其中参数
S
1
,
S
2
S_1, S_2
S1,S2 为集合,右侧注释仅代表行数。
记 C k C_k Ck 为这份代码中第 k k k 行的时间复杂度, C k → l C_{k\rightarrow l} Ck→l 为 k k k 到 l l l 行的时间复杂度,可得 C 1 → 6 = C 1 + C 1 × C 2 → 6 C_{1\rightarrow6} = C_1 + C_1\times C_{2 \rightarrow 6} C1→6=C1+C1×C2→6 。
-
某块所有需要相加的复杂度累计后最小为 Θ ( 1 ) \Theta(1) Θ(1), 不存在 O ( 1 ) O(1) O(1)。比如 C 4 + C 5 → 6 = Θ ( 1 ) + O ( 1 ) = Θ ( 1 ) C_4 + C_{5\rightarrow 6} = \Theta(1) + O(1) = \Theta(1) C4+C5→6=Θ(1)+O(1)=Θ(1) 。所以可以忽略掉 O ( 1 ) O(1) O(1)。
-
Θ ( 1 ) \Theta(1) Θ(1) 在与其他复杂度相加或相乘时可以忽略。
-
对于循环发起处,时间复杂度通常等于循环次数。如果不等于说明该行有几处步骤,拆分到不同行。本例的循环不用拆分,在忽略掉 O ( 1 ) O(1) O(1) 和 Θ ( 1 ) \Theta(1) Θ(1) 后,等式 “ C 1 → 6 = C 1 + C 1 × C 2 → 6 C_{1\rightarrow6} = C_1 + C_1\times C_{2 \rightarrow 6} C1→6=C1+C1×C2→6” 中可以忽略掉 “ C 1 + C_1 + C1+”。
现在,我们可以直接忽略掉 2、4、5、 6 行,但保留第二行中的 if continue。分析 C 3 C_3 C3, Θ ( ∣ S 2 ∣ ) \Theta(|S_2|) Θ(∣S2∣) 结合第二行的 if 可得 O ( ∣ S 2 ∣ ) O(|S_2|) O(∣S2∣)。总结果 C = C 1 × C 3 = Θ ( ∣ S 1 ∣ ) × O ( ∣ S 2 ∣ ) = O ( ∣ S 1 ∣ ∣ S 2 ∣ ) C = C_1 \times C_3 = \Theta(|S_1|) \times O(|S_2|) = O(|S_1||S_2|) C=C1×C3=Θ(∣S1∣)×O(∣S2∣)=O(∣S1∣∣S2∣)。
时间复杂度的分析总结:
-
可以忽视 O ( 1 ) , Θ ( 1 ) O(1), \Theta(1) O(1),Θ(1) 的部分。
-
在循环发起处,通常可直接将时间复杂度与内部累计的时间复杂度相乘。如果循环发起处包含多个步骤,使该行的时间复杂度 > > > 循环次数,将步骤拆分到不同行。
空间复杂度的分析总结:比较简单,一般考虑元素容器即可。本例中没有新建元素容器,所以结果为 Θ ( 1 ) \Theta(1) Θ(1)。
此外函数最低的 时/空 复杂度都是 Θ ( 1 ) \Theta(1) Θ(1), 因为要考虑函数栈的影响。
个人倾向
- Θ \Theta Θ 比较精确,而且我并不认为有什么额外的思想负担,能用的情况下尽量用。这对评估平均复杂度有很大帮助,不过也不强求。一般只用 O O O,这是不应该的。我认为在乎这些是修养的体现,不过大多数人不注重。
- 在职场面试中,如果你用到 Θ \Theta Θ 应该是会加分的。
- 如果不能将复杂度降级,优先选择自己熟悉的算法,因为人力也很宝贵。尤其是在客户端中,数据量远没有服务端那么大。
笔者在 Leetcode 上写算法教程,目前上架的有 《图论入门》 和 《图论进阶》,且在陆续写其他 Leetbooks。致力于提升广大读者真正的算法水平,并兼顾不同阶段的读者,如本科阶段、硕士阶段、职场面试、算法竞赛。
读者如有困惑可留言。欢迎提出写法或者宣传上的建议,不甚感谢!