【算法学习】1.时间复杂度、logN算法的底数

171 阅读4分钟

时间复杂度

一个数据规模为n的程序,执行多少次能得到结果。这两者之间有一种函数映射关系:O(f(n))。分析程序时间复杂度的核心就是得到这个函数表达式。

不过时间复杂度并没有像数学中函数这么较真,当n很大的时候,表达式中的低阶项和常数项就会省略,就比如表达式:n^3 + n^2 + 1000,他的时间复杂度就是O(n^3), 因为当n很大的时候,n^2与1000比起n^3就如九牛一毛,所以可以省略。

概念没有实际的支撑就会显得十分空洞,通过分析几个简单排序定能有所感觉。

排序的时间复杂度分析

function bubbleSort (arr) {
  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length - i - 1; j++) {
      if (arr[j] > arr[j + 1]) swap(arr, j, j + 1)
    }
  }
}

对于冒泡排序,我们分析程序执行次数与n的关系:

  • 第一次循环执行 n - 1
  • 第二次循环执行 n - 2
  • 第三次循环执行 n - 3
  • ....
  • 1

所以执行次数为:(n-1) + (n-2) + (n-3) + ... + 2 + 1 = 2/n^2(等差数列求和), 抛去常数与低阶项,得到冒泡排序的时间复杂度为 O(n^2)

递归中的时间复杂度

一下是求一个数组中最大值的迭代写法与递归写法,对比一下两则的时间复杂度。

function getMax(arr) {
  let max = Number.MIN_SAFE_INTEGER
  for (let i = 0; i < arr.length; i++) {
    max = Math.max(max, arr[i])
  }
  return max
}

对于迭代版本,无非就是一个遍历。时间复杂度就是O(n)

function getMax(arr, l = 0, r = arr.length - 1) {
  if (l === r) return arr[l]
  // 取中间数
  let mid = l + ((r - l) >> 1)
  return Math.max(getMax(arr, l, mid), getMax(arr, mid+1, r))
}

对于递归版本我们需要使用master公式: 一个数组每次分成两份,比较左边的最大值和右边的最大值,即为整个数组的最大值。

master公式

T(N) = a * T(n/b) + O(n^d),简单解释一下这个公式:

  1. T(N):代表母问题
  2. a * 代表子问题在一次递归中执行了多少次。对于上诉方法思路,求左边的最大值和求右边的最大值,所以a等于2
  3. T(n/b) 子问题的规模。每次递归都是去中间值,一分为2,所以b为2
  4. O(n^d) 除了递归部分,剩下程序的时间复杂大。对于上诉程序,除去递归,就是一些判断,求中间值,比较左边与右边哪个值大,所以为:O(1)

所以此问题的master公式为: T(N) = 2*T(n/2) + O(1) ===> a=2 b=2 d=0。 调用一下结论:

image.png

log(b,a) = log(2,2) = 1。 因为1>0 所以时间复杂度为: O(n^log(b,a)) = n ^ 1 = O(n)。后续可以用此方法分析归并排序。

关于O(logN)算法的底数

对于O(logN)复杂度的算法,比较让人疑惑的点有:

  1. logN的底数到底是多少
  2. logN的底数能够省略

通过二分查找发来看看底数问题

分析二分查找法的执行次数

对于数组arr=[1,2,3,4...100],这个数组,我们找一个数,到底要找多少次?举一个最坏的情况,我们找的书就是arr[0], 划分范围如下:

  • 0 ~ 49
  • 0 ~ 24
  • 0 ~ 12
  • 0 ~ 6
  • 0 ~ 3
  • 0 ~ 1
  • 0 ~ 0

每次划分范围都是上一次的基础上除以2,可以换个思路就是100能够除以多少次2,也就是log(2,100)。所以二分查找的底数为2。

总结来说就是对于二分查找,我们最多只需要log(2,n)次就能找到我们要的数

for (let i = 0; i < 1000; i *= 10) {
    console.log(i)
  }

对于这个程序,它每个乘以10,意思就是乘以多少次10到1000。次数就是log(10, 100)。也就是log(10,n)

为什么底数可以省略

之所以 底数可以省略,是因为数学公式:

log(a,n) = log (a, b) * log (b, n) ==> log(a,n) / log (b, n) = log(a,b)

其中log(a,b)为一个常数,相当于 log(a,n)与log(b,n)是一个倍数关系,在时间复杂度中,常数项的倍数可以被省略。

这就是log(N)算法没有底数的原因。