复杂度分析

前言

对于数据结构相关的博客文章,我是根据大学本科阶段《数据结构和算法》课程的内容和王争老师在即刻时间上的《数据结构和算法之美》系列课程(墙裂推荐)进行了一些排版参考和笔记性梳理。这些文章作为笔记总结,一方便是为了夯实自己的基础,一方面为同事和朋友提供一些日后相关内容解释的参考和公开的分享链接。同时数据结构相关内容也必须要具备某一种编程语言的基础知识且内容较为庞杂枯燥。如果是未曾接触过编程语言的读者,建议请先选择一门编程语言,并了解其基础语法,这样才能在学习数据结构与算法的过程中坚持下去。这里我的笔记使用的编程语言为 Python3 ,且大部分图片为手绘。

数据结构和算法解决计算机的什么问题

数据结构和算法自身解决的是“快”和“省”的问题,即如何让编写的程序能够更快得在海量且无序的数据里面找到答案;同时如何让程序在运行的过程中更好的节省计算机存储空间(主要指计算机内存),以防止干扰物理机(服务器或者个人电脑等)其它软件程序的运行。这里我们假设计算机CPU是一个疯狂的剁手党,我们把要处理的数据想象成物品放入包裹(从内存写入硬盘)里或者收到后开始拆开包裹,取出物品(从硬盘读入内存)。计算机架构里,通常我们只有把数据装载到内存,我们才能一窥数据的全貌,并处理它,而硬盘仅仅是堆放整理好了的数据包裹的地方,这就是内存和硬盘的区别。我们获取将面对两种方式:

1)将很多东西一起集中放在一个包裹里面;

2)将每一个物品打上标签放在自己独属的包裹里。

那么在这里我们就会在装包裹和拆包裹面对不同的问题和便利。

1)装包裹的方式(内存写入硬盘)无疑是第二个时间长,这就解释了为什么在计算机中,拥有许多零散文件的压缩包解压过程总是比差不多大小的几个大文件组成的压缩包慢;

2)如果剁手党收到包裹后,扛着大包裹上楼(相当于将海量数据一次性装载到内存)无疑是很吃力的。这时候如果只是想拿到自己最中意的手机包裹,肯定是打了标签的小包裹更方便(相当于海量数据被分割成很多小块,并已经打上标签存储到硬盘上,就能快速找到手机标签的数据导入内存)。

总而言之,数据结构和算法解决的是面对海量且杂乱数据的时候,如何更省和更快整理和查找数据的问题。

什么是复杂度分析

复杂度分析要求首先我们要有一个具体问题,比如剁手党如何在购买的一堆东西中找到心爱手机。从时间角度来讲,剁手党肯定是针对性的找单号为手机的包裹最快,而不是把零食、牛奶和手机等等一起混乱的放在一个包裹里翻找;从空间角度讲,剁手党扛着几十斤的包裹上楼和拿着几百克的手机上楼孰轻孰重也高下立判。这就催生了时间复杂度和空间复杂度分析。

为什么要时间和空间复杂度分析

大部分原因已经由前文讲述,但还有一个原因。我们这里所说的时间复杂度和空间复杂度分析是一种事前统计法。我们不可能跟测评一样,把找包裹和拆包裹的不同过程都掐个秒表实践一下,并算一下时间(事后统计法),再去抉择。

大 O 分析法

时间复杂度分析

数据结构和算法基本上侧重于时间复杂度分析,即代码执行时间的分析。由于代码运行时间的复杂度相比于代码运行占用内存复杂度的情况要多得多,所以传统意义上的数据结构与算法都是对运行时间的分析,也可理解为预估代码的执行效率。如下代码所示,我们可以分析一下sum_demo函数的执行时间,假设每一行代码为 1 ms ,代码的意思为默认从 1 至 10000 的累加,运行求和结果为:50005000 。每一行的执行时间为 1 + 2 ∗ 10000 + 1 1+2*10000+1 1+2∗10000+1 ms ,由于 capacity (默认10000)是个变量,可以当成变量 n ,我们可以得出代码总运行时间为: T ( n ) = 2 + 2 ∗ n T(n)=2+2*n T(n)=2+2∗n

class Complexity:
    def __init__(self, capacity: int = 10000):
        self._capacity = capacity

    def sum_demo(self) -> int:
        summary = 0
        for index in range(1, self._capacity + 1, 1):
            summary += index
        return summary


if __name__ == "__main__":
    print("sum demo result: %d" % Complexity().sum_demo())

