大 o 法评价算法效率

在大 o 法去评价一个算法的效率时,我们一定要假设 n 时无穷大,因为我们有的时候会发现,不同的数量级,同样两个算法的对比将产生不同的效果,但是凡事总有一个标准,当我们评价算法,使用大 o 法去测试算法时,n 就是无穷大,因为计算机中的数量远超人类的想象,比如有些人认为 1 亿是一个很大的数量级,但是在计算机中这不算什么,所以鉴于人类无法想象在计算机中数量的级别,我们在测试算法时默认 n 就是无穷大。

我们使用 T(n) 表示数量 n 和时间的函数关系,f(n) 表示 n 数量级的数执行的次数总和,使用 T(n) = O(f(n)) 公式,来表示代码的执行时间随着数量级的上升所呈现出的变化趋势。

其中 O 表示代码执行的时间 T(n) 和执行次数 f(n) 表达式成正比,因为在数学的体系中,o 的概念就是表示:如果两个函数 f(n) g(n) 在 n 趋近于无穷大的时候,比值是一个常数,那么它们就可以看成同一个数量级的函数。

所以大 o 表达法并不是真的表示具体的执行时间,而仅仅是表示一种趋势,通常我们使用 O(n) 此类的简写方式来表达算法的执行效率,称之为复杂度,如果前面的是 T(n) 就表示时间复杂度,如果前面的是表示内存的大小的,就表示是空间复杂度,

总结一下,大 o 法表示的是随着数量的提升,消耗资源的渐进式趋势。

上文说到,o 并不是真的时间,而是一种趋势,我们解释一下,看一个例子:

func number(n int) {
  sum :=0
  i:= 0
  j:= 0

  for ;i<n;i++ {
    j = 1
    for ;j<n;j++ {
      sum+= i *j
    }
  }
}

我们就可以推断 T(n) = (2n^2+2n+3)*每次的时间 但是其实我们并不 care 每次的时间是多少,那么假设我们舍弃了每次的时间这个参数,就相当于除以了这个参数,相除就是比值,得出来的数据使用 O 来表示的话,这个算式的结局就是 T(n) = O(2n^2+2n+3) 简称 O(2n^2+2n+3)

当 n 无穷大时,系数,常量,低阶都不能左右增长的趋势,那么我们完全可以省略它们,所以这个算法的最终时间随着数量的增长而存在的增长趋势 --- 时间复杂度就是 T(n) = O(n^2)

寻找复杂度的一些技巧

  1. 关注循环执行最多的一段代码,总复杂度等于量级最大的代码的复杂度
  2. 乘法法则,嵌套代码的复杂度等于里外之积

我们依然使用同样的例子去解释技巧

 sum :=0
  i:= 0
  j:= 0

  for ;i<n;i++ {
    j = 1
    for ;j<n;j++ {
      sum+= i *j
    }
  }

可以看到循环最多的代码就是在 sum += i * j 这里,不是 i := 0 因为它是常数级的代码,也不是 j = 1,因为它的复杂度是 n,我们观察 sum += j *j 就会发现,使用嵌套的代码复杂度等于里外复杂度之乘机可以得出,它执行了 n * n = n^2 次,又根据,总复杂度等于量级最大的代码的复杂度,所以可以得出这段代码的时间复杂度是 O(n^2)

常见的时间复杂度

  • O(1) 常量级
  • O(n) 线性级
  • O(log n) 对数级
  • O(n log n) 线性对数级
  • O(n^2) 平方级
  • O(n^3) 立方级
  • O(n^k) k 次放级
  • O(2^n) 指数级
  • O(n!) 阶乘级

其中后两个是非常低效的算法,谁这么写,就可以滚蛋了,所以不考虑这两种算法时间复杂度了。

常量级:

sum :=0
  i:= 0
  j:= 0

线性级:

for i:= 0;i<n;i++ {
  sum+=i
}

线性级的特殊情况:

当算法里有 m,n,并且它们俩无法获取规模时,必须使用 O(m+n) 的方式表示

for i:= 0;i<n;i++ {

}

for i:=0;i<m;i++ {
}

对数级和线性对数级:

i:= 1
for i < n {
  i *= 2
}

最大值是 n,从 1 开始每次都是乘以 2,直到接近 n 结束,我们可以看每次的执行过程是 1 2 4 8 16 可以看出是以 2 为底数的次方增长的,那么它跟 n 的关系就是:2^i = n,我们只需要知道 i 的次数增长趋势就获得了这次的复杂度,那么 f(i) = log 2 n,根据省略系数的原则,这次的时间复杂度就是 O(n) = logn

线性对数:

i:= 1
for j:= 0;j < n ;j++ {
  for i < n {
  i *=2
  }
}

根据乘积原则,这段代码的时间复杂度是 nlogn

对数级的时间复杂度,是目前来说最佳的算法复杂度,因为常量级的复杂度本身几乎没什么意义,它没有变化的趋势,从趋势来说,一个无穷大的 n,使用对数得到的值在众多算法中是最佳的。

空间复杂度分析

常见的空间复杂度有:

  • O(1)
  • O(n)
  • O(n^2)

O(log n)O(nlog n) 几乎不会用到,其它更多的也几乎遇不到。

时间复杂度的几种概念

  • 最好时间复杂度
  • 最坏时间复杂度
  • 平均时间复杂度
  • 均摊时间复杂度

最好时间复杂度:

  • 最理想的情况下,执行这段代码的时间复杂度,比如数组中查找 x 的时候,第一个就找到了

最坏时间复杂度:

  • 最糟糕的情况下的时间复杂度,比如数组中查找 x 的时候,最后一个才找到

平均时间复杂度:

  • 平常用的那个就是平均时间复杂度,因为大 o 法是要省略常数,系数,低阶的,所以使用概率去算出来的平均时间复杂度省略完成以后还是那个

使用冒泡法举一个例子:

((1 + 2 + ..+n)/n)*n,结果算出来的去掉各种系数还是 O(n^2)

均摊时间复杂度:

  • 举个例子,一个算法有插入操作,n 次操作,第一次 O(n),就跟着 n-1 次的 O(1) 的插入操作,将 n 平均分摊到 n-1 的上面,那么得到的均摊时间复杂度就还是 o(1) 这就是均摊时间复杂度。

这里给出排序算法的平均时间复杂度,最好时间复杂度,最坏时间复杂度。

  1. 冒泡排序 O(n^2) O(n) O(n^2)
  2. 选择排序 O(n^2) O(n^2) O(n^2)
  3. 插入排序 O(n^2) O(n) O(n^2)
  4. 希尔排序 O(nlogn) O(n(logn)^2) O(n(logn)^2)
  5. 归并排序 O(nlogn) O(nlogn) O(nlogn)
  6. 快速排序 O(nlogn) O(nlogn) O(n^2)
  7. 堆排序 O(nlogn) O(nlogn) O(nlogn)
  8. 计数排序 O(n+k) O(n+k) O(n+k)
  9. 桶排序 O(n+k) O(n+k) O(n^2)
  10. 基数排序 O(n*k) O(n*k) O(n*k)

如何寻找最好的算法

我们通过一道例题去讲解一下这个问题。

通过排序算法的案例去讨论复杂度

参考资料

最后更新: 2/27/2024, 6:53:03 AM