算法分析 - 基础

算法分析主要研究两个内容:时间复杂度 和 空间复杂度。

时间复杂度分析

一般来说,最关注的是时间复杂度。(或称时间上的增长数量级)

  1. 时间复杂度的渐进表示

    1. 渐近上界:T(N)=O(f(N))表示 T(N) 的阶数小于或等于 f(N) 的阶数,这种记法称为大 O 标记法。(该算法不会比 f(N) 还坏)
      写成极限形式就是:lim_(N->oo)|(T(N))/f(N)|<=M,M为某一正常数

    2. 渐近下界:T(N)=Ω(g(N))表示 T(N) 的阶数大于或等于 g(N) 的阶数。(下界,该算法不可能比 g(N) 更好)

    3. 渐近精确界:T(N)=Θ(h(N))表示 T(N) 与 h(N) 的同阶。
      极限形式:lim_(N->oo)(T(N))/(h(N)) = C, C为某一非0常数

    4. 非渐近紧确上界:T(N)=o(p(N))表示 T(N) 的阶数小于 p(N) 的阶数,称为小 o 标记法,他不同于大 O,因为大 O 包含增长率相同的情况。
      T(N)=O(p(N))T(N)≠Θ(p(N))T(n)=o(p(N)).

    5. T(N) ~ q(N), 表示lim_(N->oo)(T(N))/(q(N))=1,这比 Θ 更精确。(算法 4th 中主要就用这个)

  2. 时间复杂度的理论分析的一般过程

    1. 确定输入模型,定义问题的规模。(自变量M、N)
    2. 识别内循环
    3. 根据内循环中的操作定义成本模型。(访问数组的次数、修改数组内容等)
    4. 对于给定的输入,判断成本:即内循环操作执行的频率。这里可能需要做数学分析。(最常用的是累加操作和级数)
    5. 做倍率实验来验证结果。
  3. 注意事项
    上述一般过程,在某些情况下可能会不适用

    1. 大常数:一般的算法分析都会忽略其中的常数。但是如果常数接近问题的规模N时,这种近似就是错误的。
    2. 内循环是决定性因素的假设并不总是正确的。
    3. 对输入的强烈依赖:上述过程成立的前提之一是——运行时间和输入相对无关。若这个条件无法满足,可能会出现非预期的结果。
    4. 其他

空间损耗分析

对空间损耗的分析相对而言就简单很多,主要原因是它只涉及到会分配内存的语句。
不过话虽如此,对一个自带GC的、使用了各种 内存/性能 优化技术的代码,内存的使用可能会是一个复杂的动态过程,导致分析困难。

画外:各语言对空间损耗的态度

在 C/C++ 里可能会很在意空间,甚至为了节省内存,使出各种奇技淫巧。
但是在 Java 中,内存损耗向其他方面作出了妥协:

  • 删除了无符号整数,规避了由它带来的坑。
  • 数组自带长度信息,索引越界变得很容易检测。
    • 真正的数组(等同于 C 的数组)存放在堆中,然后用一个 Java数组对象包装它,这个对象包含 16 bytes 的对象开销、4 bytes 保存长度信息和 4 bytes 的填充字节。(一般内存的使用会以 8 bytes (对 64 位机) 为基本单位,缺的地方就填充)
  • 自动内存管理GC,解放了程序员,提高了代码健壮性。代价是响应速度降低,(GC 可能会导致卡顿)内存和CPU使用提高。

而 Python3,就更进一步,连内存溢出也避免了,实现了“无限精度”运算。。算是用速度和内存为代价,提升了代码的表达能力、可读性,降低了学习难度,也规避了很多坑。

要再说到函数式语言如 Elixir,它们内部的所有量都是 Immutable,也就是不可变的类型。对它们的任何操作都会产生一个“副本”。(这和 C 语言感觉就像两个极端)在牺牲了内存的同时,在并发编程上得到了其他语言无法比拟的优势。

数据结构方面也有这样的例子,如 Hash Table,在提供更多内存的前提下,实现了几乎为常数的对象访问。

总结一下就是,越往底层的东西,就越在意节省内存。而在上层,内存往往会让步给代码可读性、健壮性、并发性、访问速度等等。
而在大数据时代下,并发性、访问速度、健壮性、灾容设计等变得越来越重要。仅为了灾容,一份数据就往往会在不同的地方有好几个副本。甚至已经出现了完全运行在内存条里的高速数据库。
空间一直在给这些指标不断让步。不过得益于内存价格的不断降低,这个让步大概还能持续很久。

参考

posted @ 2018-05-12 14:32  於清樂  阅读(465)  评论(0编辑  收藏  举报