轻松搞定时间复杂度

经过学习本文,你能够掌握如下三点内容。javascript

  1. 为何须要时间复杂度
  2. 时间复杂度怎么表示
  3. 怎样分析一段代码的时间复杂度

相信认真阅读过本文,面对一些常见的算法复杂度分析,必定会游刃有余,轻松搞定。文章中举的例子,也尽可能去贴近常见场景,难度递增。前端

复杂度是用来衡量代码执行效率的指标,直白讲代码的执行效率就是一段代码执行所须要的时间。java

那么,有人会问了,代码执行所须要的时间,我执行一下,看看用了多少时间不就能够了?还要时间复杂度干啥?算法

为何须要时间复杂度

实际开发时,咱们但愿本身的代码是最优的,但总不能把全部的实现的方式都写出来,再跑一遍代码作比较,这样不太实际,因此须要一个指标能够衡量算法的效率。
并且,直接跑代码看执行时间会有一些局限性:数组

1.测试结果受当前运行环境影响

一样的代码在不一样硬件的机器上进行测试,获得的测试结果明显会不一样。有时候一样是a,b两段代码,在x设备上,a执行的速度比b快,但到了y设备上,可能会彻底相反的结果。bash

2.测试结果受测试数据的影响

不一样的测试数据可能会带来不一样的结果,好比咱们采用顺序查找的方法查找数组中的一个元素,若是这个元素恰好在数组的第一位,执行一次代码就能找到,而若是要找的元素位于数组最后,就要遍历完整个数组才能获得结果。函数

因而乎,咱们须要一个不受硬件,宿主环境和数据集影响的指标来衡量算法的执行效率,它就是算法的复杂度分析。性能

时间复杂度怎么表示

咱们知道了为何须要时间复杂度,那要怎么来表示它呢?下面经过一个简单例子,来讲明一下 大O时间复杂度表示法。 首先看第一个例子学习

function factorial(){
    let i = 0 // 执行了1次
    let re = 1 // 执行了1次
    for(;i < n; i++ ){ // 执行了n次
        re*= n // 执行了n次
    }
    return re // 执行了1次
}
复制代码

上面代码是求n的阶乘(n!)的方法,即n×...×3×2×1测试

咱们粗略的计算一下这段代码的执行时间。 首先,为了方便计算,假设执行每行代码的执行时间是相同的。 在这里,假设每行代码执行一次的时间设为t,代码的总执行时间为T(n)。 代码的第2行,第3行执行了一次,须要的时间是t + t = 2t; 代码的第4行,第5行都执行了n次,因此用时(n + n)t = 2n*t; 代码的第7行,只执行了一次,须要时间 t。

因此执行这段代码的总时间是

T(n) = 2t + 2nt + t = (2n + 3)t
复制代码

咱们以n为x轴,T(n)为y轴,绘制出坐标图以下:

很明显,代码的总执行时间和每行代码执行的次数成正比。大O时间复杂度表示法就是用来表示来这样的趋势。 大O表示法表示代码执行时间随数据规模增加的变化趋势 下面是大O表示法的公式: T(n) = O(F(n))

  • n: **表明数据规模, 至关于上面例子中的n
  • F(n):表示代码执行次数的总和,代码执行次数的总和与数据规模有关,因此用F(n)表示, F(n)对应上面例子中的(2n+3)
  • T(n): 表明代码的执行时间,对应上面例子中的T(n)
  • O: 大O用来表示代码执行时间T(n) 与 代码执行次数总和F(n)之间的正比关系。

如今已经知道了大O表示法公式的含义,咱们尝试着把上面例子得出的公式改写成大O表示法,结果以下:

T(n) = O(2n + 3)
复制代码

上面已经说过,大O表示法表示代码执行时间随数据规模增加的变化趋势,只是表示趋势,而不是表明实际的代码执行时间, 当公式中的n无穷大时,系数和常数等对趋势形成的影响就会微乎其微,能够忽略,因此,忽略掉系数和常数,最终上面例子简化成以下的大O表示法:

T(n) = O(n)
复制代码

至此,咱们已经知道了什么是大O表示法以及如何使用大O表示法来表示时间复杂度,下面咱们利用上面的知识,来分析下面代码的时间复杂度。

function reverse(arr){
    let n = arr.length // 执行了1次
    let i = 0 // 执行了1次
    for(;i<n-1;i++){ // 执行了n次
        let j = 0 // 执行了n次
        for(j=0;j<n-1;j++){ // 执行了n²次
            var temp=arr[j] // 执行了n²次
            arr[j]=arr[j+1] // 执行了n²次
            arr[j+1]=temp // 执行了n²次
        }
    }
}
复制代码

