• 算法的时间复杂度到底怎么算?


    算法的时间复杂度到底怎么算?

    引言

    假设计算机运行一行简单语句算作一次运算。

    def func1(num):
        print("Hello, World!
    ")       # 需要执行 1 次
        return 0                       # 需要执行 1 次
    

    那么上面这个方法需要执行 2 次运算

    def func2(num):
        for i in range(num):            
            print("Hello, World!
    ")    # 需要执行 n 次
        return 0                        # 需要执行 1 次
    

    这个方法需要 (n + 1) = n + 1 次运算。

    为了方便描述, 我们用f(n) 代表某个方法完成处理n个数据时,需要执行运算的次数

    所以上面的例子中,func1的f(n) = 2 func2的f(n) = n + 1
    

    是不是感觉跟以前课本看见的不太一样, 以前课本都是什么O(n), O(n^2), O(1)等等, 别急,继续看

    什么是算法

    想计算算法的时间复杂度,首先要理解,什么是算法。

    算法,是一种抽象出来的逻辑范式,是解决特定问题的方法, 是一个def(python 语言)

    针对同一问题A,存在不同的解决方法(算法),
    智商高的人写出来的优秀算法,在小数据集上,优秀算法解决A只需要嗖嗖嗖,3秒。 
    普通智商的人抽象出来的算法,要吭哧吭哧, 4秒完成。 差距还不是很大。
    
    这时,让我们将数据量增大1W倍,优秀算法可能需要3W秒,普通算法就需要4W秒甚至更多。 这样的差距可就不可以接受了。
    

    那为什么计算时间会长,因为两个算法为了完成等量计算任务,执行运算的次数却不同。 普通算法可能在数值的比较和交换上明显多于优秀算法。 一旦数据量增大,消耗的时间也会显著增大

    在标准配置的CPU下,相差一万秒,计算次数相差可以达到万亿的水平。

    从此,不同算法有了优劣之分。 那如何评价算法的优劣呢,时间复杂度就来了。

    那什么是算法的时间复杂度?

    简单的来说,时间复杂度是用来概括描述算法运行时间和数据量n之间的关系。 记作T(n)

    T(n)可以当做是算法执行时间的衡量,也可以是描述算法优劣和数据量n之间的一个函数。

    更直白的说,时间复杂度T(n)=算完刚才那道题A需要用的时间的大概估计 = 算完刚才那道题A需要用执行的运算次数f(n)

    所以最开始的两个例子,func1的f(n) = 2 = T(n) func2的f(n) = n + 1 = T(n) 是不是更顺眼了一点。
    f(n) 不止代表运算次数,还可以代表算法的运行时间了(在相同配置的CPU下)
    

    那大O记号又是什么, 为什么书里都是这些圆圈O

    首先,大O记号是一种运算符,跟+-*%一样,要有一个这样的预设。

    大O记号完成的是约等于的功能。 想象math.floor() 或者 math.ceil(),都是约等于的功能。
    具体的, O(2 * n^2 + 3) = O(n^2) O(2^n + n^2) = O(2^n) 大概就是这样估计的

    量级高(阶数大)的可以直接覆盖量级低(阶数小)的,同时将倍数置成1

    量级参考:
    O(1) < O(n) < O(n*logn) < O(n^2) < O(n^3) < O(2^n) <O(n!)
    

    知道了大O记号,现在给出T和O之间的关系是 T(n) = O(f(n))

    为了方便描述和简便计算,大家习惯用大O记号近似表示运行时间和数据规模之间关系。

    上面的两个例子,func1的T(n) = f(n) = 2 = O(2) = O(1)
    func2的T(n) = f(n) = n + 1 = O(n + 1) = O(n)
    

    就像,面试官问你这个算法时间复杂度是多少,你会说O(n) 而不是 n + 1, 不然面试官可能会把你踢出去。

    O(1) 表示算法有限次执行,跟数据量无关
    O(n) 表示算法运次时间跟数据量呈一次线性关系。

    那么f(n) 和T(n) 到底有是什么关系

    (这段不想看可以忽略,就记住T(n) = f(n) 就成)

    弄清这两个的关系,又要说到刚才的大O记号了

    但在此之前,首先要明确,f(n) 是描述算法运算次数与数据量之间的一个函数(比如 f(n) = n + 1)

    是因为方便描述,我们才引入了大O记号,因为我们习惯说,“这个算法的时间复杂度是跟数据量呈线性关系的”,而不是说,“这个算法的时间复杂度和数据量呈现2倍加2的关系”。

    接着我们定义:存在常数 c 和函数 f(n),使得当 n >= c 时 T(n) <= f(n),我们称T(n)以f(n)为界或者称T(n)受限于f(n)。 然后给出T(n) = O(f(n))

    以f(n)为界,也就是无论自变量n如何取,T(n)的值永远都不会超过f(n), 我们用一个对大数的估计,来表示T(n)的增长幅度。 所以才有这个式子T(n) = O(f(n))

    那如何计算f(n),算法的执行次数呢?

    1. 对于一个循环,默认循环体的执行次数为 1,循环次数为 n,则这个
      循环的时间复杂度为 O(1xn)。
    def func3(num):
        for i in range(num):            # 执行次数为 num
            print("Hello, World!
    ")    # 循环体执行为 1                     
    

    此时时间复杂度为 O(num × 1),即 O(n)。

    1. 对于多个循环,默认循环体的执行次数为 1,从内向外,各个循环的循环次数分别是a, b, c...,则这个循环的时间复杂度为 O(1×a×b×c...)。
    def func4(num):
        for i in range(num):                # 循环次数为 num
            for j in range(num):            # 循环次数为 num
                print("Hello, World!
    ")    # 循环体执行为 1                    
    

    此时时间复杂度为 O(num × num × 1),即 O(n^2)。

    1. 对于顺序执行的语句或者算法,总的时间复杂度等于其中最大的时间复杂度。
    def func5(num):
        for i in range(num):                # 循环次数为 num
            for j in range(num):            # 循环次数为 num
                print("Hello, World!
    ")    # 循环体执行为 1  第一部分时间复杂度为 O(n^2)
    
        for k in range(num):                # 循环次数为 num
            print("Hello, World!
    ")        # 循环体执行为 1  第二部分时间复杂度为 O(n)    
    

    此时时间复杂度为 max(O(n^2), O(n)),即 O(n^2)。

    1. 对于条件判断语句,总的时间复杂度等于其中 时间复杂度最大的路径 的时间复杂度。
    def func6(num):
        if (num >= 0):
            for i in range(num):                # 循环次数为 num
                for j in range(num):            # 循环次数为 num
                    print("Hello, World!
    ")    # 循环体执行为 1  第一部分时间复杂度为 O(n^2)
        else:
            for k in range(num):                # 循环次数为 num
                print("Hello, World!
    ")        # 循环体执行为 1  第二部分时间复杂度为 O(n)    
    

    此时时间复杂度为 max(O(n^2), O(n)),即 O(n^2)。

    时间复杂度分析的基本策略是:从内向外分析,从最深层开始分析。如果遇到函数调用,要深入函数进行分析。

    最后,我们来练习一下

    • 基础题
      求该方法的时间复杂度
    def  exe1(n):
        for i in range(n):                  # 循环次数为 n
            for j in range(i, n):           # 循环次数为 n - i
                print("Hello, World!
    ")    # 循环体时间复杂度为 O(1)                     
    

    参考答案:
    当 i = 0 时,内循环执行 n 次运算,当 i = 1 时,内循环执行 n - 1 次运算……
    当 i = n - 1 时,内循环执行 1 次运算。
    所以,执行次数 f(n) = n + (n - 1) + (n - 2)……+ 1 = n(n + 1) / 2 = n^2 / 2 + n / 2
    f(n)的最高次是n^2

    所以 T(n) = O(f(n)) = O(n^2 / 2 + n / 2) = O(n^2) 该算法的时间复杂度为 O(n^2)

    • 进阶题
      求该方法的时间复杂度
    def  exe2(n):
        for i in range(2, n):             
            i *= 2                          # 循环体时间复杂度为 O(1)
            print("Hello, World!
    ")        # 循环体时间复杂度为 O(1)                     
    

    参考答案:
    假设循环次数为 t,则循环条件满足 2^t < n。
    可以得出,执行次数t = log(2)(n),即 f(n) = log(2)(n),
    可见时间复杂度为 T(n) = O(log(2)(n)),即 O(log n)。

    • 再次进阶
      求该方法的时间复杂度
    def exe3(n):
        if (n <= 1):
            return 1
        else:
            return exe3(n - 1) + exe3(n - 2);    
    

    参考答案:
    显然运行次数,f(0) = f(1) = 1,同时 f(n) = f(n - 1) + f(n - 2) + 1,这里的 1 是其中的加法算一次执行。
    显然 f(n) = f(n - 1) + f(n - 2) 是一个斐波那契数列,通过归纳证明法可以证明,
    当 n >= 1 时 f(n) < (5/3)^n,同时当 n > 4 时 f(n) >= (3/2)^n。

    所以该算法的时间复杂度可以表示为 O((5/3)^n),简化后为 O(2^n)。

    参考

    1. https://www.jianshu.com/p/f4cca5ce055a
    2. https://www.zhihu.com/question/20196775
  • 相关阅读:
    vj p1034题解
    2010.11.9南高模拟赛
    vj p1041神风堂人数 题解
    noi99钉子和小球 解题报告
    vj p1032题解
    vj p1037题解
    vj p1040题解
    vj p1038题解
    vj p1042捕风捉影 题解
    vj p1046 观光旅游 题解
  • 原文地址:https://www.cnblogs.com/sight-tech/p/13123575.html
Copyright © 2020-2023  润新知