这里我们会发现详细的计算公式并不利于我们快速估算,同时我们也不需要具体的执行时间,只需要一个能直观表现运行时间变化趋势的公式。根据数学中对变化趋势的定义,我们可以将常数、系数和公式中所有低阶项全部舍弃,便产生了大 O 表示法,即 T ( n ) = O ( 2 + 2 ∗ n ) = O ( n ) T(n)=O(2+2*n)=O(n) T(n)=O(2+2∗n)=O(n)。这种分析法在时间领域,我们也可以叫做渐进时间复杂度。

    def sum_demo_2(self) -> int:
        summary = 0
        for index1 in range(1, self._capacity + 1, 1):
            summary += index1

        for index2 in range(1, self._capacity + 1, 1):
            for index3 in range(1, self._capacity + 1, 1):
                summary = summary + index2 + index3
        return summary

上面的代码进行复杂度分析为 T ( n ) = O ( 1 + 2 ∗ n + n ∗ 2 n + 1 ) = O ( 2 n 2 + 2 n + 2 ) T(n)=O(1+2*n+n*2n+1)=O(2n^2+2n+2) T(n)=O(1+2∗n+n∗2n+1)=O(2n2+2n+2),根据刚刚说的取最高阶项,并且舍弃常数和系数规律,即 T ( n ) = O ( n 2 ) T(n)=O(n^2) T(n)=O(n2)。

常见的时间复杂度

O ( 1 ) O(1) O(1)

这个代表前文代码 summary = 0最简单的一行代码的时间估算,即运行时间为 1 ms

O ( n ) O(n) O(n) 和 O ( n 2 ) O(n^2) O(n2)

这个之前已经用代码展示过了,不在赘述。

O ( l o g n ) 和 O ( n l o g n ) O(logn) 和 O(nlogn) O(logn)和O(nlogn)
    def log_n(self) -> int:
        index = 1
        while index <= self._capacity:
            index = 3 * index
        return index
      
    def n_log_n(self) -> int:
        summary = 0
        for _ in range(1, self._capacity + 1, 1):
            index = 1
            while index <= self._capacity:
                index = 3 * index
            summary += index
        return summary

我们先观察log_n函数,这里while循环的判断条件相当于 3 x < = 10000 3^x<=10000 3x<=10000,而 x 我们就可以看成循环的次数,所以我们就可以求出 x = l o g 3 10000 x=log_{3}^{10000} x=log310000​,这里数据结构规定,对数函数不论下标是什么,都舍弃。我们便可得出: T ( n ) = O ( l o g n ) T(n)=O(logn) T(n)=O(logn) ;那么我们不难看出函数n_log_n的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn) 。我们从代码运行结果也可以发现两个函数执行时间(index变量返回结果估算)结果大约相差一万倍。

时间复杂度加法法则和乘法法则

上面的代码我们都使用了self._capacity一个变量作为参数,所以我们可以直接取高阶项为变化趋势,其它低阶项直接舍弃。但是我们面对多个不同变量的时候,我们不能错误的把不同变量变化趋势简单取为最高阶项,这就出现了加法法则和乘法法则:

    def m_add_n(self, value1: int, value2: int) -> int:
        summary = 0
        for index in range(1, value1 + 1, 1):
            summary += index

        for index in range(1, value2 + 1, 1):
            summary += index
        return summary

    def m_times_n(self, value1: int, value2: int) -> int:
        summary = 0
        for _ in range(1, value1 + 1, 1):
            for index in range(1, value2 + 1, 1):
                summary += index
        return summary

由于value1value2是两个不同的变量,那么可以看出两者的时间复杂度分别写为 O ( m + n ) O(m+n) O(m+n) 和 O ( m n ) O(mn) O(mn)

空间复杂度分析

空间复杂度比较简单,我们的变量 self._capacity 每调用一次会申明 10000 * 单个 int 型所占的字节,同时它同样适用取最高阶项,舍弃常数和系数的规律,所以如下代码的空间复杂度分别为 O ( n ) O(n) O(n) 和 O ( n 2 ) O(n^2) O(n2) 。

    def sum_demo_1(self) -> int:
        summary = 0
        for index in range(1, self._capacity + 1, 1):
            summary += index
        return summary

    def sum_demo_2(self) -> int:
        summary = 0
        for index1 in range(1, self._capacity + 1, 1):
            summary += index1

        for index2 in range(1, self._capacity + 1, 1):
            for index3 in range(1, self._capacity + 1, 1):
                summary = summary + index2 + index3
        return summary

复杂度曲线

通常我们认为:基于海量的数据,代码效率为 O ( l o g n ) < O ( n ) < O ( n l o g n ) < O ( n 2 ) O(logn)<O(n)<O(nlogn)<O(n^{2}) O(logn)<O(n)<O(nlogn)<O(n2) , 如图所示:

复杂度分析

上一篇:dremio 内置support key 说明


下一篇:Nunit Test Project(C#) + selenium环境搭建