教练,我也想学复杂度分析

教练.jpeg

为什么想学?

  • 如果你是一名业务开发工程师,你可能要说,我整天就是做数据库CRUD(增删改查)又或者你像我一样是个前端仔,整日最常用的命令就是npm run dev,哪里用得到数据结构和算法啊?那还有必要学习复杂度分析吗?
  • 是的,对于大部分开发来说,网上的现有框架已经足够我们平时的开发了,很多现成的框架,封装使用方便,拿来就用,还不用太担心性能的问题。而我们已经很少需要自己实现数据结构和算法。
  • 不需要自己实现,并不代表什么都不需要了解。如果不知道这些类库背后的原理,不懂得时间、空间复杂度分析,你如何能用好、用对它们?调用了某个函数之后,你又该如何评估代码的性能和资源的消耗呢?而数据结构和算法学习的精髓就是复杂度分析

为什么需要复杂度分析?

  • 之前的我通过代码跑一遍,通过统计、监控,就能得到算法执行的时间和占用的内存大小。为什么还要做时间、空间复杂度分析呢?
    1. 测试结果依赖测试环境
      测试环境中硬件的不同会对测试结果有很大的影响。比如,用不同的cpu处理器 i9处理器上的代码显然会比i3处理器上的代码运行的快。
    2. 测试结果受数据规模的影响很大。比如测试数据规模太小,测试结果可能无法真实地反应算法的性能。
  • 我们需要一个不用具体的测试数据来测试,就可以粗略地估计算法的执行效率的方法。这就是时间、空间复杂度分析方法。

大O复杂度表示法

算法的执行效率,其实就是算法代码执行的时间。如何在不运行代码的情况下,来评估一段代码的执行时间呢?

  1. 求1,2,3…n的累加和,那么段这代码的执行时间又是多少呢。

    var Sum = function(n) {
        let result = 0
        for(let i= 0;i<n;i++){
            result = result+i
        }
        return result
    };
    

    可以看到每一行的代码都执行着类似的操作:读取-运算-记录。在这里粗略估计,就可以假设每行代码执行的时间都一样,为n_time

    第2行代码需要1个为n_time的执行时间,第3,4行都运行了n遍,所以需要2nn_time的执行时间,所以这段代码总的执行时间就是(2n+1)n_time

  2. var Sum = function(n) {
            let result = 0
            for(let i= 0;i<n;i++){
                 for(let j= 1;j<n;j++){
                    result = result+i*j
                 }
            }
            return result
        };
    

    我们依旧假设每个语句的执行时间是n_time。那这段代码的总执行时间T(n)是多少呢?
    第2行代码,每行都需要1个n_time的执行时间,第3行代码循环执行了n遍,需要n * n_time的执行时间,第4、5行代码循环执行了n²遍,所以需
    要2n² * n_time的执行时间。所以,整段代码总的执行时间T(n) = (2n²+n+1)*n_time

    通过这两段代码执行时间的推导过程,我们可以得到一个非常重要的规律,那就是,所有代码的执行时间T(n)与每行代码的执行次数n成正比。
    我们可以把这个规律总结成一个公式。

    T(n)=Of(n)
    T(n)表示代码执行的时间;n表示数据规模大小;f(n)表示每行代码执行的次数总和;O表示代码的执行时间T(n)与f(n)表达式成正比。

    第一个例子中的T(n) = O(2n+1),第二个例子中的T(n) = O(2n²+n+1)。这就是大O时间复杂度表示法。大O时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度

    当n很大,甚至趋近于∞时。在公式中的低阶、常量、系数三部分并不能影响他的趋势,所以都可以忽略。我们只需要记录一个最大量级就可以,如果用大O表示法表示刚讲的那两段代码的时间复杂度,就可以记为:T(n) = O(n); T(n) = O(n²)。

时间复杂度分析

  1. 只关注循环执行次数最多的一段代码最多法则

    大O这种复杂度表示方法表示一种变化趋势。所以,我
    们在分析一个算法、一段代码的时间复杂度的时候,也和大O这种复杂度一样只关注循环执行次数最多的那一段代码就可以了。这段核心代码执行次数的n的量级,就是整段要分析代码的时间复杂度。

    还是上文的代码,这次我们来分析他的时间复杂度

    var Sum = function(n) {
        let result = 0
        for(let i= 0;i<n;i++){
            result = result+i
        }
        return result
    };
    

    其中第2行代码是常量级的执行时间,只是声明了一个变量与n的大小无关,所以对于复杂度并没有影响。循环执行次数最多的是第3、4行代码,这两行代码被执行了n次,所以总的时间复杂度就是O(n)。

  2. 总复杂度等于循环次数最多的那段复杂度。加法法则

    var Sum = function(n) {
        let sum1 = 0
        for(let i = 0;i<1000;i++){
            sum1 = sum1+i
        }
        let sum2 = 0
        for(let j= 0;j<n;j++){
            sum2 = sum2+j
        }
        let sum3 = 0
        for(let k= 1;k<n;k++){
            for(let m = 0;m<n;m++){
                sum3 = sum3+k*m
            }
        }
        return sum1+sum2+sum3
    }
    

    这个代码分为三部分,分别是求sum1、sum2、sum3。可以分别分析每一部分的时间复杂度,然后取一个量级最大的作为整段代码的复杂度。

    第一段的时间复杂度是多少呢?这段代码循环执行了1000次,所以是一个常量的执行时间,跟n的规模无关。因此无论这个这段代码循环了多少次只要是一个已知的数与n无关那么当n趋向∞时就可以忽略。因为它本身对算法执行效率与数据规模增长的趋势并没有影响。

    第二段,第三段代码的时间复杂度分别是O(n)和O(n²)综合这三段代码的时间复杂度,取其中最大的值。整段代码的时间复杂度就为O(n²)。

    抽象成公式T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n)))

  3. 遇到嵌套的 for 循环的时,时间复杂度呢就是内外循环的乘积。乘法法则

