在估算代码的时间复杂度中,我们往往会发现由于编程语言中类似于break
和continue
关键词的出现,复杂度估算会出现多种模棱两可的情况。例如如下代码:
class Complexity:
@staticmethod
def search_position(array: list, element: int) -> int:
position = -1
index = 0
for value in array:
if value == element:
position = index
break
index += 1
return position
if __name__ == "__main__":
complexity = Complexity()
print("-100 position is: %d" % complexity.search_position([-1, -3, 199, 100, 50, 0], -100))
print("100 position is: %d" % complexity.search_position([-1, -3, 199, 100, 50, 0], 100))
print("-1 position is: %d" % complexity.search_position([-1, -3, 199, 100, 50, 0], -1))
正如代码所示:我们在[-1, -3, 199, 100, 50, 0]
数组中查找-100、100和-1三个不同的元素的时候,由于受到break
关键词的影响,我们分别执行了 6 次,4 次和 1 次。这个时候,我们如果粗略认为这段代码的时间复杂度为
O
(
n
)
O(n)
O(n) 从思维上就感觉不大准确。为了更好的评估时间复杂度,我们便有了更加细分的最好、最差、均分和摊分四种时间复杂度类型。
最好和最差时间复杂度
如上述代码所示,我们在查找 -1 的时候,我们直接在第一个元素比对的时候就确认其所在的位置,这是一个类似于 O ( 1 ) O(1) O(1) 复杂度的情况,我们可以将其称为最好时间复杂度。相反,我们在查找元素 -100 的时候,元素并不能在列表里面,我们必须挨个比对完才能确定其是否真的不存在,这就是最差时间复杂度。
平均时间复杂度
有了最好和最差时间复杂度,我们自然而然的就想到了数学里平均这个概念。所以我们可以用平均去衡量在正常情况下查找元素的时间浮动,那么上述代码的平均值该怎么算呢?这里需要引入概率论中数学期望的概念。由代码可以看出,我们存在两种大情况,即元素存在于列表中和元素不存在列表中。这样我们就可以设置两者的概率均为 50%。同理,在已知这个元素存在的情况下,它在任何一个位置的概率为 1 / n 1/n 1/n ,所以我们数学期望值就是如下公式:
T ( n ) = 1 ∗ 1 2 n + 2 ∗ 1 2 n + . . . + n ∗ 1 2 n + n ∗ 1 2 = 3 n + 1 4 T(n)=1*\frac{1}{2n}+2*\frac{1}{2n}+...+n*\frac{1}{2n}+n*\frac{1}{2}=\frac{3n+1}{4} T(n)=1∗2n1+2∗2n1+...+n∗2n1+n∗21=43n+1
根据时间复杂度常数和系数舍弃的原则,我们可以得出其平均时间复杂度为 O ( n ) O(n) O(n) 。
摊分时间复杂度
最后如果我们把元素不存在的情况作为一个特殊情况来看待,因为不存在的情况都是要比对 n 次的。即我们可以说这种情况摊分到位置肯定存在情况下,每个位置头上的时间为 n/n 单位时间。即不存在情况的摊分时间复杂度为 O ( n / n ) = O ( 1 ) O(n/n)=O(1) O(n/n)=O(1) 。
在日常项目中,我们可能需要考虑项目某些性能的边际值。比如 HTTP 请求最长响应时间,这时候我们就要仔细估算后端代码的最差时间复杂度。同时我们在非关键业务中(比如监控业务,备份业务等),将更加注重系统平均性能的衡量。这个时候我们就需要优化代码的平均时间复杂度。总之,数据结构与算法需要具体情况具体分析,是不会存在一种定式的答案的。