数据结构与算法

浅析时间、空间复杂度

2018-12-11  本文已影响3人  Mr_zY

为什么学习复杂度分析

大O复杂度表示法

算法的执行效率,简单讲就是算法代码的执行时间。大O表示法可以让我们通过观察算法代码直接估算出其执行时间。
下面是一个1,2,3,,,n的累加函数

# 在Python中range函数遵循左闭右开原则,此处需运算1到n的累加和,故为range(1, n+1)
def cal(n):
    sum = 0
    for i in range(1, n+1):
        sum += i
    return sum

我们假设在CPU中每行代码的执行时间都是一样的,用unit_time表示。
第2行代码分别需要1个unit_time的执行时间;第3、4行分别运行n遍,故需要2*n*unit_time的执行时间;第6行代码需要1个unit_time的执行时间。故,该函数总共需要(2n+2)*unit_time的执行时间。通过该公式可以看出,所有代码的执行时间T(n)与每行代码的执行时间成正比。

按照上述分析思路,在看下面一段代码,它的总执行时间T(n)是多少呢?

def cal(n):
    sum = 0
    for i in range(1, n+1):
        print(sum)
        for j in range(1, n+1):
            sum = sum + i * j
    return sum

第1、7行代码需要1个unit_time的执行时间;第2、3行代码分别执行了n遍,需2n*unit_time的执行时间;第4、5行代码分别执行了n*n遍,需2n2*unit_time的执行时间,所以该函数总执行时间T(n) = 2*unit_time + 2n*unit_time + 2n2*unit_time = 2(n2+n+1)*unit_time。
综合两端代码可知,所有代码的执行时间T(n)与每行代码的执行次数n成正比。故得出以下公式:
T(n) = O(f(n))

T(n): 代码执行时间
n: 数据规模的大小
f(n): 每行代码执行的次数总和。因为是一个公式,所以用f(n)表示。
O: 表示代码的执行时间T(n)与f(n)成正比,即代码执行时间随数据规模增长的变化趋势

因此,第一段代码中T(n) = O(2n+2),第二段代码中T(n) = O(2n2+2n+2)。而公式中的低阶、常量、系数三部分并不会左右增长趋势,可以忽略,只需要记录一个最大量级就行。那么用大O表示法表示上面两段代码的时间复杂度,就可以写作:T(n) = O(n);T(n) = O(n2)

时间复杂度分析

  1. 只关注循环次数最多的一段代码
def cal(n):
    sum = 0
    for i in range(1, n+1):
        sum += i
    return sum

第2行代码是常量级的执行时间,与n的大小无关,即对复杂度没影响;第3、4行代码运行了n遍,所以该段代码的时间复杂度为O(n)。

  1. 加法法则:总复杂度等于量级最大的那段代码的复杂度
def cal(n):
    sum1 = 0
    for p in range(1, 101):
        sum1 += p
    
    sum2 = 0
    for q in range(1, n+1):
        sum2 += q
    
    sum3 = 0
    for i in range(1, n+1):
        print(sum3)
        for j in range(1, n+1):
            sum3 = sum3 + i * j
    
    return sum1+sum2+sum3

该段代码分为sum1、sum2、sum3三部分。sum1部分代码执行了100遍,为常量级的执行时间(一段代码无论执行了1w次、100w次,只要是已知的数,都是常量级的执行时间);sum2、sum3部分代码的时间复杂度分别是O(n),O(n2)。总复杂度等于量级最大的那段代码的复杂度,那么该函数的的复杂度为:O(n2)

  1. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
def cal(n):
    ret = 0
    for i in range(1, n+1):
        ret += f(i)
    return ret

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

cal()函数中,先将第4行的f(i)看做常量级操作,那么其时间复杂度为T1(n) = O(n);但f()也是函数,且它的时间复杂度为T2(n) = O(n),所以,整个cal()函数的时间复杂度是:T(n) = T1(n) * T2(n) = O(n * n) = O(n2)

几种常见时间复杂度实例分析

复杂度量级(按数量级递增)

  • 常量阶 O(1) ---- 多项式量级
  • 对数阶 O(logn) ---- 多项式量级
  • 线性阶 O(n) ---- 多项式量级
  • 线性对数阶 O(nlogn) ---- 多项式量级
  • 平方阶 O(n2)、立方阶 O(n3)、···、k平方阶 O(nk) ---- 多项式量级
  • 指数阶 O(2n) ---- 非多项式量级
  • 阶乘阶 O(n!) ---- 非多项式量级

当数据规模n越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。

  1. O(1)
i = 1
j = 2
sum = i + j

只要代码的执行时间不随n的增大而增长,这样的代码的时间复杂度记作O(1)。一般情况下,只要代码中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度均为O(1)。

  1. O(logn)、O(nlogn)
i = 1
while i <= n:
    i *= 2

变量i从1开始,每循环一次,i乘2,当i大于n时,结束循环。转换成数学公式就是:
2^1*2^2*···*2^i = n
所以,只要知道i的值就可以得出代码的执行次数。根据数学知识:2i = n 得出 i = log2n,因此这段代码的时间复杂度就是O(log2n)。并且,对数之间是可以相互转换的,log3n等于log32 * log2n,所以O(log3n) = O(C * log2n),其中C=log32是常量,在大O标记复杂度时可以忽略,即O(Cf(n)) = O(f(n))。因此,在对数阶时间复杂度的表示方法中统一表示为O(logn)。
理解了O(logn),再结合上面讲的乘法法则。如果一段代码的时间复杂度是O(logn),我们再将这段代码循环n次,那么,时间复杂度就是O(nlogn)了。O(nlogn)也是一种非常常见的时间复杂度,比如:归并排序、快速排序。

空间复杂度分析

类比时间复杂度,空间复杂度是表示算法的存储空间与数据规模之间的增长关系。

void p(int n){
    int i = 0;
    int[] a = new int[n];
    for (i; i < n; ++i){
        a[i] = i * i
    } 
    
    for (i=n-1; i>=0; --i){
        print out a[i]
    }
}

和时间复杂度分析一样,在第2行中,我们申请了一个空间存储变量i,但他是常量阶的,可以忽略;第3行中,申请了一个大小为n的int类型数组;剩下的代码中都没有占用更多的空间,所以整段代码的空间复杂度为O(n)。常见的空间复杂度有O(1)、O(n)、O(n2),像O(logn)、O(nlogn)这样的对数阶空间复杂度平时用不到。:smile:

上一篇下一篇

猜你喜欢

热点阅读