算法复杂度-(详细版)时间复杂度计算

---------------------------------------本篇文章来自力扣算法通关手册---------------------------------------------

如何计算时间复杂度呢?

求解时间复杂度一般分为以下几个步骤:

  • 找出算法中的基本操作(基本语句):算法中执行次数最多的语句就是基本语句,通常是最内层循环的循环体部分。
  • 计算基本语句执行次数的数量级:只需要计算基本语句执行次数的数量级,即保证函数中的最高次幂正确即可。像最高次幂的系数和低次幂可以忽略。
  • 用大 O 表示法表示时间复杂度:将上一步中计算的数量级放入 O 渐进上界符号中。

同时,在求解时间复杂度还要注意一些原则:

  • 加法原则:总的时间复杂度等于量级最大的基本语句的时间复杂度。

如果 T1(n)=O(f1(n))T1​(n)=O(f1​(n)),T2(n)=O(f2(n))T2​(n)=O(f2​(n)),T(n)=T1(n)+T2(n)T(n)=T1​(n)+T2​(n),则 T(n)=O(f(n))=max(O(f1(n)),O(f2(n)))=O(max(f1(n),f2(n)))T(n)=O(f(n))=max(O(f1​(n)),O(f2​(n)))=O(max(f1​(n),f2​(n)))。

  • 乘法原则:循环嵌套代码的复杂度等于嵌套内外基本语句的时间复杂度乘积。

如果 T1=O(f1(n))T1​=O(f1​(n)),T2=O(f2(n))T2​=O(f2​(n)),T(n)=T1(n)×T2(n)T(n)=T1​(n)×T2​(n),则 T(n)=O(f(n))=O(f1(n))×O(f2(n))=O(f1(n)×f2(n))T(n)=O(f(n))=O(f1​(n))×O(f2​(n))=O(f1​(n)×f2​(n))。

下面通过实例来说明如何计算时间复杂度。

2.3.1 常数 O(1)O(1)

一般情况下,只要算法中不存在循环语句、递归语句,其时间复杂度都为 O(1)O(1)。

O(1)O(1) 只是常数阶时间复杂度的一种表示方式,并不是指只执行了一行代码。只要代码的执行时间不随着问题规模 nn 的增大而增长,这样的算法时间复杂度都记为 O(1)O(1)。

def algorithm(n):
    a = 1
    b = 2
    res = a * b + n
    return res

上述代码虽然有 44 行代码,但时间复杂度也是 O(1)O(1),而不是 O(3)O(3)。

2.3.2 线性 O(n)O(n)

一般含有非嵌套循环,且单层循环下的语句执行次数为 nn 的算法涉及线性时间复杂度。这类算法随着问题规模 nn 的增大,对应计算次数呈线性增长。

def algorithm(n):
    sum = 0
    for i in range(n):
        sum += 1
    return sum

上述代码中 sum += 1 的执行次数为 nn 次,所以这段代码的时间复杂度为 O(n)O(n)。

2.3.3 平方 O(n2)O(n2)

一般含有双层嵌套,且每层循环下的语句执行次数为 nn 的算法涉及平方时间复杂度。这类算法随着问题规模 nn 的增大,对应计算次数呈平方关系增长。

def algorithm(n):
    res = 0
    for i in range(n):
        for j in range(n):
            res += 1
    return res

上述代码中,res += 1 在两重循环中,根据时间复杂度的乘法原理,这段代码的执行次数为 n2n2 次,所以其时间复杂度为 O(n2)O(n2)。

2.3.4 阶乘 O(n!)O(n!)

阶乘时间复杂度一般出现在与「全排列」、「旅行商问题暴力解法」相关的算法中。这类算法随着问题规模 nn 的增大,对应计算次数呈阶乘关系增长。

def permutations(arr, start, end):
    if start == end:
        print(arr)
        return
 
    for i in range(start, end):
        arr[i], arr[start] = arr[start], arr[i]
        permutations(arr, start + 1, end)
        arr[i], arr[start] = arr[start], arr[i]

上述代码中实现「全排列」使用了递归的方法。假设数组 arrarr 长度为 nn,第一层 for 循环执行了 nn 次,第二层 for 循环执行了 n−1n−1 次。以此类推,最后一层 for 循环执行了 11 次,将所有层 for 循环的执行次数累乘起来为 n×(n−1)×(n−2)×…×2×1=n!n×(n−1)×(n−2)×…×2×1=n! 次。则整个算法的 for 循环中基本语句的执行次数为 n!n! 次,所以对应时间复杂度为 O(n!)O(n!)。

2.3.5 对数 O(log⁡n)O(logn)

对数时间复杂度一般出现在「二分查找」、「分治」这种一分为二的算法中。这类算法随着问题规模 nn 的增大,对应的计算次数呈对数关系增长。