这段代码的目的是颠倒数组中元素的顺序(代码只是为了方便咱们讲解用,不须要考虑代码是否最优),按照以前的分析方法分析,依然设每行代码执行时间为t,代码执行的总时间为T(n)。 第2,3行代码只执行了1次,须要时间2t; 第4,5行代码执行了n次,须要时间 2n*t 第6,7,8,9行代码执行了n²次 因此,执行这段代码的总时间

T(n) = (4n² + 2n + 2)t
复制代码

去除低阶,系数和常数,最终使用大O表示法表示为

T(n) = O(n²)
复制代码

经过上面两个例子,咱们能够总结出用大O表示法表示时间复杂度的一些规律:

  1. 不保留系数
  2. 只保留最高阶
  3. 嵌套代码的复杂度内外代码复杂度的乘积

如何快速分析一段代码的时间复杂度

咱们上面总结出了大O表示法的特色,只保留最高阶,因此在分析代码的时间复杂度时,咱们只须要关心代码执行次数最多的代码,其余的均可以忽略。 仍是看咱们上面reverse的例子,执行次数最多的是代码的6,7,8,9行,执行了n²次,咱们就能够很容易计算出它的复杂度为大O(n²),这个方法一样适用于存在判断条件的代码。下面是一段带条件判断语句的代码:

function test(n){
    let res = 0
    if(n > 100){
        const a = 1
        const b = 100
        res = (a + b)*100/2
    }else {
        let i = 1
        for(;i<n; i++){
            res+=i
        }
    }
    return res
}

复制代码

这段代码的含义是,当n > 100时, 直接返回1-100的和,n < 100时,返回1到n的和。 若是按照n的大小分开分析,当n > 100时,代码的执行时间不随 n 的增大而增加,其时间复杂度记为

T(n) = O(1)
复制代码

当n <= 100时,时间复杂度为

T(n) = O(n)
复制代码

上面n > 100的状况,是最理想的状况,这种最理想状况下执行代码的复杂度称为最好状况时间复杂度 n < 100时,是最坏的状况,在这种最糟糕的状况下执行代码的时间复杂度称为最坏状况时间复杂度 后面咱们会有单独的文章来分析最好状况时间复杂度,最坏时间复杂度,平均状况时间复杂度, 均摊时间复杂度。

除了特别说明,咱们所说的时间复杂度都是指的最坏状况时间复杂度,由于只有在最坏的状况下没有问题,咱们对代码的衡量才能有保证。因此咱们这种状况,咱们依然只须要关注执行次数最多的代码,本例子的时间复杂度为O(n)。

为了方便咱们肯定哪段代码在计算时间复杂度中占主导地位,熟悉常见函数的时间复杂度对比状况十分必要。

常见的时间复杂度

最多见的时间复杂度有常数阶O(1),对数阶O(logn),线性阶O(n),线性对数阶O(nlogn),平方阶O(n²) 从下图能够清晰的看出常见时间复杂度的对比:

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2)
复制代码

这些常见的复杂度,其中常数阶O(1),线性阶O(n),平方阶O(n²)对咱们来讲已经不陌生了,在上文的例子中咱们已经认识了他们,只有O(logn)还比较陌生,从图中可见对数阶的时间复杂度仅次于常数阶,能够说是性能很是好了。下面就看一个复杂度是对数阶的例子:

function binarySearch(arr,key){
    const n = arr.length
    let low = 0
    let high = n - 1
    let mid = Math.floor((low + high) / 2)
    while (low <= high) {
        mid = Math.floor((low + high) / 2)
        if (key === arr[mid]) {
            return mid
        } else if (key < arr[mid]) {
            high = mid - 1
        } else {
            low = mid + 1
        }
    }
    return -1
}
复制代码

这是二分查找查找的代码,二分查找是一个比较高效的查找算法。 如今,咱们就分析下二分查找的时间复杂度。 这段代码中执行次数最多的是第7行代码,因此只须要看这段代码的执行次数是多少。上面已经说过,咱们如今考虑的都是最坏状况下的时间复杂度,那么对于这段代码,最坏的状况就是一直排除一半,直到只剩下一个元素时才找到结果,或者要找的数组中不存在要找的元素。
如今已知,每次循环都会排除掉1/2不适合的元素,假设执行第T次后,数组只剩余1个元素。
那么:
第1次执行后,剩余元素个数 n/2
第2次执行后,剩余元素个数 n/2/2 = n/4
第3次执行后,剩余元素个数 n/2/2/2 = n/8
... ...
第n次执行后,剩余元素个数 n/(2^T) = 1

由公式 n/(2^T) = 1 可得 2^T = n, 因此总次数等于 log2n,使用大O表示法,省略底数,就是O(logn)

到这里为止,一段简单的代码时间复杂度就能够分析出来了,更复杂的时间复杂度分析,以及最好状况、最坏状况、平均状况、均摊时间复杂度,将在后面文章中介绍。 更多精彩文章,就在前端小苑。

相关文章
相关标签/搜索