时间复杂度表示代码的执行时间随着数据规模增长的趋势。使用O()表示
【假设】每一行代码执行的时间都一样,都是unit_time。所有代码的总执行时间 T(n) 与每行代码的执行次数成正比。

1、只关注循环执行次数最多的一段代码

def calc(n):
    """
    分析这个代码执行时间,(3+2n)*unit_time,去掉常量,去掉高阶的系数,从而得出时间复杂度就是O(n)
    """
    total = 0  # 执行1次
    for i in range(0, n + 1):  # 执行n+1次
        total = total + i  # 执行n+1次
    return total

再举一例子:

def combination(data: list):
    """
    从一个列表中每次取出两个,找出所有组合方式。
    分析这个代码执行时间,(1+n+2n*n)*unit_time,去掉常量、低阶、高阶的系数,从而得出时间复杂度就是O(n*n)
    思考:前后顺序不同的组合算同一个,比如(1,3)和(3,1)算一个组合,该如何写?
    """
    n = len(data)  # 执行1次
    for x in range(0, n):  # 执行n次
        for y in range(0, n):  # 执行n*n次
            print(data[x], data[y])  # 执行n*n次

3、乘法法则

嵌套代码的复杂度等于嵌套内外代码复杂度的乘积。

def geometric_progression(n: int) -> int:
    """
    打印等比数列
    i从1开始,每次循环乘以2,当大于n时结束循环。i其实是个等比数列,公比是2。循环次数是log2n,时间复杂度就是 O(log2n)。
    不管是以 2 为底、以 3 为底,还是以 10 为底,我们把所有对数阶的时间复杂度都记为 O(logn)
    """
    i = 1
    while i <= n:
        i = i * 2
        print(i)

4、对数阶复杂度

def geometric_progression(n: int) -> int:
    """
    打印等比数列
    i从1开始,每次循环乘以2,当大于n时结束循环。i其实是个等比数列,公比是2。循环次数是log2n,时间复杂度就是 O(log2n)。
    不管是以 2 为底、以 3 为底,还是以 10 为底,我们把所有对数阶的时间复杂度都记为 O(logn)
    """
    i = 1
    while i <= n:
        i = i * 2
        print(i)

5、复杂度由两个数据的规模来决定

def dependent_on_two_scale(m, n):
    """
    从代码中可以看出,m 和 n 是表示两个数据规模。我们无法事先评估 m 和 n 谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。
    代码的复杂度由两个数据的规模来决定m和n。时间复杂度就是 O(m+n)
    """
    sum_1 = 0
    sum_2 = 0
    for i in range(m):
        sum_1 = sum_1 + i
    for i in range(n):
        sum_2 = sum_2 + i

    return sum_1 + sum_2

6、递归算法时间复杂度分析

递归算法是时间复杂度超高的。

def fibonacci(n):
    """
    求fibonacci的第n项。
    递归算法复杂度分析。画递归树,比如fibonacci(6)如图。复杂度为O(2^N)
    """
    if n == 0:
        return 0
    if n in [1, 2]:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

7、优化递归算法降低时间复杂度

def fibonacci_plus(n: int) -> int:
    """
    斐波那契数列也可以从左到右依次求出每一项的值,那么通过顺序计算求得第N项即可。其时间复杂度为O(N)。
    """
    a, b = 0, 1
    if n < 2:
        return n
    for _ in range(2, n+1):
        a, b = b, a + b
    return b

8、最好、最差时间复杂度

代码在不同情况下的不同时间复杂度。最好情况时间复杂度就是,在最理想的情况下,执行这段代码的时间复杂度。最坏情况时间复杂度就是,在最糟糕的情况下,执行这段代码的时间复杂度。

def search(data: list, x: int) -> int:
    """
    从无序数组中查找某个元素的下标。最好时间复杂度是O(1),最差时间复杂度是 O(n)
    要查找的变量 x 可能出现在数组的任意位置。
    如果数组中第一个元素正好是要查找的变量 x,那就不需要继续遍历剩下的 n-1 个数据了,那时间复杂度就是 O(1)。
    但如果数组中不存在变量 x,那我们就需要把整个数组都遍历一遍,时间复杂度就成了 O(n)。
    :param data: 无重复元素无序数组
    :param x: 待寻找的元素
    :return: x的下标
    """
    length = len(data)
    for i in range(length):
        if data[i] == x:
            return i
        else:
            return -1

9、加权平均时间复杂度
上面介绍了最好和最差的时间复杂度,都是比较极端的情况。那么平均的时间复杂度是多少呢?

查找变量x在data中的位置,有n+1中可能,处于0~n-1的下标,和不在data中。我们把每种情况下,查找时需要遍历的元素个数累加起来,然后再除以 n+1,就可以得到需要遍历的元素个数的平均值。(1+2+3…+n+n)/(n+1),省略掉系数、低阶、常量,所以,得到的平均时间复杂度就是 O(n)。

前面的推导过程中存在的最大问题就是,没有将各种情况发生的概率考虑进去。在数组中与不在数组中的概率都为 1/2。另外,要查找的数据出现在 0~n-1 这 n 个位置的概率也是一样的,为 1/n。
如果我们把每种情况发生的概率也考虑进去,那平均时间复杂度的计算过程就变成了这样:

 用大 O 表示法来表示,去掉系数和常量,这段代码的加权平均时间复杂度仍然是 O(n)。

10、空间复杂度分析

就看额外分配了多少内存。

def space_complexity(n: int):
    """
    第 1 行申请了一个大小为 n 的 列表,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是 O(n)。
    """
    space = [0] * n  # 申请了一个大小为 n 列表
    for i in range(n):
        space[i] = i * i
    print(space)

11、总结
复杂度分析的实用方法:

只关注循环执行次数最多的一段代码;
加法法则:总复杂度等于量级最大的那段代码的复杂度;
乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
复杂度由两个数据的规模来决定m和n时,时间复杂度是 O(m+n)
常见的时间复杂度:O(1)<O(logn)<O(n)<O(nlogn)<O(n*n)
空间复杂度分析比较简单,就看额外分配了多少内存就好。

 

02-13 00:38