BlackFlame33

BlackFlame33

若无力驾驭,自由便是负担。个人博客 https://blackflame33.cn/

算法學習筆記-01

复杂度分析浅析

一、為什麼需要複雜度分析#

可能平常,都会通过统计、监控,来得到相关的性能数据,来评估性能。有人将这种平时使用的性能分析方式,称为:事后分析法

但是事后分析法,虽然普遍存在,但其有一些缺陷:

  1. 测试结果非常依赖测试环境。
    • 某种意义上这也很方便,但是面对不同的机器、不同的编译环境,同段代码可能会有不同的结果。
  2. 测试结果受测试规模的影响很大。
    • 同段代码,在大规模和小规模下会有不一样的结果。

所以,能不能有一种方法,能让我们无视测试环境,在任意规模下都能较精确地分析出代码的性能呢?

自然是有的,那就是使用大 O 复杂度表示法运用时间复杂度空间复杂度来分析代码

二、大 O 复杂度表示法#

大 O 表示法:算法的时间复杂度通常用大 $O$ 符号表述,定义为 $T (n) = O\big (f (n)\big)$。称函数 $T (n)$ 以 $f (n)$ 为界或者称 $T (n)$ 受限于 $f (n)$。 如果一个问题的规模是 n,解这一问题的某一算法所需要的时间为 $T (n)$。$T (n)$ 称为这一算法的 “时间复杂度”。当输入量 n 逐渐加大时,时间复杂度的极限情形称为算法的 “渐近时间复杂度”。—— 百度百科

时间复杂度#

时间复杂度:全称 “渐进时间复杂度”,表示算法的执行时间与数据规模之间的增长关系。

我们这里用一个公式来表示:

T(n)=O(f(n))T(n) = O\big(f(n)\big)
$T(n)$$n$$f(n)$$O$
代码的执行时间代码的规模每行代码的执行次数$T (n)$ 与 $f (n)$ 成正比关系

为了更方便理解这个公式,我举一个例子:

#include <stdio.h>
int main(){
    int c = 0;
    for(int i=1; i<=n; ++i)
        c += i;

    return 0;
}

从 CPU 的角度来看,这段代码的每一行都执行着类似的操作:读数据 - 运算 - 写数据。尽管每行代码对应的 CPU 执行的个数、执行的时间都不一样,但是,我们这里只是粗略估计,所以可以假设每行代码执行的时间都一样,为 unit_time。在这个假设的基础之上,这段代码的总执行时间是多少呢?

第 3 行代码需要 1 个 unit_time 的执行时间,第 4、5 行都运行了 n 遍,所以需要 2n * unit_time 的执行时间,所以这段代码总的执行时间就是 (2n+1) * unit_time。可以看出来,所有代码的执行时间 T (n) 与每行代码的执行次数成正比

时间复杂度的分析方法#

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

这里有一条目前我觉得很重要的推断:由于低阶、系数和常量均不影响规模大小,所以 $f (n)$ 取最高阶代表即可。

2、加法法则#

总复杂度等于量级最大的那段代码的复杂度

我们举一个例子:

#include <stdio.h>
int main(){
    int x=0;
    for(int i=1; i<=100; ++i)
        x += i;

    int y=0;
    for(int i=1; i<=n; ++i)
        x += i;

    int z=0;
    for(int i=1; i<=n; ++i){
        for(int j=1; j<=n; ++j)
            z += j;
    }

    printf("%d %d %d\n", x, y, z);
    return 0;
}

在这个例子中,要求 x,y,z。我们分别求出每个部分的时间复杂度,然后取最高阶的作整段代码的复杂度。

求 x 的这段,它会循环 100 次。由于是常数,所以不作数。

求 y 的这段,它会循环 n 次。记 $O (n)$。

求 z 的这段,它会循环 $n^2$ 次。记 $O (n^2)$。

我们从中取最高阶 $O (n^2)$ 作为整段代码的复杂度。

所以将加法公式化作表达式就为:

