时间与空间复杂度分析

212 阅读3分钟

空间复杂度

概念

表示算法的存储空间与数据规模间的增长关系

分析方法

只关注这段代码中申请的最大空间

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

上面代码第二行申请了一个空间存储 i,它是常量阶的,跟 n 无关,可以忽略。 第三行申请了一个大小为 n 的 int 数组,除此之外,没有申请其他空间,所以这段代码的空间复杂度为 O(n)。

时间复杂度

概念

表示算法的执行时间与数据规模之间的增长关系

分析方法

  • 只关注循环次数最多的一段代码
/**
 * 函数中第一、二行代码执行 1 次
 * 第三、四行代码执行了 n 次,所以总复杂度为 O(n)
 */
function cal(n) {
  let sum = 0
  let i = 1
  for (; i <= n; i++) {
    sum += i
  }
  return sum
}

  • 加法法则:总复杂度等于量级最大的那段代码的复杂度
/**
 * 第一个 for 循环复杂度为常量级 O(1),第二个是 n,复杂度为 O(n)
 * 所以总复杂度为 O(n)
 * 注:只要是一个已知数(如:100,10000,1000000)都是常量级,复杂度为O(1) 
 */
function cal(n) {
  let i = 1
  let sum_1 = 0
  for (; i <= 100; i++) {
    sum_1 += i
  }

  let sum_2 = 0
  for (; i <= n; i++) {
    sum_2 += i
  }
  return sum_1 + sum_2
}
  • 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
/**
 * cal 中嵌套了函数 fn,fn 的总复杂度为 O(n),所以 cal 的总复杂度为 O(n²)
 */
function cal(n) {
  let i = 1
  let sum = 0
  for (; i <= n; i++) {
    sum += fn(i)
  }
}

function fn(n) {
  let sum = 0
  for (; i <= n; i++) {
    sum += i
  }
  return sum
}

对数阶时间复杂度分析

let i = 1
while (i <= n) {
  i = i * 2
}

根据上面的分析方法,找出执行次数最多的一行,为第 3 行; 可以看出 i 的值为每次乘以 2 的等比数列。当 2 的 k 次方大于 n 时,结束循环。

k 就为第 3 行的最大执行次数 2^k = n, k = log 以 2 为底的 n 次方。因为对数之间可以相互转换,不管以什么为底,我们都统一记为 O(logn)

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

/**
 * @param {Array} arr 
 * @param {Number} n 数组的长度
 * @param {*} x 要查找的值
 */
function find (arr, n, x) {
  let i = 0
  let pos = -1
  for (i; i < n; i++) {
    if (arr[i] === x) {
      pos = i
      break
    }
  }
  return pos
}

上面这段代码在 arr[0] === x 时,为最好情况,此时时间复杂度为 O(1);
arr[n] === x 时,为最坏情况,此时时间复杂度为 O(n)

平均情况时间复杂度

上节代码要查找 x 在数组中的位置,有 n+1 种情况,分别是 x 在 0 ~ n-1 的位置中和不在数组中。

x 在索引为 0 处时,遍历 1 次,在索引为 1 时遍历 2 次,总遍历次数为 1 + 2 + 3 + ... + n + (n+1) = n(n+2)/2。(等差数列求和公式)。

平均每种情况要遍历的次数为 n(n+2)/2(n+1) ≈ n/2,平均情况的时间复杂度就为 O(n)

参考:数据结构与算法之美