数据结构和算法第 2 章:复杂度分析

一、复杂度分析

首先要明确一点,数据结构和算法本质是解决“快”和“省”的问题。要描述一个算法的好坏就需要用到复杂度分析了,复杂度分析可分为如下两种。

  • 时间复杂度

  • 空间复杂度

时间复杂度就是描述算法的快,空间复杂度则是描述算法的省。一般说的复杂度都是时间复杂度,毕竟现代计算机存储空间已经不那么拮据了,时间复杂度是我们重点研究的内容。

二、大 O 复杂度表示法

首先看一段代码,求从 1~n 的累加之和。

int demo(int n) {

    int i;
    int sum = 0;

    for(i=1; i<n; i++) {
        sum += i;
    }

    return sum;
}

现在就来估算一下这段代码的执行时间(下面都是以时间复杂度为例讲解,空间复杂度最后再讲)。

CPU 的角度来看,每一行代码都执行着类似的操作读数据-运算-写数据。这里为了方便计算,假设每行代码的执行时间都是一样的,用 t 表示执行一行代码所需要的时间,n 表示数据规模的大小,T(n) 表示代码执行的总时间。

那么这段代码总执行时间是多少呢?我们来数一下。

首先,函数体内有 5 条语句,第 1、2、5 条语句总共执行了 3 次,所需时间是 3*t;第 3、4 条语句各自执行了 n 次,所需时间是 2*n*t。把这两个代码段执行的时间相加,所得到的结果就是这段代码总共所需的时间。

T ( n ) = ( 2 n + 3 ) t T(n)=(2n+3)t T(n)=(2n+3)t

通过上述公式可以得到一个规律,T(n) 随着 n 变大而变大,变小而变小。所以,T(n)n 是成正比的,用数学符号表示就可以写成。

T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))

其中 f(n) 是代码段执行所需的时间之和,O 表示 T(n)f(n) 之间的关系是成正比的。

由公式可得代码段执行所需的时间可表示为 T ( n ) = O ( 2 n + 3 ) T(n)=O(2n+3) T(n)=O(2n+3)。这就是O 时间复杂度表示法。大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随着数据规模增长的变化趋势,所以,也叫做渐进时间复杂度简称时间复杂度

其实 O ( 2 n + 3 ) O(2n+3) O(2n+3)并不是最终时间复杂度的表示方式。在实际的复杂度分析中,一般会把公式中的常量系数低阶忽略。因为这三部分并不影响增长趋势(还记得时间复杂度其实是渐进时间复杂度吧!),所以只需要记录一个最大量级就可以了,时间复杂度的最终表示方式就是 O ( n ) O(n) O(n)

三、复杂度的分析方法

1. 最大量阶

int demo(int n) {

    int i;
    int sum = 0;

    for(i=1; i<n; i++) {
        sum += i;
    }

    return sum;
}

在分析一个算法、一段代码的时间复杂度的时候,只关注循环执行次数最多的那一段代码即可。

2. 加法法则

int demo(int n) {

    int i;
    int sum = 0;

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

    return sum;
}

如果代码中存在着不同量级的时间复杂度,总的时间复杂度就等于量级最大的那段代码的时间复杂度。

3. 乘法法则

int demo(int n) {

    int i;
    int sum = 0;

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

    return sum;
}

如果是嵌套、函数调用、递归等操作,只需要将各部分相乘即可。

四、复杂度的量级

  • 常量阶: O ( 1 ) O(1) O(1)

  • 对数阶: O ( log ⁡ n ) O(\log n) O(logn)

  • 线性阶: O ( n ) O(n) O(n)

  • 线性对数阶: O ( n log ⁡ n ) O(n \log n) O(nlogn)

  • 平方阶: O ( n 2 ) O(n^2) O(n2)

  • 立方阶: O ( n 3 ) O(n^3) O(n3)

  • k次方阶: O ( n k ) O(n^k) O(nk)

  • 指数阶: O ( 2 n ) O(2^n) O(2n)

  • 阶乘阶: O ( n ! ) O(n!) O(n!)

对于上述不同的量级可以分为两类:多项式量级非多项式量级。其中,非多项式量级只有两个: O ( 2 n ) O(2^n) O(2n) O ( n ! ) O(n!) O(n!),非多项式也叫做 NP问题。

一般情况下,我们常见的复杂度只有 O ( 1 ) O(1) O(1) O ( log ⁡ n ) O(\log n) O(logn) O ( n ) O(n) O(n) O ( n log ⁡ n ) O(n \log n) O(nlogn) O ( n 2 ) O(n^2) O(n2) 这五个,常用的分析方法有最大量阶、加法法则、乘法法则这三个。只要把这些掌握,基本上就没有太大问题了。

五、时间复杂度

我们已经分析了时间复杂度,但是还是有一点儿小问题,比如我们要查找某个元素在长度为 n 的数组中的下标。如果按照顺序遍历,最理想的情况是第一个就是我们要找的,所以时间复杂度是 O(1);如果最后一个才找到我们要的数据,那么它的时间复杂度是 O(n)

为了解决同一段代码在不同情况下时间复杂度出现量级差异,我们就需要对时间复杂度进一步细化分类,为了更准确、更全面的描述代码的时间复杂度,引入了一下 4 个概念。

1. 最好情况时间复杂度

代码在最理想情况下执行的时间复杂度。

2. 最坏情况时间复杂度

代码在最坏情况下执行的时间复杂度。

3. 平均情况时间复杂度

上面两个最好、最坏情况都是小概率事件,平均情况时间复杂度才是最能代表一个算法的时间复杂度。因为平均情况时间复杂度需要引入概率进行分析,所以也叫做加权平均时间复杂度

4. 均摊时间复杂度

正常情况下,代码在执行过程中都处于低阶的复杂度,极个别情况会出现高阶的复杂度,这是我们就可以将高阶的复杂度均摊到每个低阶的复杂度上,这种分析使用的是摊还分析法的思想。

其实我们只需要知道时间复杂度就够了。这四种方法都是对时间复杂度的一些特殊情况的补充,也没必要花大力气去研究它,大概知道有这种时间复杂度分类就可以了,如果你自己想学或者有脑残面试官要问这些,那你就自己去查找资料研究研究,这里不会展开讲解。

六、空间复杂度

前面讲解过,时间复杂度是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。那么空间复杂度就是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。

看一段代码,定义一个新数组,赋值后遍历输出。

void demo(int n) {

    int i;
    int data[n];

    for(i=0; i<n; i++) {
        data[i] = i * i;
    }

    for(i=0; i<n; i++) {
        printf("%d\n",data[i]);
    }
}

跟时间复杂度分析一样,函数体内第 1 条语句是常量阶,直接忽略;第 2 条语句申请了一个大小为 nint 类型数组,所以整段代码的空间复杂度就是 O ( n ) O(n) O(n)

相关文章

背景:计算机内部用补码表示二进制数。符号位1表示负数,0表...
大家好,我们现在来讲解关于加密方面的知识,说到加密我认为不...
相信大家在大学的《算法与数据结构》里面都学过快速排序(Qui...
加密在编程中的应用的是非常广泛的,尤其是在各种网络协议之...
前言我的目标是写一个非常详细的关于diff的干货,所以本文有...
对称加密算法 所有的对称加密都有一个共同的特点:加密和...