T1(n)=O(f(n)),T2(n)=O(g(n))T(n)=T1(n)+T2(n)=max(O(f(n)))O(g(n))=O(max(f(n),g(n)))\because T_1(n)=O\big(f(n)\big), T_2(n)=O\big(g(n)\big)\\\\ \therefore T_总(n)=T_1(n)+T_2(n)=max\Big(O\big(f(n)\big)\Big)\\\\ \\\\ O\big(g(n)\big)=O\Big(max\big(f(n),g(n)\big)\Big)
3、乘法法则#

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

我们举个例子:

#include <stdio.h>
int f(int n);
int main(){
    int x = 0;
    for(int i=0;i<=n;++i)
        x += f(i);

    return 0;
}

int f(int n){
    int y = 0;
    for(int i=0;i<=n;++i)
        y += i;

    return y;
}

在这个例子中,如果我们把 $f ()$ 看作一个普通操作,那么第 6 行的时间复杂度就是 $T_1 (n)=O (n)$。但是 $f ()$ 是一个函数,它的时间复杂度为 $T_2 (n)=O (n)$,所以,这段代码的时间复杂度应该是,$T_总 = T_1 (n) * T_2 (n)=O (n*n)=O (n^2)$。

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

复杂度量级

其中,我们可以将其分为两类:多项式量级非多项式量级。非多项式量级只有两个:$O (2^n)$ 和 $O (n!)$。

我们把时间复杂度为非多项式量级的算法问题叫做 NP 问题。当数据规模 n 越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法其实是非常低效的算法。

1、$O (1)$—— 常量级时间复杂度#
  • 这个‘1’只是一种表示方法。

只要代码的执行时间不随 n 的增大而增长,或者,一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是 $O (1)$

2、$O (\log n)$、$O (n\log n)$—— 对数阶时间复杂度#
  • 可以把所有对数阶的时间复杂度都记作 $O (\log n)$,$O (n\log n)$ 同理。

根据换底公式, 对数之间是可以互相转换的,所以说,任意对数都可以转换成 $Cf (n)$,其中 $f (n)$ 为对数。而根据之间得到的结论,常数等不影响代码规模的可以忽略不计。所以在对数阶时间复杂度的表示方法里,我们忽略对数的 “底”,统一表示为 $O (\log n)$

3、$O (m+n)$、$O (m*n)$—— 两个数据规模的时间复杂度#

一般的,由两个数据的规模来决定的代码的复杂度,由于无法事先评估谁的量级大,所以我们在表示复杂度的时候,不能简单地利用加法法则,省略掉其中一个。所以,我们用 $O (m+n)$、$O (m*n)$ 来表示。

那么,在这种情况下,加法法则应该为:

T1(m)=O(f(m)),T2(n)=O(g(n))T(n)=T1(m)+T2(n)=O(f(n)+g(n))\because T_1(m)=O\big(f(m)\big), T_2(n)=O(g(n))\\\\ \therefore T_总(n)=T_1(m)+T_2(n)=O\big(f(n)+g(n)\big)

而乘法法则不变。

空间复杂度分析#

空间复杂度,全称渐进空间复杂度表示算法的存储空间与数据规模之间的增长关系

我们用一个例子来看一看。

#include <stdio.h>
int main(){
    int a[n];
    for(int i=0; i<=n; ++i)
        a[i] = i * i;
    for(int i=n-1; i>=0; --i)
        pritntf("%d", a[i]);

    return 0;
}

那么,分析这段代码,第三行申请了一个大小为 n 的 int 类型数组,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是 $O (n)$。

三、內容小結#

複雜度也叫漸進複雜度,包括時間複雜度空間複雜度,用來分析算法執行效率與數據規模之間的增長關係,可以粗略地表示,越高階複雜度的算法,執行效率越低。常見的複雜度並不多,從低階到高階有 $O (1)$、$O (\log n)$、$O (n)$、$O (n\log n)$、$O (n^2)$。

各常見複雜度中 T (n) 與 n 的正比關係

參考文章#

MathJax 基本的使用方式

數據結構與算法之美 —— 王爭

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。