算法精进之路(一)〖入门〗

614

image.png

时间复杂度

  • 时间复杂度不是代码执行的时间,而是一种随着数据规模不断增长的一种变化趋势。

image.png

  • T(n)=O(f(n)):哪段代码复杂度量级最大,复杂度就是哪一段的!

    • 只关注循环执行次数最多的一段代码

    • 加法法则:总复杂度等于量级最大的那段代码的复杂度

    • 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

  • 复杂度量级:多项式量级和非多项式量级。其中,非多项式量级只有两个:O(2n) 和 O(n!)。

image.png

  • 当数据规模 n 越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法其实是非常低效的算法。

空间复杂度

void print(int n) {
  int i = 0;
  int[] a = new int[n];
  for (i; i <n; ++i) {
    a[i] = i * i;
  }

  for (i = n-1; i >= 0; --i) {
    print out a[i]
  }
}
  • 整段代码中,定义变量的地方是占用空间的地方,数组变量占用的空间是n,后续的操作是对n的内容进行修改,不算占用新的空间,所以复杂度就是n

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

为了表示代码在不同情况下的不同时间复杂度,我们需要引入三个概念:最好情况时间复杂度、最坏情况时间复杂度和平均情况时间复杂度。

顾名思义,最好情况时间复杂度就是,在最理想的情况下,执行这段代码的时间复杂度。就像我们刚刚讲到的,在最理想的情况下,要查找的变量 x 正好是数组的第一个元素,这个时候对应的时间复杂度就是最好情况时间复杂度。

同理,最坏情况时间复杂度就是,在最糟糕的情况下,执行这段代码的时间复杂度。就像刚举的那个例子,如果数组中没有要查找的变量 x,我们需要把整个数组都遍历一遍才行,所以这种最糟糕情况下对应的时间复杂度就是最坏情况时间复杂度。

平均情况时间复杂度

最好情况时间复杂度和最坏情况时间复杂度对应的都是极端情况下的代码复杂度,发生的概率其实并不大。为了更好地表示平均情况下的复杂度,我们需要引入另一个概念:平均情况时间复杂度,后面我简称为平均时间复杂度。

均摊时间复杂度

对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系,这个时候,我们就可以将这一组操作放在一块儿分析,看是否能将较高时间复杂度那次操作的耗时,平摊到其他那些时间复杂度比较低的操作上。而且,在能够应用均摊时间复杂度分析的场合,一般均摊时间复杂度就等于最好情况时间复杂度。

尽管很多数据结构和算法书籍都花了很大力气来区分平均时间复杂度和均摊时间复杂度,但其实我个人认为,均摊时间复杂度就是一种特殊的平均时间复杂度,我们没必要花太多精力去区分它们。你最应该掌握的是它的分析方法,摊还分析。至于分析出来的结果是叫平均还是叫均摊,这只是个说法,并不重要。

个人体会:平均和平摊基本就是一个概念,平摊是特殊的平均。在分析时间复杂度是O(1)还是O(n)的时候最简单就是凭感觉。出现O(1)的次数远大于出现O(n)出现的次数,那么平均平摊时间复杂度就是O(1)。

小结

一、什么是复杂度分析?

  1. 数据结构和算法解决是“如何让计算机更快时间、更省空间的解决问题”。

  2. 因此需从执行时间和占用空间两个维度来评估数据结构和算法的性能。

  3. 分别用时间复杂度和空间复杂度两个概念来描述性能问题,二者统称为复杂度。

  4. 复杂度描述的是算法执行时间(或占用空间)与数据规模的增长关系。

二、为什么要进行复杂度分析?

  1. 和性能测试相比,复杂度分析有不依赖执行环境、成本低、效率高、易操作、指导性强的特点。

  2. 掌握复杂度分析,将能编写出性能更优的代码,有利于降低系统开发和维护成本。

三、如何进行复杂度分析?

  1. 大O表示法

  2. 来源:算法的执行时间与每行代码的执行次数成正比,用T(n) = O(f(n))表示,其中T(n)表示算法执行总时间,f(n)表示每行代码执行总次数,而n往往表示数据的规模。

  3. 特点:以时间复杂度为例,由于时间复杂度描述的是算法执行时间与数据规模的增长变化趋势,所以常量阶、低阶以及系数实际上对这种增长趋势不产决定性影响,所以在做时间复杂度分析时忽略这些项。

  4. 复杂度分析法则

  5. 单段代码看高频:比如循环。

  6. 多段代码取最大:比如一段代码中有单循环和多重循环,那么取多重循环的复杂度。

  7. 嵌套代码求乘积:比如递归、多重循环等。

  8. 多个规模求加法:比如方法有两个参数控制两个循环的次数,那么这时就取二者复杂度相加。

四、常用的复杂度级别?

  • 多项式阶:随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增长。包括:O(1)(常数阶)、O(logn)(对数阶)、O(n)(线性阶)、O(nlogn)(线性对数阶)、O(n2)(平方阶)、O(n3)(立方阶)

  • 非多项式阶:随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法性能极差。包括:O(2^n)(指数阶)、O(n!)(阶乘阶)

五、复杂度分析的4个概念

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

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

  3. 平均时间复杂度:用代码在所有情况下执行的次数的加权平均值表示。

  4. 均摊时间复杂度:在代码执行的所有复杂度情况中绝大部分是低级别的复杂度,个别情况是高级别复杂度且发生具有时序关系时,可以将个别高级别复杂度均摊到低级别复杂度上。基本上均摊结果就等于低级别复杂度。

六、为什么要引入这4个概念?

  1. 同一段代码在不同情况下时间复杂度会出现量级差异,为了更全面,更准确的描述代码的时间复杂度,所以引入这4个概念。

  2. 代码复杂度在不同情况下出现量级差别时才需要区别这四种复杂度。大多数情况下,是不需要区别分析它们的。

七、如何分析平均、均摊时间复杂度?

  1. 平均时间复杂度:代码在不同情况下复杂度出现量级差别,则用代码所有可能情况下执行次数的加权平均值表示。

  2. 均摊时间复杂度:两个条件满足时使用:a) 代码在绝大多数情况下是低级别复杂度,只有极少数情况是高级别复杂度;b) 低级别和高级别复杂度出现具有时序规律。均摊结果一般都等于低级别复杂度。