时间复杂度

2021-02-26 15:51:11 浏览数 (1)

一、时间复杂度简介

时间复杂度,又称为时间复杂性。用来描述程序运行时间的长短,程序(通常指算法)的执行时间可以反应程序的效率,即程序(算法)的优劣。

程序的时间复杂度是一个与问题规模n相关的数学函数。问题规模指算法中一段代码重复执行的次数,重复执行 n 次,问题规模就是 n 。

要定量(如5分钟,1小时)描述程序的运行时间,是不可能的。因为不同计算机的性能可能有很大的差异,同一段代码在不同计算机中运行的时间是不同的,即使是同一台计算机,因为CPU的性能是动态变化的,运行同一段代码的时间也不可能每次都相同。

为了能描述程序的运行时间,先将程序的每一个执行步骤(如赋值、打印输出、返回值等)定义为一个基本操作,并假设每一个基本操作的执行时间相等,执行一个基本操作需要使用一个时间单位。在这个假设下,一个程序需要执行的基本操作数量越多,需要花的时间单位就越多,也就是说,程序需要执行的步骤越多,时间复杂度就越大。程序的执行步骤可以根据代码计算出来,不依赖于执行程序的计算机。

那么,上面假设程序每个步骤的执行时间单位相等,这个假设是否合理?很显然,对于的不同的计算机环境,确切的时间单位并不同,同一台计算机的时间单位也是波动的。但在同一台计算机中,这种波动是在一个固定的小区间内的,这种小范围的波动不会影响到最终结果的数量级,真正的影响因素是执行程序需要多少个时间单位,即程序的操作步骤。而刚好,在不同的计算机内,同一程序的操作步骤并不会变化。

综上,程序花费的时间与程序中语句的执行步骤数成正比例,哪个程序中语句执行次数多,它花费的时间就多。所以,可以用程序的执行步骤来描述时间复杂度,而程序执行的步骤可以表示成问题规模 n 的一个数学函数 T(n)。

如下面的代码只有1个步骤,问题规模也是1(只执行1次),所以T(n)=1,需要花费1个时间单位,时间复杂度是1。

代码语言:javascript复制
def say_hello():
    print("hello world!")
    
    
say_hello()

下面的代码也只有1个步骤,问题规模是n(需要执行n次),所以T(n)=n,需要花费n个时间单位,时间复杂度是 n 。当n=10000时,就需要花10000个时间单位。

代码语言:javascript复制
def say_sorry(n):
    for _ in range(n):
        print("I'm sorry!")


say_sorry(10000)

二、时间复杂度的计算规则

1. 程序中的基本操作按一个操作步骤计算,如执行一个打印语句、算术运算、逻辑运算、赋值运算、字符串拼接、返回值等,每一个操作步骤的时间复杂度为1。

2. 顺序结构的代码,时间复杂度按加法进行计算,时间复杂度为每行顺序执行的代码的时间复杂度相加。

3. 循环结构的代码,时间复杂度按乘法进行计算,时间复杂度为每一层循环结构的时间复杂度相乘。

如下面的代码,整体是一个循环结构的代码,在最内层循环中,前两行代码都会做一次字符串拼接和一次打印,第三行代码会做一次乘法运算、一次字符串拼接和一次打印,三行代码的时间复杂度依次为2,2,3。这三行代码是顺序结构,所以三行代码的时间复杂度为2 2 3=7。

对于整个循环结构,问题规模是n,内层循环需要执行n次,外层循环也要执行n次,所以整体的时间复杂度为n*n*7,即T(n)=7n^2 。

代码语言:javascript复制
def cycle(n):
    for i in range(1, n):
        for j in range(1, n):
            print("i={}".format(i), end=" ")
            print("j={}".format(j), end=" ")
            print("i*j={}".format(i*j))


cycle(10)

4. 分支结构的代码,时间复杂度取各分支时间复杂度的最大值。

如下面的代码,是if...else...的分支结构,在第一个分支中,需要执行除法运算和打印,操作步骤是2,问题规模是n/2,时间复杂度T(n)=2*(n/2)=n,在第二个分支中,时间复杂度是T(n)=1。整个分支结构的时间复杂度按最大的分支计算,所以整体的时间复杂度为T(n)=n。

代码语言:javascript复制
def branch(n):
    if n % 2 == 0:
        for i in range(0, n, 2):
            print(i/2)
    else:
        print(n)


branch(12)

5. 在没有特殊说明时,程序的时间复杂度都是指最坏时间复杂度。

在上面的分支结构中,计算时间复杂度按最大的分支计算,这就是一种按最坏时间复杂度计算的情况。

计算程序的时间复杂度时,存在如下三种方式:

最坏时间复杂度,指程序完成工作(运行完成)最多需要多少次基本操作。

最优时间复杂度,指程序完成工作(运行完成)最少需要多少次基本操作。

平均时间复杂度,指程序完成工作(运行完成)平均需要多少次基本操作。

如下面的代码中,是从一个1到n的数字列表中查找m。如果传入的m是数字1,for循环只需要执行1次,时间复杂度是1(最优时间复杂度),如果传入的m与n相等,for循环需要执行n次,时间复杂度是n(最坏时间复杂度)。计算这段程序的时间复杂度时,按最坏时间复杂度计算,所以,T(n)=n。

代码语言:javascript复制
def search(n, m):
    data_list = range(1, n)
    for i in data_list:
        if i == m:
            return i
    else:
        return -1


print(search(10, 1))

最优时间复杂度反映的只是最乐观最理想的情况,没有提供真实有效的信息,所以没有参考价值。平均时间复杂度是对程序的一个全面评价,可以完整全面地反映程序的性质。但是,这种衡量并没有保证,不是每次运行都能在这个时间内完成。而且,平均时间复杂度也会因为程序运行时间的不均匀分布(除非一次函数)而难以计算。

最坏时间复杂度提供了一种保证,表明程序在此时间内一定能完成工作。因此,一般都是计算最坏时间复杂度。

三、时间复杂度的大O记法

时间复杂度常用大O记法来表示。时间复杂度可以表示成一个问题规模n的数学函数T(n),大O记法是用一个与该数学函数渐近的简化数学函数来表示时间复杂度。

一般情况下,程序的时间复杂度是问题规模n的某个数学函数,用T(n)表示,若有(一定有)某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,函数T(n)的增长速度主要受函数f(n)的约束,则称f(n)是T(n)的渐近函数。记作T(n)=O(f(n)),称O(f(n))为程序的渐近时间复杂度,简称时间复杂度。

大O记法只关注时间复杂度数学函数的最高次项,忽略了其它低次项和常数项,同时忽略了最高次项的系数。

对程序进行特别具体的细致分析虽然很好,但在实践中的实际价值有限。对于程序的时间复杂度,最重要的是其数量级和趋势,这是分析程序效率的主要部分,而计量程序基本操作数量的规模函数中的那些常量因子可以忽略不计。

根据大O记法,若程序执行次数为一个常数,则时间复杂度为一个O(1)。若程序执行次数为问题规模n的一次函数,如T(n)=3n 20和T(n)=5n 8,则时间复杂度都为O(n)。若程序执行次数为问题规模n的二次函数,如T(n)=5n^2 8n 10和T(n)=8n^2 10n 10,则时间复杂度都为O(n^2)。以此类推。

常见的时间复杂度比较如下:

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)

0 人点赞