和我一起学习前端算法 —— 时间复杂度

430 阅读5分钟

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」。

本来想取名为《前端算法入门》的,但是又担心自己水平不够把大家带沟里了,所以此系列文章就改名为和我一起学习前端算法

我们拿到一个问题,吭哧吭哧的写出来了一个算法。此时心里直呼:我太厉害了🐮。但是问题来了,我们如何证明自己写的这个算法足够好呢?隔壁小明用暴力算法也解决了这个问题,你刚刚写的算法到底比他写的好在那里呢?

此时如果你去百度,一般都会告诉你,衡量一个算法优劣一般是看它运行时间的长短和占用内存的大小。因为计算机最重要的资源就是CPU 和 内存,你的算法要是对两者的占用很高,那么就可能会影响到其他程序的运行。

那理论我们现在知道了,需要对比不同算法之间的运算时间长短和占用内存的大小。但是实际上我们怎么来判断呢?

实际运行一下然后统计耗时和占用的内存嘛?

先不说这样会费时费力,即使统计出来了也是没多大意义的。不同的计算机之间,CPU 的性能和内存的大小可能会有差异,实际被计算的数据量级以及大小也大概率不会相同。这样从一台电脑一组数据得出来的结论,换到另外一种情况可能就会相反。

那么有没有一个比较合理的办法来评价算法的优劣呢?

答案是肯定的,我们一般使用 时间渐近复杂度 、 和 空间渐近复杂度 ,两者都是使用 大O符号 来表示。

时间复杂度

为了计算时间复杂度,我们通常会估计算法的操作单元数量,每个单元运行的时间都是相同的。因此,总运行时间和算法的操作单元数量最多相差一个常量系数。

操作单元是什么呢?我们可以简单的理解为 CPU 执行一次指令,但是由于我们的程序的不同操作对应的指令数是不同的,而且我们也不记得具体会运行哪些指令。所以可以再简单的认为是一次操作,包括赋值、判断、加减乘除等。在下面的例子中,我们假设需要处理的数据数量为 n,第一个算法对每一个数据需要先进行乘法再进行加法,所以可以简单认为操作单元数量为 2n,第二个算法每个数据只需要进行一次减法,可以简单认为操作单元数量是 n 。

const newArr1 = arr.map(n => n * 2 + 4);
const newArr2 = arr.map(n => n - 2)

但是这样其实还是会有问题的,很多算法不是简简单单的将每一项循环处理一遍,而是有各种条件语句,这样就会出现相同大小(数据量)的不同输入值可能会造成算法的运行时间不同。

// 很明显,如果 arr 的值都小于 100 会比 值都大于 100 的操作数少很多
const newArr = arr.map(n => {
    if (n < 100) {
        return n;
    } else {
        return n ** (n * 0.6 - 10);
    }
})

因此我们通常使用算法的最坏情况复杂度,记为 T(n) ,定义为任何大小的输入 n 所需的最大运行时间。

那有了 T(n) 是不是就能分析比较算法的运行时间呢?其实还是不好比较的。

例如:算法 A 的 T(n) = 100n,算法 B 的 T(n) = 5n*n,在 n = 5 的时候 是不是 100 * 5 > 5 * 25 。那我们就能说 A 的时间复杂度比 B 高嘛?明显是不太对的。

所以为了进一步方便的比较算法的运行时间,就有了 使用 大O符号 来表示的 时间渐近复杂度

大O符号(Big O notation)是用于描述函数渐近行为的数学符号。更确切地说,它是用另一个(通常更简单的)函数来描述一个函数数量级的渐近上界。在数学中,它一般用来刻画被截断的无穷级数尤其是渐近级数的剩余项;

若存在函数 f(n) ,使得当 n 趋近于无穷大时,T(n)/f(n) 的极限值为不等于零的常熟,则称 f(n) 是 T(n) 的同数量级函数。记作 T(n) = O(f(n)),称为 O(f(n))。

我来翻译翻译:大 O 就是用来反映函数阶的,系数什么的都可以忽略掉,而且只看最大的函数阶。

所以一般推导渐进时间复杂度的步骤就是:1、找到函数阶最高的那一项;2、忽略掉这一项的系数。

举个例子:T(n) = 10n ^ 3 + 1000n ^ 2 + 10000n + 10000,首先只找最高的函数阶也就是 10n ^ 3,然后忽略掉系数,得出 O(n ^ 3)。

同样我们也能得出简单的运算规律:如果两种算法进行组合,一般只能是相加或者相乘,当两者相加时,直接取更高的那种复杂度,如果是相乘则复杂度相乘。

举个例子:

  • O(1) + O(n) = O(n)
  • O(n) * O(log n) = O(n log n)

下面是在分析算法的时候常见的函数分类列表。所有这些函数都处于 n 趋近于无穷大的情况下,增长得慢的函数列在上面。 c 是一个任意常数。

符号名称
O(1)常数(阶,下同)
O(log*n)迭代对数
O(log n)对数
O[(log n)^c]多对数
O(n)线性,次线性
O(n log n)线性对数,或对数线性、拟线性、超线性
O(n^2)平方
O(n^c),Integer(c>1)多项式,有时叫作“代数”(阶)
O(c^n)指数,有时叫作“几何”(阶)
O(n!)阶乘,有时叫做“组合”(阶)

结语

啊啊啊啊啊,怎么就11点半了!!!

这篇文章就先讲到这里了,空间复杂度只能明天再讲了。本文为系列文章,将和大家一起逐步的学习前端算法相关知识。后续的文章都将收录在 专栏 里方便大家查看,大家可以关注我或者收藏本专栏。

下一篇将简单介绍一下关于算法中大家经常听到的空间复杂度等知识。