def algorithm(n):
    cnt = 1
    while cnt < n:
        cnt *= 2
    return cnt

上述代码中 cnt = 1 的时间复杂度为 O(1)O(1) 可以忽略不算。while 循环体中 cntcnt 从 11 开始,每循环一次都乘以 22。当大于等于 nn 时循环结束。变量 cntcnt 的取值是一个等比数列:20,21,22,…,2x20,21,22,…,2x,根据 2x=n2x=n,可以得出这段循环体的执行次数为 log⁡2nlog2​n,所以这段代码的时间复杂度为 O(log⁡2n)O(log2​n)。

因为 log⁡2n=k×log⁡10nlog2​n=k×log10​n,这里 k≈3.322k≈3.322,是一个常数系数,log⁡2nlog2​n 与 log⁡10nlog10​n 之间差别比较小,可以忽略 kk。并且 log⁡10nlog10​n 也可以简写成 log⁡nlogn,所以为了方便书写,通常我们将对数时间复杂度写作是 O(log⁡n)O(logn)。

2.3.6 线性对数 O(n×log⁡n)O(n×logn)

线性对数一般出现在排序算法中,例如「快速排序」、「归并排序」、「堆排序」等。这类算法随着问题规模 nn 的增大,对应的计算次数呈线性对数关系增长。

def algorithm(n):
    cnt = 1
    res = 0
    while cnt < n:
        cnt *= 2
        for i in range(n):
            res += 1
    return res

上述代码中外层循环的时间复杂度为 O(log⁡n)O(logn),内层循环的时间复杂度为 O(n)O(n),且两层循环相互独立,则总体时间复杂度为 O(n×log⁡n)O(n×logn)。

2.3.7 常见时间复杂度关系

根据从小到大排序,常见的时间复杂度主要有:O(1)O(1) < O(log⁡n)O(logn) < O(n)O(n) < O(n×log⁡n)O(n×logn) < O(n2)O(n2) < O(n3)O(n3) < O(2n)O(2n) < O(n!)O(n!) < O(nn)O(nn)。

2.4 最佳、最坏、平均时间复杂度

时间复杂度是一个关于输入问题规模 nn 的函数。但是因为输入问题的内容不同,习惯将「时间复杂度」分为「最佳」、「最坏」、「平均」三种情况。这三种情况的具体含义如下:

  • 最佳时间复杂度:每个输入规模下用时最短的输入所对应的时间复杂度。
  • 最坏时间复杂度:每个输入规模下用时最长的输入所对应的时间复杂度。
  • 平均时间复杂度:每个输入规模下所有可能的输入所对应的平均用时复杂度(随机输入下期望用时的复杂度)。

我们通过一个例子来分析下最佳、最坏、最差时间复杂度。

def find(nums, val):
    pos = -1
    for i in range(n):
        if nums[i] == val:
            pos = i
            break
    return pos

这段代码要实现的功能是:从一个整数数组 numsnums 中查找值为 valval 的变量出现的位置。如果不考虑 break 语句,根据「2.3 时间复杂度计算」中讲的分析步骤,这个算法的时间复杂度是 O(n)O(n),其中 nn 代表数组的长度。

但是如果考虑 break 语句,那么就需要考虑输入的内容了。如果数组中第 11 个元素值就是 valval,那么剩下 n−1n−1 个数据都不要遍历了,那么时间复杂度就是 O(1)O(1),即最佳时间复杂度为 O(1)O(1)。如果数组中不存在值为 valval 的变量,那么就需要把整个数组遍历一遍,时间复杂度就变成了 O(n)O(n),即最差时间复杂度为 O(n)O(n)。

这样下来,时间复杂度就不唯一了。怎么办?

我们都知道,最佳时间复杂度和最坏时间复杂度都是极端条件下的时间复杂度,发生的概率其实很小。为了能更好的表示正常情况下的复杂度,所以我们一般采用平均时间复杂度作为时间复杂度的计算方式。

还是刚才的例子,在数组 numsnums 中查找变量值为 valval 的位置,总共有 n+1n+1 种情况:「在数组的的 0∼n−10∼n−1 个位置上」和「不在数组中」。我们将所有情况下,需要执行的语句次数累加起来,再除以 n+1n+1,就可以得到平均需要执行的语句次数,即:1+2+3+...+n+nn+1=n(n+3)2(n+1)n+11+2+3+...+n+n​=2(n+1)n(n+3)​。将公式简化后,得到的平均时间复杂度就是 O(n)O(n)。

通常只有同一个算法在输入内容不同,不同时间复杂度有量级的差距时,我们才会通过三种时间复杂度表示法来区分。一般情况下,使用其中一种就可以满足需求了。

希望你喜欢这篇文章!请点关注和收藏吧。你的关注和收藏会是我努力更新的动力,祝关注和收藏的帅哥美女们今年都能暴富。如果有更多问题,欢迎随时提问

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值