```
var Sum = function(n) {
    let result = 0
    let func =  function(n) {
        let func_result = 0
        for(let j= 1;j<n;j++){
            func_result = func_result +j
        }
        return func_result
   }
    for(let i = 1;i<n;i++){
        result = result+func(i)
    }
    return result
} 
```
如果func()函数只是一个普通的操作,那第10~12行的时间复杂度就是,T1(n) = O(n)。但func()函数本身不是一个简单的操作,而其本身的时间复杂度是T2(n) =
O(n),所以,整个Sum()函数的时间复杂度就是,T(n) = T1(n) * T2(n) = O(n*n) = O(n²)。

抽象成公式 **T(n)=T1(n)✖️T2(n)=O(f(n)✖ g(n))** 

几种常见时间复杂度分析

对于复杂度量级可以分为以下两类多项式量级和非多项式量级

  • 多项式量级

    1. O(1)
      通常被称为常量阶,他时间复杂度的一种表示方法,并不是指只执行了一行代码。即便有多行,它的时间复杂度也是O(1),一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。
    2. ** O(n)** 线性阶
    3. ** O(n²)...O(nᵏ)** 线性阶
    4. ** O(logn)、O(nlogn)**
      对数阶,线性对数阶时间复杂度非常常见,例如
      var Sum = function(n) {
          let i=1
          while (i <= n) {
              i = i * 2;
          }
          return i
      }
      

    根据我们前面讲的复杂度分析方法,第4行代码是循环执行次数最多的。所以,我们只要算出这行代码被执行了多少次,就能知道整段代码的时间复杂度。
    变量i的值从1开始取,每循环一次就乘以2。当大于n时,循环结束。而i的取值就是一个等比数列

    2⁰ 2¹ 2² 2³ ...... 2ˣ = n
    

    通过2x=n求解x可以算出x=log2ⁿ,所以,这段代码的时间复杂度就是O(log2ⁿ)

    而实际上,不管是以2为底、以3为底,还是以10为底,因为对数之间可以互相转化,例如log3ⁿ = log3² *log2ⁿ 所以O(log3ⁿ) = O(C * log2ⁿ)因为C是个常量我们又可以像在大O复杂度中一样将它忽略掉因此可以把所有对数阶的时间复杂度都记为O(logn)。

    而如果一段代码的时间复杂度是O(logn),我们循环执行n遍,时间复杂度就是O(nlogn)了。

    1. ** O(n)**
  • 非多项式量级

    1. O(2ⁿ)和O(n!)。指数阶,阶层阶

空间复杂度分析

时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。相似的,渐进空间复杂度(asymptotic space complexity)简称就是空间复杂度表示算法的存储空间与数据规模之间的增长关系。

var Sum = function(n) {
    let result = []
    for(let i= 0;i<n;i++){
        result[i] = i 
    }
    return result
};

跟时间复杂度分析一样,我们可以看到,第2行代码中,我们声明了存储变量result,整段代码的空间复杂度就是O(n)。
我们常见的空间复杂度就是O(1)、O(n)、O(n²),

image

最好情况时间复杂度、最坏情况时间复杂度、平均情况时间复杂度

  • 是什么?
    1. 最坏情况时间复杂度:代码在最理想情况下执行的时间复杂度。
    2. 最好情况时间复杂度:代码在最坏情况下执行的时间复杂度。
    3. 平均时间复杂度:用代码在所有情况下执行的次数的加权平均值表示。
  • 为什么要引入这几个概念?
    1. 同一段代码在不同情况下时间复杂度会出现量级差异,为了更全面,更准确的描述代码的时间复杂度。
    2. 代码复杂度在不同情况下出现量级差别时才需要区别这几种复杂度。大多数情况下,是不需要区别分析它们的。

写在最后

image

渐进时间,空间复杂度分析为我们提供了一个很好的理论分析的方向,他能够让我们对我们的程序或算法有一个大致的认识,复杂度分析能让我们对不同的算法有了一个“效率”上的感性认识。

渐进式时间,空间复杂度分析仅仅只是一个理论模型,只能大概的分析,不能直接断定就觉得O(logn)的算法一定优于O(n)

所以在实际开发中,时刻关心理论时间,空间度模型是有助于产出效率高的程序的,同时,而通过文中提供的粗略的分析模型,也不会浪费太多时间,重点在于要具有这种复杂度分析的思维。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容